Przejdź do treści
School IT / Przestrzeń do nauki
← C# od podstawLekcja 51 z 58

Struktury — małe wartości, które kopiujemy

Pozycja pionka składa się z dwóch współrzędnych. Gdy kopiujesz pozycję, zwykle chcesz niezależną wartość, a nie drugą nazwę tego samego zmiennego obiektu.

C# struct readonly struct record struct typ wartościowy ref 90 min
CEL LEKCJI

Czego się dziś nauczysz

  • Wyjaśnisz, dlaczego wywołanie metody zmieniającej strukturę w List<T> nie daje żadnego skutku.
  • Odróżnisz typ wartościowy od referencyjnego i wskażesz, co zawiera zmienna w każdym z nich.
  • Wymienisz sześć miejsc, w których struktura jest kopiowana, i wskażesz jedyne, gdzie kopii nie ma.
  • Napiszesz readonly struct ze zmianą przez zwrócenie nowej wartości.
  • Użyjesz zapisu readonly record struct i wyjaśnisz, co kompilator dopisał za Ciebie.
  • Porównasz zachowanie Equals i == dla struktury oraz dla klasy.
  • Rozstrzygniesz czterema warunkami, czy dany typ powinien być strukturą, czy klasą.
  • Rozpoznasz komunikaty CS1612, CS1604, CS0019 i CS0527.

Przygotowanie: lekcje 01–49. Przewidywany czas: 90 min z zadaniami. Przykłady wymagają .NET 8 lub nowszego, z włączonymi ImplicitUsings i Nullable.

TEORIA

Licznik, który nie liczy

W lekcji 49 poznałeś struct od strony wygody: mały typ, kopia przy przypisaniu. Ta lekcja pokazuje, co z tego wynika — bo błędy z tym związane są ciche i wyjątkowo trudne do znalezienia.

Napiszmy licznik jako strukturę ze zmiennym polem. Zapis, który wygląda całkowicie niewinnie:

definicja typu
struct Licznik
{
    public int Wartosc;

    public void Zwieksz()
    {
        Wartosc++;
    }
}

Na zwykłej zmiennej działa dokładnie tak, jak się spodziewasz:

Program.cs
Licznik a = new Licznik();
a.Zwieksz();
a.Zwieksz();
Console.WriteLine(a.Wartosc);       // 2

W tablicy — też:

Program.cs
Licznik[] tab = new Licznik[1];
tab[0].Zwieksz();
tab[0].Zwieksz();
Console.WriteLine(tab[0].Wartosc);  // 2

A teraz to samo w List<T>:

Program.cs
List<Licznik> lista = new List<Licznik> { new Licznik() };
lista[0].Zwieksz();
lista[0].Zwieksz();
Console.WriteLine(lista[0].Wartosc);   // 0  ???
wynik — zero, nie dwa
0

Najbardziej podstępny błąd w tej lekcji

Program się skompilował, nie zgłosił ostrzeżenia i nic nie policzył. lista[0] nie jest zmienną — to wynik metody pobierającej element. Wynik ten jest kopią struktury, a Zwieksz() zwiększył wartość w kopii, która zaraz potem przestała istnieć. Tablica zachowuje się inaczej, bo tab[0] daje dostęp do samego elementu, nie do jego kopii.

Gdybyś próbował zmienić pole wprost, kompilator przynajmniej by cię ostrzegł:

Program.cs
// lista[0].Wartosc = 5;        // BLAD CS1612 - nie mozna zmieniac wyniku metody

Ale wywołanie metody, która zmienia pole, przechodzi w ciszy. Dlatego cała ta lekcja sprowadza się do jednej zasady, którą wyprowadzimy krok po kroku: struktury pisz jako niezmienne. Wtedy taki błąd nie ma jak powstać.

TEORIA

Dwie grupy typów w C#

W lekcji 43 poznałeś różnicę między obiektem a odwołaniem do niego. Teraz ta różnica dzieli wszystkie typy w C# na dwie grupy.

Typ wartościowyTyp referencyjny
Co zawiera zmiennasamą wartośćadres obiektu
Słowo kluczowestruct, enumclass, interface, tablica
Przykłady wbudowaneint, double, bool, char, decimal, DateTimestring, List<T>, object, każda Twoja klasa
Przypisaniekopiuje zawartośćkopiuje adres — dwa odwołania do jednego obiektu
Wartość początkowa polawszystkie pola zerowenull
Może być nullnie (chyba że Typ?)tak
Equals domyślnieporównuje polaporównuje adresy
Dziedziczenienietak

Dwa programy różniące się jednym słowem dają różne wyniki — to najkrótszy możliwy dowód:

