cs

Typy danych, wyrażenia warunkowe i instrukcje sterujące, wskaźniki w języku C#.

  1. Wymień typy proste w C#, jakie wartości mogą przechowywać?
  2. Typ wartościowy a referencyjny, jakie są różnice?
  3. Do czego służy default w C#?
  4. Jakie są

Typy proste w C#

Typy proste w C# to fundament języka, który pozwala na przechowywanie różnych rodzajów danych, takich jak liczby całkowite, zmiennoprzecinkowe, logiczne czy znaki. Są one szybkie i efektywne pod względem zużycia pamięci, a ich wybór zależy od rodzaju danych, które musimy przechować. Wszystkie typy proste, oprócz string, są typami wartościowymi, co oznacza, że dane są przechowywane bezpośrednio w zmiennej.

Typy całkowite

Typy całkowite w C# służą do przechowywania liczb całkowitych, czyli takich, które nie mają części ułamkowej. Podstawowe typy to byte, sbyte, short, ushort, int, uint, long i ulong. Różnią się one zakresem wartości oraz tym, czy mogą przechowywać tylko liczby dodatnie (byte, ushort, uint, ulong), czy również ujemne (sbyte, short, int, long). Najczęściej używanym typem jest int, który obejmuje wartości od -2 147 483 648 do 2 147 483 647. Wybór odpowiedniego typu zależy od potrzeb programisty – im mniejszy zakres, tym mniejsze zużycie pamięci.

Wartości domyślne dla typów całkowitych w C# to 0. Oznacza to, że jeśli zmienna całkowita nie zostanie jawnie zainicjalizowana, otrzyma wartość zero. Dotyczy to zarówno pól w klasach, jak i struktur, ale w przypadku zmiennych lokalnych kompilator wymaga ich inicjalizacji przed użyciem. Na przykład, int myNumber; w metodzie spowoduje błąd, jeśli spróbujemy go odczytać bez wcześniejszego przypisania wartości.

Console.WriteLine($"byte:     Min = {byte.MinValue}, Max = {byte.MaxValue}, Default = {default(byte)}");
Console.WriteLine($"sbyte:    Min = {sbyte.MinValue}, Max = {sbyte.MaxValue}, Default = {default(sbyte)}");
Console.WriteLine($"short:    Min = {short.MinValue}, Max = {short.MaxValue}, Default = {default(short)}");
Console.WriteLine($"ushort:   Min = {ushort.MinValue}, Max = {ushort.MaxValue}, Default = {default(ushort)}");
Console.WriteLine($"int:      Min = {int.MinValue}, Max = {int.MaxValue}, Default = {default(int)}");
Console.WriteLine($"uint:     Min = {uint.MinValue}, Max = {uint.MaxValue}, Default = {default(uint)}");
Console.WriteLine($"long:     Min = {long.MinValue}, Max = {long.MaxValue}, Default = {default(long)}");
Console.WriteLine($"ulong:    Min = {ulong.MinValue}, Max = {ulong.MaxValue}, Default = {default(ulong)}");

byte:     Min = 0, Max = 255
sbyte:    Min = -128, Max = 127
short:    Min = -32768, Max = 32767
ushort:   Min = 0, Max = 65535
int:      Min = -2147483648, Max = 2147483647
uint:     Min = 0, Max = 4294967295
long:     Min = -9223372036854775808, Max = 9223372036854775807
ulong:    Min = 0, Max = 18446744073709551615

Typy zmienno przecinkowe - float i double przechowują liczby w formacie zmiennoprzecinkowym (IEEE 754), co oznacza, że mają zaokrąglenia binarne i mogą wprowadzać błędy precyzji.

Console.WriteLine($"float:    Min = {float.MinValue}, Max = {float.MaxValue}, Default = {default(float)}");
Console.WriteLine($"double:   Min = {double.MinValue}, Max = {double.MaxValue} Default = {default(double)}");

Console.WriteLine(0.1 + 0.2);  // Wynik: 0.30000000000000004
float:    Min = -3.4028235E+38, Max = 3.4028235E+38
double:   Min = -1.7976931348623157E+308, Max = 1.7976931348623157E+308
0.30000000000000004

Typ decimal przechowuje liczby jako liczby całkowite z przesuniętą kropką dziesiętną (stała skala), dzięki czemu nie traci precyzji przy obliczeniach finansowych.

Console.WriteLine($"decimal:  Min = {decimal.MinValue}, Max = {decimal.MaxValue}, Default = {default(decimal)}");

