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.
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 structze zmianą przez zwrócenie nowej wartości. - Użyjesz zapisu
readonly record structi wyjaśnisz, co kompilator dopisał za Ciebie. - Porównasz zachowanie
Equalsi==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.
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:
struct Licznik
{
public int Wartosc;
public void Zwieksz()
{
Wartosc++;
}
}
Na zwykłej zmiennej działa dokładnie tak, jak się spodziewasz:
Licznik a = new Licznik();
a.Zwieksz();
a.Zwieksz();
Console.WriteLine(a.Wartosc); // 2
W tablicy — też:
Licznik[] tab = new Licznik[1];
tab[0].Zwieksz();
tab[0].Zwieksz();
Console.WriteLine(tab[0].Wartosc); // 2
A teraz to samo w List<T>:
List<Licznik> lista = new List<Licznik> { new Licznik() };
lista[0].Zwieksz();
lista[0].Zwieksz();
Console.WriteLine(lista[0].Wartosc); // 0 ???
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ł:
// 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ć.
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ściowy | Typ referencyjny | |
|---|---|---|
| Co zawiera zmienna | samą wartość | adres obiektu |
| Słowo kluczowe | struct, enum | class, interface, tablica |
| Przykłady wbudowane | int, double, bool, char, decimal, DateTime | string, List<T>, object, każda Twoja klasa |
| Przypisanie | kopiuje zawartość | kopiuje adres — dwa odwołania do jednego obiektu |
| Wartość początkowa pola | wszystkie pola zerowe | null |
Może być null | nie (chyba że Typ?) | tak |
Equals domyślnie | porównuje pola | porównuje adresy |
| Dziedziczenie | nie | tak |
Dwa programy różniące się jednym słowem dają różne wyniki — to najkrótszy możliwy dowód:
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})"; }
}
a = (2,3), b = (9,3)
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})"; }
}
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.
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
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:
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
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
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
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)
| Zapis | Tablica Pozycja[] | Lista List<Pozycja> |
|---|---|---|
x[0].Pole = 5; | działa | CS1612 |
x[0].MetodaZmieniajaca(); | działa | kompiluje się i nic nie robi |
x[0] = nowaWartosc; | działa | działa |
odczyt x[0].Pole | działa | działa |
6. Wrzucenie do object
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.
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
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:
public Pozycja Przesun(int oRzad, int oKolumne)
{
return new Pozycja(Rzad + oRzad, Kolumna + oKolumne);
}
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”:
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 ==:
readonly record struct Pozycja(int Rzad, int Kolumna);
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.
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.
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
struct | class | |
|---|---|---|
Equals bez przesłonięcia | porównuje pola → True | porównuje adresy → False |
== bez definicji | nie 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ęć:
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.
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:
- typ opisuje jedną wartość, nie rzecz z tożsamością i historią;
- jest mały — orientacyjnie do 16 bajtów, czyli około czterech liczb
int; - jest niezmienny — po utworzeniu nic się w nim nie zmienia;
- 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.
| Typ | Wybór | Dlaczego |
|---|---|---|
| pozycja na planszy | struct | dwie liczby, niezmienna, (2,3) równa się (2,3) |
| kwota z walutą | struct | wartość, porównywana po zawartości |
| zakres dat | struct | dwie daty, niezmienny |
| kolor RGB | struct | trzy bajty, wartość |
| uczeń | class | ma tożsamość; dwóch uczniów o tym samym imieniu to nie ten sam uczeń |
| bilet, sprzęt, konto | class | ma numer, historię i zmienny stan |
| plansza, lista wyników | class | duża i zmienna |
| cokolwiek z dziedziczeniem | class | struktura nie dziedziczy |
Dlaczego biblioteka .NET wybrała tak, jak wybrała
| Typ z biblioteki | Co to jest | Dlaczego tak |
|---|---|---|
int, double, char, bool | struct | jedna mała, niezmienna wartość |
decimal | struct | 16 bajtów — granica rozsądku, ale wartość |
DateTime, TimeSpan | struct | niezmienne wartości; AddDays zwraca nową datę |
string | class | zmienna długość, więc nie zmieści się w stałym rozmiarze |
List<T>, Dictionary | class | rosną i zmieniają się w trakcie |
| tablica | class | dlatego przekazana do metody daje się zmienić (lekcja 23) |
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 - 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();
}
}
}
=== 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.
p2jest niezależną kopią, więc przesunięcie nie dotykap1.z2to drugie odwołanie do tego samego obiektu, więc zmiana widoczna jest z obu nazw. - Blok 2.
SprobujZmienicdostaje kopię i zmienia kopię — po powrocie nic się nie stało.ZmienNaPewnodostaje zmienną przezref, więc skutek zostaje. - Blok 3. Dwie różne pozycje o tych samych liczbach są równe, bo
Equalsstruktury porównuje pola. Dwa różne znaczniki nie są równe, boEqualsklasy porównuje adresy. Zapisua == bdla 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.
Ustawodrzuca pozycję (7,7), boPoprawnasprawdza oba zakresy. Pionekpstoi 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). GdybyPrzesunzmieniało pozycję w miejscu, sprawdzenie „czy wolno” byłoby już po fakcie. readonly structgwarantuje, że wszystkie te rozumowania są prawdziwe — nikt nie dopisze metody, która po cichu zmieni pozycję w środku.
Sprawdź cztery rzeczy
- Zmień
readonly struct Pozycjanaclass Pozycja. Blok 1 zacznie wypisywać dwie takie same pozycje, a blok 3 zmieniTruenaFalse. Jedno słowo, dwie zmiany w wynikach. - W bloku 6 zamień
pionek = nowa;na samopionek.Przesun(...). Pionek przestanie się ruszać, a kompilator wypisze ostrzeżenie o niewykorzystanym wyniku. - Spróbuj dopisać w strukturze metodę
public void PrzesunWMiejscu(int d) { Rzad += d; }. Dostaniesz błąd, bo struktura jestreadonly— dokładnie o to nam chodziło. - Dopisz
Console.WriteLine(a == b);w bloku 3. Zobaczysz CS0019; potem zmień definicję nareadonly record struct Pozycja(int Rzad, int Kolumna);, usuń własneToStringiPrzesuni sprawdź, co jeszcze trzeba poprawić w programie.
Zestawienie elementów
| Element | Znaczenie | Uwagi |
|---|---|---|
| typ wartościowy | zmienna zawiera samą wartość | struct, enum i wszystkie typy proste. |
| typ referencyjny | zmienna zawiera adres obiektu | class, tablica, string, object. |
readonly struct | struktura niezmienna | Kompilator odrzuca każdą próbę zmiany pola. |
readonly record struct | niezmienna struktura w jednym wierszu | Dostajesz konstruktor, ToString, == i with. |
with | kopia z jedną zmienioną wartością | Tylko dla typów record. |
ref przy parametrze | przekazanie zmiennej, nie kopii | Poznane w lekcji 20; przy strukturach zmienia wynik. |
default | wartość ze wszystkimi polami zerowymi | Struktura nie może być null. |
Typ? | struktura, która może nie mieć wartości | Zapis z lekcji 34. |
Equals w strukturze | porównanie po polach | Działa bez przesłaniania. |
== w strukturze | nie istnieje bez definicji | CS0019. Użyj Equals albo record struct. |
| pakowanie (boxing) | kopia struktury na stercie | Zachodzi przy przypisaniu do object. |
| CS1612 | próba zmiany pola w wyniku metody | Typowe dla lista[0].Pole = x. |
| CS1604 | zmiana pola w readonly struct | Zwróć nową wartość zamiast zmieniać istniejącą. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
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 przypisania | Wynik 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 struktury | CS0019 — operator nie istnieje. Equals działa od razu. |
Oczekiwanie, że Equals klasy porówna pola | Porównuje adresy. Do porównania po zawartości trzeba record albo własnego Equals. |
struct na coś dużego | Każ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 dziedziczenia | CS0527. Struktura implementuje interfejsy, ale nie ma klasy bazowej. |
struct ustawiany na null | CS0037. Potrzebny zapis Typ?. |
Zadania
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.
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ą.
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”.
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.
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.
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.
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 structplus metody zwracające nową wartość. readonly record structdaje konstruktor,ToString(),==i zapiswithw jednym wierszu.Equalsstruktury porównuje pola,Equalsklasy — adresy. Operator==dla własnej struktury nie istnieje bez definicji.structwybieraj tylko dla małej, niezmiennej wartości porównywanej po zawartości. W razie wątpliwościclass.- Struktura nie dziedziczy, nie bywa
nulli nie ma składowychvirtual— to nie jest „lżejsza klasa”.
Dokumentacja: Microsoft Learn — temat tej lekcji.