wersja ze struct
Pozycja a = new Pozycja(2, 3);
Pozycja b = a;
b.Rzad = 9;

Console.WriteLine($"a = {a}, b = {b}");

struct Pozycja                      // STRUKTURA
{
    public int Rzad;
    public int Kolumna;

    public Pozycja(int rzad, int kolumna) { Rzad = rzad; Kolumna = kolumna; }

    public override string ToString() { return $"({Rzad},{Kolumna})"; }
}
wynik w konsoli
a = (2,3), b = (9,3)
wersja z class
Pozycja a = new Pozycja(2, 3);
Pozycja b = a;
b.Rzad = 9;

Console.WriteLine($"a = {a}, b = {b}");

class Pozycja                       // KLASA - jedyna zmiana
{
    public int Rzad;
    public int Kolumna;

    public Pozycja(int rzad, int kolumna) { Rzad = rzad; Kolumna = kolumna; }

    public override string ToString() { return $"({Rzad},{Kolumna})"; }
}
wynik w konsoli
a = (9,3), b = (9,3)

Jak to sobie wyobrazić

Zmienna typu wartościowego to kartka z zapisaną wartością. Kopiujesz kartkę — masz dwie niezależne kartki. Zmienna typu referencyjnego to kartka z adresem. Kopiujesz kartkę — masz dwa adresy tego samego domu, a przemalowanie domu widać z oba adresów.

TEORIA

Sześć miejsc i jedno bez kopii

Kopiowanie zachodzi w sześciu miejscach i warto je znać wszystkie, bo w każdym może zaskoczyć.

1. Przypisanie do zmiennej

Program.cs
Pozycja a = new Pozycja(2, 3);
Pozycja b = a;                 // kopia

2. Przekazanie do metody

Metoda dostaje kopię, więc jej zmiany nie wychodzą na zewnątrz — chyba że użyjesz ref z lekcji 20:

Program.cs
static void Bez(Pozycja p)
{
    p.Rzad = 100;              // zmienia kopie
}

static void Z(ref Pozycja p)
{
    p.Rzad = 100;              // zmienia zmienna wywolujacego
}

Pozycja x = new Pozycja(1, 1);

Bez(x);
Console.WriteLine(x);          // (1,1)

Z(ref x);
Console.WriteLine(x);          // (100,1)

Tu jest realna różnica wobec klas

Gdyby Pozycja była klasą, wywołanie Bez(x) zmieniłoby obiekt, bo metoda dostałaby adres. To ten sam kod i inny skutek — dokładnie dlatego trzeba wiedzieć, czy dany typ jest strukturą, czy klasą.

3. Wynik metody i właściwości

Program.cs
class Gracz
{
    public Pozycja Pozycja { get; private set; }

    public Gracz(Pozycja p) { Pozycja = p; }
}

Gracz g = new Gracz(new Pozycja(0, 0));

// g.Pozycja.Rzad = 5;         // BLAD CS1612 - wynik wlasciwosci to kopia

Pozycja p = g.Pozycja;         // trzeba wziac kopie...
p.Rzad = 5;                    // ...zmienic ja...
Console.WriteLine(g.Pozycja);  // (0,0) - a obiekt gracza sie nie zmienil

4. Element tablicy — tu kopii nie ma

Program.cs
Pozycja[] tab = new Pozycja[3];
tab[0].Rzad = 7;               // WOLNO - dostep bezposredni do elementu
Console.WriteLine(tab[0]);     // (7,0)

5. Element List<T> — tu kopia jest

Program.cs
List<Pozycja> lista = new List<Pozycja> { new Pozycja(0, 0) };

// lista[0].Rzad = 7;          // BLAD CS1612

Pozycja tmp = lista[0];        // poprawny sposob:
tmp.Rzad = 7;                  // 1. wez kopie, 2. zmien ja,
lista[0] = tmp;                // 3. wstaw z powrotem
Console.WriteLine(lista[0]);   // (7,0)
ZapisTablica Pozycja[]Lista List<Pozycja>
x[0].Pole = 5;działaCS1612
x[0].MetodaZmieniajaca();działakompiluje się i nic nie robi
x[0] = nowaWartosc;działadziała
odczyt x[0].Poledziaładziała

6. Wrzucenie do object

Program.cs
Pozycja p = new Pozycja(1, 2);
object o = p;                  // kopia trafia na sterte ("pakowanie")
Pozycja q = (Pozycja)o;        // i jest z powrotem kopiowana

Dlatego tablica object[] pełna struktur jest wolniejsza od tablicy Pozycja[] — przy każdym włożeniu i wyjęciu powstaje kopia na stercie. W programach szkolnych to bez znaczenia, ale warto wiedzieć, skąd się bierze.