decimal a = 0.1m;
decimal b = 0.2m;
Console.WriteLine(a + b);  // Wynik: 0.3
decimal:  Min = -79228162514264337593543950335, Max = 79228162514264337593543950335
0.3

Typ logiczny bool przechowuje wartości true i false.

bool var = default; // domyślnie przechowuje wartość false
if (var)
{
    Console.WriteLine("domyślna wartość to true");
}else{
    Console.WriteLine("domyślna wartość to false");
}

bool isReady = true;
bool isLoggedIn = false;
domyślna wartość to false

Typ char przechowuje jeden znak w formacie Unicode.

 char letter = 'A';
 char symbol = '@';
 char unicodeChar = '\u263A'; // ☺
 char defaultValue = default; // wartosć domyślna'\0'

 Console.WriteLine(letter);  // A
 Console.WriteLine(symbol);  // @
 Console.WriteLine($"`{unicodeChar}`"); // ☺
 Console.WriteLine($"`{defaultValue}`");

Konsola może mieć ustawione kodowanie inne niż unicode. Wtedy tekst w konsoli nie będzie poprawnie wyświetlany. Aby zmienić kodowanie w konsoli cmd.exe należy wywołac polecenie chcp 65001 które zmienia kodowanie na UTF-8.

A
@
`☺`
``

Typ string jest używany do przechowywania tekstu, czyli sekwencji znaków. Jest to typ referencyjny, co oznacza, że przechowuje odwołanie do danych w pamięci, a nie same dane.

string imie = "Jan";
string nazwisko = "Kowalski";
string pelneImie = imie + " " + nazwisko; // Wynik: "Jan Kowalski"
pelneImie = $"{imie} {nazwisko}"; // Wynik: "Jan Kowalski"

string tekst1 = "Hello";
string tekst2 = "Hello";
int dlugosc = imie.Length; // Wynik: 3
bool saRowne = tekst1 == tekst2; // Wynik: true - porównuje wartości nie tylko referencje

string tekst = "Hello World!";
string duzeLitery = tekst.ToUpper(); // "HELLO, WORLD!"
string maleLitery = tekst.ToLower(); // "hello, world!"
bool zawiera = tekst.Contains("World"); // true
bool zawieraNie = tekst.Contains("CSharp"); // false

tekst = "apple,banana,orange";
string[] owoce = tekst.Split(','); // Tablica: ["apple", "banana", "orange"]

tekst = "   Hello, World!   ";
string przycietyTekst = tekst.Trim(); // "Hello, World!"

tekst = "apple, banana, apple, orange";
string nowyTekst = tekst.Replace("apple", "grape"); // "grape, banana, grape, orange"

tekst1 = "Hello";
tekst2 = "hello";
saRowne = tekst1.Equals(tekst2, StringComparison.OrdinalIgnoreCase); // true (ignoruje wielkość liter)

int porownanie = string.Compare(tekst1, tekst2, StringComparison.OrdinalIgnoreCase); // Zwraca 0, jeśli ciągi są równe (ignorując wielkość liter), < 0, jeśli tekst1 < tekst2, > 0 jeśli tekst1 > tekst2

tekst = "Hello, World!";
string podciag = tekst.Substring(7, 5); // "World"


string[] owoce2 = { "apple", "banana", "orange" };
string polaczony = String.Join(", ", owoce2); // "apple, banana, orange"

double liczba = 12345.6789;
string sformatowany = $"Kwota: {liczba:C}"; // "Kwota: 12,345.68 zł" (format waluty)

typ `string` jest niemutowalny (immutable), co oznacza, że nie możemy edytować pojedynczych znaków w istniejącym ciągu znaków.

string var = "Hello world!";
var[6] = 'W'; //`property indexer is read only` - możemy tylko odczytać wartość
string tekst = "Hello";
        
// Użycie StringBuilder do zmiany litery
StringBuilder sb = new StringBuilder(tekst);
sb[1] = 'a';  // Zmieniamy drugą literę ('e' na 'a')

string nowyTekst = sb.ToString();  // "Hallo"

Console.WriteLine(nowyTekst);  // Wyświetli: Hallo

char[] tablica = nowyTekst.ToCharArray();
        
// Zmiana litery na indeksie 1
tablica[1] = 'e';  // Zmieniamy 'a' na 'e'

// Tworzymy nowy string z tablicy znaków
nowyTekst = new string(tablica);  // "Hallo"
// Metoda Console.WriteLine() korzysta z metody ToString() stąd przy samej operacji wypisania można przekazać tablicę znaków.
Console.WriteLine(tablica);  // Wyświetli: Hallo

