Lekcja 21. Struktury (struct) w C#
ŚredniPo co się tego uczymy?
Podstawa programowa (INF.04.3.2) wymaga umiejętności posługiwania się "rekordami" — czyli złożonymi typami danych grupującymi kilka wartości w jedną paczkę. W C# taką rolę pełni struct. To lekki sposób na opisanie "punktu na mapie", "koloru RGB" czy "daty" bez pisania pełnej klasy — a jednocześnie świetna okazja, żeby zrozumieć różnicę między typami wartościowymi a referencyjnymi, która w C# jest fundamentalna.
Teoria
struct wygląda i zachowuje się bardzo podobnie do klasy — może mieć pola, konstruktory, metody, właściwości. Kluczowa różnica leży głębiej, w pamięci:
- klasa to typ referencyjny — zmienna przechowuje adres obiektu na stercie (heap). Kopiując zmienną, kopiujesz tylko adres — obie zmienne wskazują na ten sam obiekt;
- struct to typ wartościowy — zmienna przechowuje same dane bezpośrednio, zwykle na stosie (stack). Kopiując zmienną struktury, dostajesz niezależną kopię całych danych.
To ma realne konsekwencje: jeśli przekażesz obiekt klasy do metody i metoda zmieni jego pole, zmiana będzie widoczna także poza metodą (bo to wciąż ten sam obiekt). Jeśli przekażesz strukturę, metoda dostaje kopię — zmiany w niej nie wpłyną na oryginał.
Kiedy używać struct, a kiedy class? Struktury sprawdzają się dla małych, niezmiennych (albo rzadko zmienianych) paczek danych bez własnej "tożsamości" — punkt (x, y), kolor (r, g, b), zakres dat. Klasy sprawdzają się tam, gdzie obiekt ma zachowanie, cykl życia, dziedziczenie albo powinien być współdzielony między różnymi częściami programu. W praktyce na start i na egzaminie wystarczy zapamiętać: struct = mała paczka danych kopiowana przez wartość, class = pełnoprawny obiekt kopiowany przez referencję.
Schemat
KLASA (typ referencyjny) STRUCT (typ wartościowy)
zmienna a ──► [obiekt na stercie] zmienna p1: (X=10, Y=20) <- dane wprost
zmienna b ──► ^ ten sam zmienna p2 = p1;
(a i b wskazują na p2: (X=10, Y=20) <- NIEZALEŻNA KOPIA
ten sam obiekt) zmiana p2.X = 99 NIE zmienia p1!
Przykład z życia
Typ Point (punkt x, y) w bibliotekach graficznych .NET jest strukturą — tworzysz ich tysiące podczas rysowania (każdy piksel, każdy wierzchołek), więc lekkość struktury i brak potrzeby "odśmiecania" pamięci przez garbage collector realnie przyspiesza program. Podobnie DateTime (data i godzina) oraz decimal (dokładne liczby, np. do kwot pieniężnych) w C# to struktury.
Kod (C#)
using System;
// Struktura opisująca punkt na płaszczyźnie.
struct Punkt
{
public int X;
public int Y;
public Punkt(int x, int y)
{
X = x;
Y = y;
}
public double OdlegloscOdZera()
{
return Math.Sqrt(X * X + Y * Y);
}
public override string ToString()
{
return $"({X}, {Y})";
}
}
class Program
{
// Metoda dostaje KOPIĘ struktury — zmiany wewnątrz nie wyjdą na zewnątrz.
static void SprobujPrzesunac(Punkt p)
{
p.X += 100;
p.Y += 100;
Console.WriteLine($"Wewnątrz metody: {p}");
}
static void Main()
{
Punkt p1 = new Punkt(3, 4);
Punkt p2 = p1; // kopiowana jest cała struktura, nie referencja
p2.X = 99;
Console.WriteLine($"p1 = {p1}"); // (3, 4) - bez zmian!
Console.WriteLine($"p2 = {p2}"); // (99, 4)
Console.WriteLine($"Odległość p1 od zera: {p1.OdlegloscOdZera():F2}");
SprobujPrzesunac(p1);
Console.WriteLine($"p1 po SprobujPrzesunac: {p1}"); // wciąż (3, 4)!
}
}
Komentarz i wyjaśnienie kodu
Struktura Punkt ma dwa pola (X, Y), konstruktor ustawiający je od razu przy tworzeniu oraz dwie metody. Kluczowy moment jest w Main: Punkt p2 = p1; tworzy pełną, niezależną kopię — zmiana p2.X nie rusza p1.X. Gdyby Punkt był klasą, ta sama linijka sprawiłaby, że p1 i p2 wskazywałyby na ten sam obiekt, a zmiana jednej zmieniałaby też drugą. Ta sama zasada widać w SprobujPrzesunac — metoda dostaje kopię, więc zmiany w niej "znikają" po jej zakończeniu.
Ćwiczenie samodzielne
Zmień Punkt na klasę (usuń słowo struct, dodaj class) i uruchom ten sam kod z lekcji jeszcze raz. Zaobserwuj, jak zmienia się wynik — p1 teraz też się zmieni po modyfikacji p2. To najlepszy sposób, żeby poczuć różnicę na własne oczy.
Plac zabaw — wypróbuj online
Poniższy kod wykonuje się od razu w Twojej przeglądarce — nic nie trzeba instalować. Zmień kod i kliknij „Uruchom”.
Zadania do pracy własnej
Utwórz strukturę
Kolorz polamiR,G,B(typubyte) oraz metodąToString()zwracającą tekst w formacie "#RRGGBB" (w systemie szesnastkowym — użyjR.ToString("X2")).Utwórz strukturę
ZakresDatz polamiOdiDo(typuDateTime) oraz metodąZawiera(DateTime data)zwracającątrue, jeśli podana data mieści się w zakresie.Utwórz strukturę
Ulamekreprezentującą ułamek zwykły (licznik/mianownik) z metodamiDodaj,PomnoziSkroc(skracającą ułamek przez największy wspólny dzielnik). Zwróć uwagę, żeby metody zwracały NOWYUlamek, a nie modyfikowały istniejący — to zgodne z duchem typów wartościowych.
Typowe błędy
Oczekiwanie, że zmiana pola struktury przekazanej do metody "wyjdzie na zewnątrz" — bez słowa kluczowego ref metoda zawsze dostaje kopię struktury. To zaskakuje osoby przyzwyczajone do klas.
Tworzenie dużych, "ciężkich" struktur — jeśli struktura ma wiele pól albo duże dane, każde kopiowanie (a dzieje się to często i niejawnie) kosztuje pamięć i czas. Zasada praktyczna: struct dla małych paczek danych (kilka prostych pól), class dla wszystkiego większego.
Mylenie struct z klasą przy dziedziczeniu — struktury w C# NIE mogą dziedziczyć po innej strukturze ani klasie (mogą tylko implementować interfejsy). Jeśli potrzebujesz dziedziczenia, potrzebujesz klasy.
Nawiązanie do egzaminu zawodowego
Struktury realizują efekt INF.04.3.2 — "posługuje się złożonymi typami danych", konkretnie rekordami grupującymi kilka wartości. Zrozumienie różnicy między typem wartościowym a referencyjnym jest też fundamentem do świadomego programowania obiektowego z lekcji 10-16 tego działu.