TEORIA

readonly struct i nowa wartość

Wszystkie kłopoty z tej lekcji mają jedną wspólną przyczynę: zmienne pole w strukturze. Usuń możliwość zmiany, a kłopoty znikają razem z nią.

Krok 1: właściwości tylko do odczytu

definicja typu
struct Pozycja
{
    public int Rzad { get; }        // bez set
    public int Kolumna { get; }

    public Pozycja(int rzad, int kolumna)
    {
        Rzad = rzad;
        Kolumna = kolumna;
    }

    public override string ToString() { return $"({Rzad},{Kolumna})"; }
}

Teraz lista[0].Rzad = 7 nie jest już „podstępnym błędem” — jest zwykłym błędem kompilacji, bo takiego przypisania nie da się napisać nigdzie.

Krok 2: zmiana przez zwrócenie nowej wartości

Skoro nie da się zmienić istniejącej pozycji, metoda zwraca nową. Tak samo działają metody string z lekcji 08 — ToUpper() nie zmienia napisu, tylko zwraca drugi:

dopisz w strukturze Pozycja
    public Pozycja Przesun(int oRzad, int oKolumne)
    {
        return new Pozycja(Rzad + oRzad, Kolumna + oKolumne);
    }
Program.cs
Pozycja p = new Pozycja(2, 3);
p = p.Przesun(1, 0);           // przypisanie wyniku jest konieczne
Console.WriteLine(p);          // (3,3)

Pozycja q = new Pozycja(2, 3);
q.Przesun(1, 0);               // wynik wyrzucony - ostrzezenie i zero skutku
Console.WriteLine(q);          // (2,3)

Krok 3: readonly struct

Słowo readonly przed struct to deklaracja dla kompilatora: „ta struktura jest niezmienna, pilnuj tego”:

definicja typu
readonly struct Pozycja
{
    public int Rzad { get; }
    public int Kolumna { get; }

    public Pozycja(int rzad, int kolumna)
    {
        Rzad = rzad;
        Kolumna = kolumna;
    }

    // public void Przesun() { Rzad++; }      // BLAD CS1604 - nie wolno zmieniac pola

    public Pozycja Przesun(int oRzad, int oKolumne)
    {
        return new Pozycja(Rzad + oRzad, Kolumna + oKolumne);
    }

    public override string ToString() { return $"({Rzad},{Kolumna})"; }
}

Zasada, którą warto zapamiętać

Jeśli piszesz struct, napisz readonly struct. Kosztuje jedno słowo, a zamyka całą rodzinę błędów opisanych na początku tej lekcji. Kompilator od razu powie ci, gdy spróbujesz zrobić coś, czego struktura nie powinna robić.

Krótszy zapis: readonly record struct

Od C# 10 tę samą strukturę można zapisać w jednym wierszu. Kompilator sam dopisze konstruktor, właściwości, ToString(), porównywanie i operator ==:

definicja typu
readonly record struct Pozycja(int Rzad, int Kolumna);
Program.cs
Pozycja a = new Pozycja(2, 3);
Pozycja b = new Pozycja(2, 3);

Console.WriteLine(a);              // Pozycja { Rzad = 2, Kolumna = 3 }
Console.WriteLine(a == b);         // True - operator dostales za darmo
Console.WriteLine(a.Equals(b));    // True

Pozycja c = a with { Rzad = 9 };   // kopia z jedna zmieniona wartoscia
Console.WriteLine(c);              // Pozycja { Rzad = 9, Kolumna = 3 }

To wygodny skrót do prostych wartości. Gdy potrzebujesz własnego ToString() albo sprawdzania w konstruktorze, wróć do pełnego zapisu readonly struct — oba rozwiązania są poprawne.

TEORIA

Equals i == zachowują się odwrotnie

Porównywanie to miejsce, w którym struktury i klasy zachowują się odwrotnie, niż się spodziewasz — i warto to sprawdzić raz, na spokojnie.

Program.cs
struct PozycjaS { public int R; public int K; }
class  PozycjaK { public int R; public int K; }

PozycjaS s1 = new PozycjaS { R = 1, K = 1 };
PozycjaS s2 = new PozycjaS { R = 1, K = 1 };
Console.WriteLine(s1.Equals(s2));      // True  - porownanie PO POLACH
// Console.WriteLine(s1 == s2);        // BLAD CS0019 - operatora nie ma