Console.WriteLine(nowyTekst);  // Wyświetli: Hallo

nowyTekst = tekst.Substring(0, 1) + 'a' + tekst.Substring(2);  // "Hallo"
Console.WriteLine(nowyTekst);

Tpy nullable

Typy całkowite w C# są typami wartościowymi (value types), co oznacza, że nie mogą przechowywać wartości null. Próba przypisania null do zmiennej typu całkowitego (int myNumber = null;) spowoduje błąd kompilacji. Jeśli jednak chcemy, aby zmienna mogła przyjmować zarówno liczby, jak i null, musimy użyć typu nullable, czyli int?. Dzięki temu int? myNumber = null; jest poprawne. W C# dla typów nullable (int?, long?, itp.) dostępne są dwie właściwości:

Wskaźniki

Wskaźnik (pointer) to zmienna, która przechowuje adres pamięci innej zmiennej. Dzięki wskaźnikom możemy manipulować danymi bezpośrednio w pamięci, co daje większą kontrolę nad zasobami, ale wymaga ostrożności, aby uniknąć błędów, takich jak dostęp do pamięci niealokowanej przez pisany program. Bloki które zawierają instrukcje manipulijące wskaźnikami wymagają specjalnego oznaczenia unsafe.

using System;

class Program
{
    unsafe static void Main()
    {
        int x = 10;
        int* ptr = &x;  // Pobieramy adres zmiennej x

        Console.WriteLine($"Wartość x: {x}");
        Console.WriteLine($"Adres x: {(ulong)ptr}"); // Konwersja na typ liczbowy do wyświetlenia
        Console.WriteLine($"Wartość pod adresem ptr: {*ptr}");

        *ptr = 20; // Zmiana wartości przez wskaźnik
        Console.WriteLine($"Nowa wartość x: {x}");
    }
}
using System;

class Program
{
    unsafe static void Main()
    {
        int[] numbers = { 1, 2, 3, 4, 5 };

        fixed (int* ptr = numbers) // "Przytwierdzenie" tablicy w pamięci
        {
            for (int i = 0; i < numbers.Length; i++)
            {
                Console.WriteLine($"Element {i}: {*(ptr + i)}");
            }
        }
    }
}

using System;

struct Point
{
    public int X;
    public int Y;
}

class Program
{
    unsafe static void Main()
    {
        Point p;
        p.X = 10;
        p.Y = 20;

        Point* ptr = &p;
        Console.WriteLine($"X: {ptr->X}, Y: {ptr->Y}");

        ptr->X = 30; // Zmiana wartości przez wskaźnik
        Console.WriteLine($"Nowy X: {p.X}");
    }
}

Dynamiczna alokacja pamięci

using System;

class Program
{
    unsafe static void Main()
    {
        int* ptr = stackalloc int[5]; // Alokacja 5 elementów na stosie

        for (int i = 0; i < 5; i++)
        {
            ptr[i] = i * 10; // Przypisujemy wartości
            Console.WriteLine(ptr[i]);
        }
    }
}

Struktury

public struct NazwaStruktury
{
    // Pola struktury
    public int pole1;
    public double pole2;

    // Konstruktor
    public NazwaStruktury(int p1, double p2)
    {
        pole1 = p1;
        pole2 = p2;
    }

    // Metoda w strukturze
    public void PokazDane()
    {
        Console.WriteLine($"Pole1: {pole1}, Pole2: {pole2}");
    }
}

Słowa kluczowe związane z prekazywaniem przez referencję

Metoda może zwracać referencję na istniejącą zmienną, zamiast zwracać jej kopię.

class Program
{
    static ref int ZnajdzNajwiekszy(ref int a, ref int b)
    {
        return ref (a > b ? ref a : ref b);
    }

    static void Main()
    {
        int x = 10, y = 20;
        ref int max = ref ZnajdzNajwiekszy(ref x, ref y);

        max = 50; // Modyfikujemy wartość x lub y!
        Console.WriteLine(x); // 10 lub 50 (zależnie od wartości)
        Console.WriteLine(y); // 50 lub 20
    }
}

Od C# 7 out jest często używany do zwracania wielu wartości jako alternatywa dla Tuple:

class Program
{
    static void PobierzDane(out string imie, out int wiek) // funkcja "zwraca" wartośći imie oraz wiek
    {
        imie = "Adam";
        wiek = 25;
    }

