Class, struct czy record w C#? Kiedy używać którego typu
“Jaka jest różnica między class a struct?” to jedno z najczęstszych pytań rekrutacyjnych na Junior .NET Developera — i jedno z tych, na które większość kandydatów odpowiada teoretycznie (“struct to typ wartościowy”), nie potrafiąc pokazać, co to realnie oznacza w działającym kodzie. Dołóż do tego record (C# 9+) i record struct (C# 10+) i robi się cztery narzędzia do wyboru zamiast dwóch.
Class — referencja, tożsamość, mutowalność
public class Zamowienie
{
public int Id { get; set; }
public decimal Kwota { get; set; }
}
var a = new Zamowienie { Id = 1, Kwota = 100 };
var b = new Zamowienie { Id = 1, Kwota = 100 };
Console.WriteLine(a == b); // False — to dwa różne obiekty w pamięci
class to typ referencyjny — zmienna przechowuje adres obiektu na stercie, nie same dane. Domyślne porównanie == sprawdza, czy to ten sam obiekt w pamięci, nie czy mają te same wartości. To sensowne dla bytów z tożsamością (np. Zamowienie o konkretnym Id, które zmienia stan w czasie) — bez sensu dla prostych danych, gdzie liczy się wartość, nie tożsamość.
Struct — wartość, kopiowanie, brak alokacji na stercie
public struct Punkt
{
public int X, Y;
}
var p1 = new Punkt { X = 1, Y = 2 };
var p2 = p1; // KOPIA całych danych, nie referencja
p2.X = 99;
Console.WriteLine(p1.X); // 1 — zmiana w p2 nie dotyka p1
struct to typ wartościowy — przypisanie kopiuje całą zawartość. To samo dzieje się przy przekazaniu do metody:
void ZmienPunkt(Punkt p) => p.X = 999; // zmienia tylko lokalną kopię
var punkt = new Punkt { X = 1, Y = 2 };
ZmienPunkt(punkt);
Console.WriteLine(punkt.X); // wciąż 1 — metoda dostała kopię
Zaleta: małe struktury nie trafiają na stertę zarządzaną przez GC (żyją na stosie), więc są tanie w tworzeniu i niszczeniu. Pułapka: duże struktury są drogie w kopiowaniu — każde przypisanie i przekazanie do metody kopiuje wszystkie pola. Microsoft rekomenduje trzymać się rozmiaru do ok. 16 bajtów.
Record — równość wartościowa “za darmo”
public record Pieniadze(decimal Kwota, string Waluta);
var x = new Pieniadze(100, "PLN");
var y = new Pieniadze(100, "PLN");
Console.WriteLine(x == y); // True — te same wartości = równe obiekty
Console.WriteLine(x); // Pieniadze { Kwota = 100, Waluta = PLN } — ToString gotowy od ręki
var z = x with { Kwota = 150 }; // nowa, niezależna kopia z jedną zmienioną wartością
Console.WriteLine(x.Kwota); // 100 — oryginał bez zmian
record (domyślnie record class) jest wciąż typem referencyjnym — dane trafiają na stertę tak jak w zwykłej klasie. Różnica: kompilator generuje za ciebie porównanie wartościowe (Equals/GetHashCode po polach), czytelny ToString() oraz operator with do tworzenia zmodyfikowanej kopii bez ręcznego przepisywania wszystkich pól. To dokładnie to, co ręcznie dopisuje się do klas reprezentujących dane (DTO, value objects) — tu masz to bez linijki dodatkowego kodu.
Record struct — połączenie obu światów
public readonly record struct Wspolrzedne(double Szerokosc, double Dlugosc);
var w1 = new Wspolrzedne(52.23, 21.01);
var w2 = new Wspolrzedne(52.23, 21.01);
Console.WriteLine(w1 == w2); // True — równość wartościowa, i bez alokacji na stercie
record struct (C# 10+) to struct z dodatkami record: kopiowanie przez wartość i brak alokacji na stercie jak w zwykłym struct, plus darmowa równość wartościowa i ToString() jak w record. readonly na początku wymusza niemutowalność — połączenie, które ma sens dla małych, niezmiennych danych, gdzie zależy ci na wydajności (np. współrzędne, kolor RGB, para wartości zwracana z metody).
Tabela decyzyjna
| Typ | Pamięć | Kopiowanie | Równość domyślna | Kiedy użyć |
|---|---|---|---|---|
class | sterta | przez referencję | tożsamość (referencja) | byty z tożsamością i zmiennym stanem (encje domenowe) |
struct | stos (zwykle) | przez wartość | pola, ale bez ładnego ToString | małe, wydajnościowo krytyczne wartości |
record | sterta | przez referencję | wartość (pola) | niemutowalne DTO, value objects, dane API |
record struct | stos (zwykle) | przez wartość | wartość (pola) | małe, niemutowalne wartości + potrzebujesz równości/ToString |
Najczęstsza pułapka: mutowalny struct
public struct LicznikZly
{
public int Wartosc;
public void Zwieksz() => Wartosc++;
}
Struktury, które można modyfikować po utworzeniu, to źródło trudnych do znalezienia bugów — bo w zależności od tego, czy operujesz na oryginale czy na jednej z wielu niewidocznych kopii (np. z foreach, z właściwości tylko-do-odczytu), zmiana albo zadziała, albo zniknie bez śladu. Zasada: struktury powinny być niemutowalne (readonly struct), a jeśli potrzebujesz “zmienić” wartość — zwróć nową, zamiast modyfikować istniejącą.
Podsumowanie
class— gdy liczy się tożsamość obiektu i jego stan zmienia się w czasie.record— gdy modelujesz dane, dla których “te same wartości = ten sam obiekt logicznie” (DTO, value objects), i chcesz darmową równość,ToString()iwith.struct— gdy potrzebujesz małej, wydajnej wartości i chcesz uniknąć alokacji na stercie.record struct— gdy chcesz obu tych rzeczy naraz: wydajnościstructi wygodyrecord.
Domyślny wybór dla nowych typów w nowoczesnym C#: jeśli dane mają być niemutowalne, sięgnij po record (albo record struct, jeśli są małe i wydajność ma znaczenie). Po zwykłą class sięgaj świadomie — gdy naprawdę potrzebujesz tożsamości i mutowalnego stanu.
🚀 Co dalej?
Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.
- 🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 kroków od podstaw C# do pierwszej pracy: dev-hobby.pl
- 🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
Zamień wiedzę w umiejętności
Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.
Pobieram roadmapę →
1 comment