PozycjaK k1 = new PozycjaK { R = 1, K = 1 };
PozycjaK k2 = new PozycjaK { R = 1, K = 1 };
Console.WriteLine(k1.Equals(k2));      // False - porownanie PO ADRESIE
Console.WriteLine(k1 == k2);           // False - to samo
Console.WriteLine(k1 == k1);           // True  - ten sam obiekt
structclass
Equals bez przesłonięciaporównuje pola → Trueporównuje adresy → False
== bez definicjinie kompiluje się (CS0019)porównuje adresy
record struct / record class== i Equals po polach, gotowe== i Equals po polach, gotowe
string—wyjątek: == porównuje treść

Dlaczego string zachowuje się inaczej

string jest klasą, ale jego autorzy zdefiniowali operator == tak, żeby porównywał treść — bo tego wszyscy oczekują. To wyjątek zrobiony ręcznie, nie reguła. Twoja klasa nie dostanie takiego zachowania sama z siebie.

Jeśli chcesz == dla własnej struktury

Najprościej użyć readonly record struct z poprzedniej sekcji. Jeśli jednak potrzebujesz pełnej kontroli, operator trzeba zdefiniować samemu — a kompilator wymaga przy tym dwóch przesłonięć:

definicja typu
readonly struct Pozycja
{
    public int Rzad { get; }
    public int Kolumna { get; }

    public Pozycja(int rzad, int kolumna) { Rzad = rzad; Kolumna = kolumna; }

    public static bool operator ==(Pozycja a, Pozycja b)
    {
        return a.Rzad == b.Rzad && a.Kolumna == b.Kolumna;
    }

    public static bool operator !=(Pozycja a, Pozycja b)
    {
        return !(a == b);
    }

    public override bool Equals(object? obj)
    {
        return obj is Pozycja p && this == p;
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(Rzad, Kolumna);
    }
}

Operatory == i != zawsze definiuje się parą, a kompilator dopomina się też o Equals i GetHashCode (ostrzeżenia CS0660 i CS0661) — bez nich Dictionary z lekcji 32 zachowywałby się niezgodnie z operatorem. Przy pojedynczym zadaniu szkolnym to przerost formy; wystarczy Equals albo record struct.

TEORIA

Cztery warunki i decyzja

Ostateczna odpowiedź jest krótsza, niż się wydaje po pięciu sekcjach ostrzeżeń.

Reguła na egzamin i na życie

Wybierz struct, gdy wszystkie cztery warunki są spełnione:

  1. typ opisuje jedną wartość, nie rzecz z tożsamością i historią;
  2. jest mały — orientacyjnie do 16 bajtów, czyli około czterech liczb int;
  3. jest niezmienny — po utworzeniu nic się w nim nie zmienia;
  4. porównywanie po zawartości ma sens: dwie pozycje (2,3) to dla Ciebie to samo.

W każdym innym przypadku wybierz class. Jeśli się wahasz — class. To jest właściwa odpowiedź domyślna i nie ma w niej nic gorszego.

TypWybórDlaczego
pozycja na planszystructdwie liczby, niezmienna, (2,3) równa się (2,3)
kwota z walutąstructwartość, porównywana po zawartości
zakres datstructdwie daty, niezmienny
kolor RGBstructtrzy bajty, wartość
uczeńclassma tożsamość; dwóch uczniów o tym samym imieniu to nie ten sam uczeń
bilet, sprzęt, kontoclassma numer, historię i zmienny stan
plansza, lista wynikówclassduża i zmienna
cokolwiek z dziedziczeniemclassstruktura nie dziedziczy

Dlaczego biblioteka .NET wybrała tak, jak wybrała

Typ z bibliotekiCo to jestDlaczego tak
int, double, char, boolstructjedna mała, niezmienna wartość
decimalstruct16 bajtów — granica rozsądku, ale wartość
DateTime, TimeSpanstructniezmienne wartości; AddDays zwraca nową datę
stringclasszmienna długość, więc nie zmieści się w stałym rozmiarze
List<T>, Dictionaryclassrosną i zmieniają się w trakcie
tablicaclassdlatego przekazana do metody daje się zmienić (lekcja 23)
PRZYKŁAD

Plansza z pionkami

Plansza z pionkami: Pozycja jest niezmienną strukturą, Znacznik tą samą rzeczą zapisaną jako klasa — dla porównania — a Plansza jest klasą, bo jest duża i zmienna.

Program.cs — pełny plik
// Program.cs - pelny plik
Console.WriteLine("=== 1. przypisanie: struktura kontra klasa ===");
Pozycja p1 = new Pozycja(2, 3);
Pozycja p2 = p1;
p2 = p2.Przesun(1, 0);
Console.WriteLine($"struct:  p1 = {p1}, p2 = {p2}");