    static void Main()
    {
        PobierzDane(out var imie, out var wiek);
        Console.WriteLine($"{imie}, {wiek} lat"); // Adam, 25 lat
    }
}

in jest używane w strukturach (struct), aby uniknąć niepotrzebnego kopiowania.

struct WielkiObiekt
{
    public int A, B, C, D, E, F, G;
}

class Program
{
    static void Wyswietl(in WielkiObiekt obj)
    {
        // obj.A = 10; // `in` blokuje modyfikację
        Console.WriteLine(obj.A);
    }
}

params można używać w konstruktorach do obsługi dowolnej liczby parametrów.

class Klasa
{
    public Klasa(params int[] liczby)
    {
        Console.WriteLine($"Podano {liczby.Length} liczb.");
    }
}

class Program
{
    static void Main()
    {
        Klasa k1 = new Klasa(1, 2, 3, 4);
        Klasa k2 = new Klasa(10, 20);
    }
}

Zadania

Napisz funkcję, która zamienia wartości dwóch liczb przy użyciu ref.

int a = 5, b = 10;
Zamien(ref a, ref b);
Console.WriteLine($"{a}, {b}"); // 10, 5

Napisz metodę, która próbuje zamienić tekst na liczbę całkowitą. Jeśli się nie uda, zwróci wartość domyślną 0.

string input = "123";
if (ParsujLiczbe(input, out int liczba))
   Console.WriteLine($"Poprawnie sparsowano: {liczba}");
else
   Console.WriteLine("Nie udało się sparsować liczby.");

Stwórz strukturę Wektor3D, która zawiera x, y, z. Napisz metodę, która oblicza długość wektora przy użyciu in.

Wektor3D w = new Wektor3D(3, 4, 5);
Console.WriteLine(DlugoscWektora(in w)); // 7.071

Napisz metodę, która oblicza średnią z dowolnej liczby podanych argumentów.

double wynik = Srednia(2, 4, 6, 8, 10);
Console.WriteLine(wynik); // 6.0

Stwórz ref struct Macierz2x2, który przechowuje dwuwymiarową macierz i umożliwia operacje na niej bez kopiowania.

Macierz2x2 m = new Macierz2x2(1, 2, 3, 4);
Macierz2x2 wynik = m.Transponuj();

Napisz metodę, która zwraca referencję na największy element w tablicy i pozwala go zmienić.

int[] liczby = { 1, 5, 3, 9, 7 };
ref int max = ref ZnajdzNajwiekszy(liczby);
max = 100;
Console.WriteLine(string.Join(", ", liczby)); // 1, 5, 3, 100, 7

Napisz metodę, która rozszerza tablicę dynamicznie, dodając nowy element. Tablica powinna być przekazywana przez ref, aby zmiana była widoczna poza metodą.

int[] liczby = { 1, 2, 3 };
DodajElement(ref liczby, 4);
Console.WriteLine(string.Join(", ", liczby)); // 1, 2, 3, 4

Podpowiedź: Wewnątrz metody utwórz nową tablicę o większym rozmiarze i skopiuj do niej dane.

// Tworzymy nową tablicę o rozmiarze +1
int[] nowaTablica = new int[tablica.Length + 1];

Napisz metodę, która parsuje ustawienia konfiguracyjne z pliku tekstowego (key=value) i zwraca je jako Dictionary<string, string> przez out

string[] linie = { "username=admin", "password=1234", "timeout=30" };
if (ParsujKonfiguracje(linie, out var config))
{
   Console.WriteLine(config["username"]); // admin
}

Podpowiedź: Użyj Split(‘=’) do podziału kluczy i wartości.

Zaimplementuj strukturę Macierz3x3, która przechowuje macierz 3x3 i posiada metodę Wyznacznik(). Skorzystaj z in, aby unikać niepotrzebnego kopiowania w funkcji liczącej wyznacznik.

Macierz3x3 m = new Macierz3x3(1, 2, 3, 4, 5, 6, 7, 8, 9);
double det = Wyznacznik(in m);
Console.WriteLine(det);

Stwórz metodę, która przyjmuje dowolną liczbę liczb (params double[]), a następnie zwraca statystyki: średnia, minimum, maksimum, suma.

var (srednia, min, max, suma) = Statystyki(10, 20, 30, 40);
Console.WriteLine($"Średnia: {srednia}, Min: {min}, Max: {max}, Suma: {suma}");

Podpowiedź: Skorzystaj z Tuple<double, double, double, double> jako wynik metody.