Znacznik z1 = new Znacznik(2, 3);
Znacznik z2 = z1;
z2.Rzad = 9;
Console.WriteLine($"class:   z1 = {z1}, z2 = {z2}");

Console.WriteLine();
Console.WriteLine("=== 2. przekazanie do metody ===");
Pozycja p3 = new Pozycja(5, 5);
SprobujZmienic(p3);
Console.WriteLine($"bez ref: {p3}");
ZmienNaPewno(ref p3);
Console.WriteLine($"z ref:   {p3}");

Console.WriteLine();
Console.WriteLine("=== 3. rownosc ===");
Pozycja a = new Pozycja(1, 1);
Pozycja b = new Pozycja(1, 1);
Console.WriteLine($"struct a.Equals(b) = {a.Equals(b)}");

Znacznik m = new Znacznik(1, 1);
Znacznik n = new Znacznik(1, 1);
Console.WriteLine($"class  m.Equals(n) = {m.Equals(n)}");
Console.WriteLine($"class  m == n      = {m == n}");
Console.WriteLine($"class  m == m      = {m == m}");

Console.WriteLine();
Console.WriteLine("=== 4. tablica kontra lista ===");
Pozycja[] tab = { new Pozycja(0, 0), new Pozycja(1, 1) };
tab[0] = tab[0].Przesun(3, 3);
Console.WriteLine($"tab[0] = {tab[0]}");

List<Pozycja> lista = new List<Pozycja> { new Pozycja(0, 0) };
Pozycja tmp = lista[0];
tmp = tmp.Przesun(7, 7);
lista[0] = tmp;
Console.WriteLine($"lista[0] = {lista[0]}");

Console.WriteLine();
Console.WriteLine("=== 5. plansza z pionkami ===");
Plansza plansza = new Plansza(4);
plansza.Ustaw(new Pozycja(0, 0), 'K');
plansza.Ustaw(new Pozycja(1, 2), 'p');
plansza.Ustaw(new Pozycja(3, 3), 'W');
plansza.Ustaw(new Pozycja(7, 7), 'X');
plansza.Rysuj();

Console.WriteLine($"na (1,2) stoi: {plansza.Co(new Pozycja(1, 2))}");
Console.WriteLine($"na (2,2) stoi: {plansza.Co(new Pozycja(2, 2))}");

Console.WriteLine();
Console.WriteLine("=== 6. ruch pionka ===");
Pozycja pionek = new Pozycja(1, 2);
Console.WriteLine($"start: {pionek}");

Pozycja[] ruchy = { new Pozycja(1, 0), new Pozycja(0, 1), new Pozycja(1, 1) };
foreach (Pozycja r in ruchy)
{
    Pozycja nowa = pionek.Przesun(r.Rzad, r.Kolumna);
    if (plansza.Poprawna(nowa))
    {
        pionek = nowa;
        Console.WriteLine($"ruch {r} -> {pionek}");
    }
    else
    {
        Console.WriteLine($"ruch {r} -> odrzucony, {nowa} poza plansza");
    }
}

static void SprobujZmienic(Pozycja p)
{
    p = p.Przesun(100, 100);
}

static void ZmienNaPewno(ref Pozycja p)
{
    p = p.Przesun(100, 100);
}


readonly struct Pozycja
{
    public int Rzad { get; }
    public int Kolumna { get; }

    public Pozycja(int rzad, int kolumna)
    {
        Rzad = rzad;
        Kolumna = kolumna;
    }

    public Pozycja Przesun(int oRzad, int oKolumne)
    {
        return new Pozycja(Rzad + oRzad, Kolumna + oKolumne);
    }

    public override string ToString()
    {
        return $"({Rzad},{Kolumna})";
    }
}

class Znacznik
{
    public int Rzad { get; set; }
    public int Kolumna { get; set; }

    public Znacznik(int rzad, int kolumna)
    {
        Rzad = rzad;
        Kolumna = kolumna;
    }

    public override string ToString()
    {
        return $"({Rzad},{Kolumna})";
    }
}

class Plansza
{
    private readonly char[,] pola;

    public int Rozmiar { get; }

    public Plansza(int rozmiar)
    {
        Rozmiar = rozmiar;
        pola = new char[rozmiar, rozmiar];

        for (int r = 0; r < rozmiar; r++)
        {
            for (int k = 0; k < rozmiar; k++)
            {
                pola[r, k] = '.';
            }
        }
    }

    public bool Poprawna(Pozycja p)
    {
        return p.Rzad >= 0 && p.Rzad < Rozmiar
            && p.Kolumna >= 0 && p.Kolumna < Rozmiar;
    }

    public void Ustaw(Pozycja p, char znak)
    {
        if (!Poprawna(p))
        {
            Console.WriteLine($"pominieto {p} - poza plansza");
            return;
        }
        pola[p.Rzad, p.Kolumna] = znak;
    }

    public char Co(Pozycja p)
    {
        if (!Poprawna(p))
        {
            return ' ';
        }
        return pola[p.Rzad, p.Kolumna];
    }

    public void Rysuj()
    {
        for (int r = 0; r < Rozmiar; r++)
        {
            for (int k = 0; k < Rozmiar; k++)
            {
                Console.Write(pola[r, k]);
                Console.Write(' ');
            }
            Console.WriteLine();
        }
    }
}
wynik w konsoli
=== 1. przypisanie: struktura kontra klasa ===
struct:  p1 = (2,3), p2 = (3,3)
class:   z1 = (9,3), z2 = (9,3)

=== 2. przekazanie do metody ===
bez ref: (5,5)
z ref:   (105,105)

=== 3. rownosc ===
struct a.Equals(b) = True
class  m.Equals(n) = False
class  m == n      = False
class  m == m      = True

=== 4. tablica kontra lista ===
tab[0] = (3,3)
lista[0] = (7,7)

=== 5. plansza z pionkami ===
pominieto (7,7) - poza plansza
K . . .
. . p .
. . . .
. . . W
na (1,2) stoi: p
na (2,2) stoi: .

=== 6. ruch pionka ===
start: (1,2)
ruch (1,0) -> (2,2)
ruch (0,1) -> (2,3)
ruch (1,1) -> (3,4) odrzucony, (3,4) poza plansza

Co dzieje się po kolei

  • Blok 1. Ten sam kod, dwa różne wyniki. p2 jest niezależną kopią, więc przesunięcie nie dotyka p1. z2 to drugie odwołanie do tego samego obiektu, więc zmiana widoczna jest z obu nazw.
  • Blok 2. SprobujZmienic dostaje kopię i zmienia kopię — po powrocie nic się nie stało. ZmienNaPewno dostaje zmienną przez ref, więc skutek zostaje.
  • Blok 3. Dwie różne pozycje o tych samych liczbach są równe, bo Equals struktury porównuje pola. Dwa różne znaczniki nie są równe, bo Equals klasy porównuje adresy. Zapisu a == b dla struktury nie ma w tym programie, bo nie skompilowałby się (CS0019).
  • Blok 4. Do tablicy i do listy zmiana wchodzi tak samo — przez przypisanie całego elementu. To jedyny sposób, który działa w obu przypadkach, więc warto się go trzymać zawsze.
  • Blok 5. Ustaw odrzuca pozycję (7,7), bo Poprawna sprawdza oba zakresy. Pionek p stoi w rzędzie 1, kolumnie 2 — porównaj z rysunkiem.
  • Blok 6. Ruch to Przesun, które zwraca nową pozycję. Program sprawdza ją przed zaakceptowaniem, więc trzeci ruch zostaje odrzucony i pionek zostaje na (2,3). Gdyby Przesun zmieniało pozycję w miejscu, sprawdzenie „czy wolno” byłoby już po fakcie.
  • readonly struct gwarantuje, że wszystkie te rozumowania są prawdziwe — nikt nie dopisze metody, która po cichu zmieni pozycję w środku.

Sprawdź cztery rzeczy

  1. Zmień readonly struct Pozycja na class Pozycja. Blok 1 zacznie wypisywać dwie takie same pozycje, a blok 3 zmieni True na False. Jedno słowo, dwie zmiany w wynikach.
  2. W bloku 6 zamień pionek = nowa; na samo pionek.Przesun(...). Pionek przestanie się ruszać, a kompilator wypisze ostrzeżenie o niewykorzystanym wyniku.
  3. Spróbuj dopisać w strukturze metodę public void PrzesunWMiejscu(int d) { Rzad += d; }. Dostaniesz błąd, bo struktura jest readonly — dokładnie o to nam chodziło.
  4. Dopisz Console.WriteLine(a == b); w bloku 3. Zobaczysz CS0019; potem zmień definicję na readonly record struct Pozycja(int Rzad, int Kolumna);, usuń własne ToString i Przesun i sprawdź, co jeszcze trzeba poprawić w programie.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementZnaczenieUwagi
typ wartościowyzmienna zawiera samą wartośćstruct, enum i wszystkie typy proste.
typ referencyjnyzmienna zawiera adres obiektuclass, tablica, string, object.
readonly structstruktura niezmiennaKompilator odrzuca każdą próbę zmiany pola.
readonly record structniezmienna struktura w jednym wierszuDostajesz konstruktor, ToString, == i with.
withkopia z jedną zmienioną wartościąTylko dla typów record.
ref przy parametrzeprzekazanie zmiennej, nie kopiiPoznane w lekcji 20; przy strukturach zmienia wynik.
defaultwartość ze wszystkimi polami zerowymiStruktura nie może być null.
Typ?struktura, która może nie mieć wartościZapis z lekcji 34.
Equals w strukturzeporównanie po polachDziała bez przesłaniania.
== w strukturzenie istnieje bez definicjiCS0019. Użyj Equals albo record struct.
pakowanie (boxing)kopia struktury na stercieZachodzi przy przypisaniu do object.
CS1612próba zmiany pola w wyniku metodyTypowe dla lista[0].Pole = x.
CS1604zmiana pola w readonly structZwróć nową wartość zamiast zmieniać istniejącą.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
struct ze zmiennymi polami publicznymiŹródło wszystkich pozostałych błędów w tej tabeli. Pisz readonly struct.
lista[0].MetodaZmieniajaca();Kompiluje się i nic nie robi — zmienia kopię. Najtrudniejszy do znalezienia błąd tej lekcji.
lista[0].Pole = 5;CS1612. Weź kopię, zmień ją, przypisz z powrotem cały element.
obiekt.WlasciwoscStruktury.Pole = 5;CS1612 z tego samego powodu — właściwość zwraca kopię.
p.Przesun(1, 0); bez przypisaniaWynik idzie do kosza. Struktury niezmienne zwracają nową wartość — zawsze ją przypisz.
Oczekiwanie, że metoda zmieni strukturęMetoda dostała kopię. Jeśli zmiana ma wyjść na zewnątrz, użyj ref albo zwróć nową wartość.
a == b dla własnej strukturyCS0019 — operator nie istnieje. Equals działa od razu.
Oczekiwanie, że Equals klasy porówna polaPorównuje adresy. Do porównania po zawartości trzeba record albo własnego Equals.
struct na coś dużegoKażde przypisanie i każde wywołanie metody kopiuje całość. Powyżej kilku pól wybierz class.
struct na rzecz z tożsamościąUczeń, konto i bilet mają numer i historię — dwie kopie o tych samych danych to nie ta sama rzecz.
struct z zamiarem dziedziczeniaCS0527. Struktura implementuje interfejsy, ale nie ma klasy bazowej.
struct ustawiany na nullCS0037. Potrzebny zapis Typ?.
ZADANIA

Zadania

ZAD 1Kopia kontra odwołanie★☆☆

Napisz strukturę Wymiar z publicznymi polami Szerokosc i Wysokosc (typ int) oraz przesłoniętym ToString(). W programie utwórz wymiar 3 na 4, przypisz go do drugiej zmiennej, zmień w drugiej zmiennej szerokość na 10 i wypisz oba. Potem zamień słowo struct na class, uruchom ponownie i zapisz w komentarzu oba wyniki oraz jednozdaniowe wyjaśnienie różnicy.

ZAD 2Metoda dostaje kopię★☆☆

Do zadania 1 (w wersji ze struct) dopisz dwie metody: static void Podwoj(Wymiar w) i static void PodwojNaPewno(ref Wymiar w), obie mnożące oba pola przez dwa. Wywołaj je po kolei na tej samej zmiennej i po każdym wywołaniu wypisz wymiar. Zapisz w komentarzu, dlaczego wyniki się różnią. Następnie sprawdź, co się dzieje przy tych samych dwóch metodach, gdy Wymiar jest klasą.

ZAD 3Pułapka listy★★☆

Do struktury Wymiar z zadania 1 dodaj metodę public void Zwieksz(int ile), która dodaje ile do obu pól. Utwórz tablicę Wymiar[] z dwoma elementami i listę List<Wymiar> z dwoma elementami. Na pierwszym elemencie tablicy i na pierwszym elemencie listy wywołaj Zwieksz(5), a potem wypisz oba. Zapisz w komentarzu, który wynik się zmienił i dlaczego. Potem spróbuj napisać lista[0].Szerokosc = 99; i zapisz numer błędu. Na koniec popraw program tak, żeby zmiana elementu listy naprawdę zadziałała, korzystając z zapisu „weź kopię, zmień, wstaw z powrotem”.

ZAD 4Struktura niezmienna★★☆

Przepisz Wymiar jako readonly struct: właściwości tylko do odczytu, konstruktor, wyliczana właściwość Pole, metoda Wymiar Powieksz(int ile) zwracająca nowy wymiar oraz przesłonięty ToString(). Sprawdź, że stare wywołanie lista[0].Zwieksz(5) nie da się już napisać. Utwórz listę pięciu wymiarów, a następnie jedną pętlą for powiększ każdy element o 2, przypisując wynik z powrotem do listy. Wypisz listę przed i po. Na koniec spróbuj dopisać metodę zmieniającą pole w miejscu i zapisz numer błędu.

ZAD 5Równość i record struct★★☆

Utwórz dwie niezależne zmienne typu Wymiar (wersja readonly struct z zadania 4) o tych samych liczbach. Wypisz wynik Equals, a potem spróbuj napisać w1 == w2 i zapisz numer błędu w komentarzu. Następnie napisz drugą wersję typu jako readonly record struct WymiarR(int Szerokosc, int Wysokosc); i dla niej wypisz: sam obiekt, ==, Equals oraz kopię utworzoną zapisem with ze zmienioną szerokością. W komentarzu wypisz trzy rzeczy, które record struct dopisał za Ciebie. Na koniec utwórz klasę WymiarK z tymi samymi polami, porównaj dwa jej obiekty operatorami Equals i == i wyjaśnij, dlaczego wyniki są inne niż dla struktury.

ZAD 6Gra na planszy — pełny program★★★

Napisz grę „zbieranie monet” na planszy 6 na 6. Zadeklaruj readonly struct Pozycja z właściwościami Rzad i Kolumna, konstruktorem, metodą Pozycja Przesun(int oRzad, int oKolumne) i przesłoniętym ToString() w formacie (r,k). Zadeklaruj enum Kierunek z pozycjami Gora, Dol, Lewo, Prawo oraz metodę static Pozycja Krok(Pozycja p, Kierunek k) zwracającą nową pozycję (użyj switch z gałęzią _). Napisz klasę Plansza z prywatną tablicą dwuwymiarową char, metodami bool Poprawna(Pozycja), void Ustaw(Pozycja, char), char Co(Pozycja) i void Rysuj(). Napisz klasę Gracz z właściwościami Pozycja (typu Pozycja, z private set), Monety i Ruchy oraz metodą bool Idz(Kierunek k, Plansza p), która: wyznacza nową pozycję, odrzuca ruch poza planszę, a przy udanym ruchu zwiększa licznik ruchów, podnosi monetę, jeśli na polu stała, i przesuwa gracza. W programie rozstaw pięć monet w ustalonych miejscach, postaw gracza w lewym górnym rogu, a potem wczytuj w pętli litery w, s, a, d (koniec na q), po każdym ruchu rysując planszę oraz wypisując pozycję gracza — podaj do WriteLine samą pozycję, żeby zadziałało jej ToString() — a obok liczbę monet i ruchów. Po zebraniu wszystkich monet zakończ grę komunikatem z liczbą ruchów. Na końcu pliku, w komentarzu, odpowiedz na trzy pytania: dlaczego Pozycja jest strukturą, a Gracz klasą; co by się zepsuło, gdyby Pozycja miała publiczne zmienne pola i metodę zmieniającą je w miejscu; oraz dlaczego metoda Idz może sprawdzić poprawność ruchu przed przesunięciem gracza.

PODSUMOWANIE

Co trzeba zapamiętać

  • Zmienna typu wartościowego zawiera samą wartość; zmienna typu referencyjnego zawiera adres obiektu.
  • Przypisanie struktury kopiuje zawartość — dwie zmienne są od tej chwili niezależne.
  • Struktura jest kopiowana także przy przekazaniu do metody, przy zwracaniu i przy odczycie właściwości.
  • Element tablicy daje dostęp bezpośredni, element List<T> — kopię. To najczęstsze źródło pomyłek.
  • Metoda zmieniająca pole, wywołana na lista[0], zmienia kopię i nie zgłasza żadnego błędu.
  • Zmianę w tablicy i w liście wprowadzaj zawsze przez przypisanie całego elementu — ten sposób działa w obu.
  • Struktury pisz jako niezmienne: readonly struct plus metody zwracające nową wartość.
  • readonly record struct daje konstruktor, ToString(), == i zapis with w jednym wierszu.
  • Equals struktury porównuje pola, Equals klasy — adresy. Operator == dla własnej struktury nie istnieje bez definicji.
  • struct wybieraj tylko dla małej, niezmiennej wartości porównywanej po zawartości. W razie wątpliwości class.
  • Struktura nie dziedziczy, nie bywa null i nie ma składowych virtual — to nie jest „lżejsza klasa”.

Dokumentacja: Microsoft Learn — temat tej lekcji.

Postęp zapisuje się w tej przeglądarce.