• C# i .NET
  • C# record bez tajemnic - record class czy record struct?

C# record bez tajemnic - record class czy record struct?

Radosław Krajewski 23 lipca 2026
Porównanie c# record, class i struct. Kluczowe różnice.

Spis treści

Gdy model danych ma głównie opisywać stan, a nie zmieniać go krok po kroku, zwykła klasa często wymaga zbyt dużej ilości kodu. W C# typ record upraszcza taki scenariusz, oferując niemutowalne właściwości, porównywanie po wartościach i wygodne tworzenie kopii. Pokażę, jak definiować rekordy, czym różnią się od klas i struktur oraz gdzie ich użycie naprawdę ma sens.

Rekordy upraszczają pracę z niezmiennymi modelami danych

  • record domyślnie oznacza referencyjny record class.
  • Porównanie wartości sprawia, że dwa rekordy z takimi samymi danymi są równe.
  • Wyrażenie with tworzy zmodyfikowaną kopię bez zmiany oryginału.
  • record struct jest typem wartościowym, a readonly record struct lepiej wspiera niemutowalność.
  • Niemutowalność jest płytka, więc obiekt wskazywany przez właściwość nadal może być zmieniany.

Co daje record w C# i kiedy ma sens

Rekord to specjalny wariant klasy albo struktury przeznaczony przede wszystkim do przechowywania danych. Kompilator generuje za nas między innymi obsługę porównywania, GetHashCode(), ToString() oraz wygodny konstruktor. Dzięki temu możemy skupić się na modelu, a nie na kodzie technicznym.

Najważniejsza różnica względem zwykłej klasy dotyczy równości. Dwie instancje klasy są domyślnie różne, nawet jeśli przechowują identyczne wartości. Dwa rekordy tego samego typu porównują swoje dane, więc poniższy warunek zwróci true.

var first = new CustomerId("PL-100");
var second = new CustomerId("PL-100");

Console.WriteLine(first == second); // True

public record CustomerId(string Value);

To dobrze pasuje do obiektów takich jak identyfikatory, adresy, wartości pieniężne, komendy, zdarzenia domenowe i modele odpowiedzi API. Nie oznacza jednak, że każdy obiekt w aplikacji powinien być rekordem. Encja mająca własny cykl życia, tożsamość i wiele operacji biznesowych nadal częściej będzie czytelniejsza jako klasa.

Jak zdefiniować rekord bez zbędnego kodu

Najkrótsza składnia wykorzystuje parametry pozycyjne. Każdy parametr staje się publiczną właściwością, a konstruktor jest generowany automatycznie.

public record Product(string Name, decimal Price);

var product = new Product("Klawiatura", 299.99m);

Console.WriteLine(product.Name);
Console.WriteLine(product);

W przypadku record class właściwości utworzone z parametrów mają setter init. Można je ustawić podczas tworzenia obiektu, ale nie można przypisać nowej wartości po zakończeniu inicjalizacji.

var product = new Product("Klawiatura", 299.99m);

// product.Price = 249.99m; // błąd kompilacji

public record Product(string Name, decimal Price);

Gdy model wymaga dodatkowej logiki albo bardziej opisowej deklaracji, używam składni blokowej. Pozwala ona dodać walidację, właściwości obliczane i metody.

public record Order
{
    public required int Id { get; init; }
    public required decimal Total { get; init; }

    public bool IsLarge => Total >= 1000m;
}

var order = new Order
{
    Id = 42,
    Total = 1250m
};

W praktyce nie przywiązuję się do jednej formy. Składnia pozycyjna świetnie działa dla małych, oczywistych modeli. W przypadku większego kontraktu blokowa deklaracja zwykle lepiej pokazuje, które pola są wymagane, a które opcjonalne.

W przykładach często stosuję także var, gdy typ wynika bezpośrednio z konstruktora. Jeśli chcesz uporządkować zasady takiego zapisu, pomocny będzie artykuł o użyciu var w C#.

Record class, record struct i readonly record struct

Słowo record bez dodatkowego modyfikatora jest skrótem dla record class. Taki typ jest referencyjny, podobnie jak zwykła klasa, ale korzysta z semantyki wartościowej i wygenerowanych mechanizmów rekordów.

record struct jest typem wartościowym. Zwykle stosuję go dla małych danych, które mają być lekkie i nie wymagają tożsamości obiektu na stercie. Trzeba jednak uważać, ponieważ pozycyjne właściwości zwykłego record struct są domyślnie zapisywalne.

public record struct Point(int X, int Y);

var point = new Point(10, 20);
point.X = 15; // dozwolone

Jeśli zależy mi na niemutowalnym typie wartościowym, wybieram readonly record struct.

public readonly record struct Temperature(double Celsius);

var temperature = new Temperature(21.5);

// temperature.Celsius = 22.0; // błąd kompilacji
Typ Kategoria Domyślna mutowalność właściwości pozycyjnych Typowe zastosowanie
record class Referencyjny init DTO, komendy, zdarzenia, modele domenowe oparte na danych
record struct Wartościowy set Małe wartości, gdy mutowalność jest akceptowalna
readonly record struct Wartościowy init Niewielkie, niemutowalne wartości, na przykład współrzędne

Moja praktyczna zasada jest prosta. Zaczynam od record class, a po record struct sięgam dopiero wtedy, gdy rozmiar, sposób przekazywania i semantyka typu wartościowego są naprawdę uzasadnione. Sam fakt, że struktura może być szybsza, nie wystarcza do podjęcia tej decyzji.

Równość, with i granice niemutowalności

Rekordy porównują wartości swoich właściwości, a nie adresy obiektów. Kompilator generuje odpowiednie metody oraz operatory == i !=. Dzięki temu model danych zachowuje się tak, jak większość osób intuicyjnie oczekuje.

public record User(string Login, string Role);

var admin = new User("anna", "admin");
var copy = new User("anna", "admin");

Console.WriteLine(admin == copy); // True
Console.WriteLine(ReferenceEquals(admin, copy)); // False

Do zmiany pojedynczej wartości służy wyrażenie with. Tworzy ono nową instancję na podstawie istniejącej, pozostawiając oryginał bez zmian.

var regularUser = new User("anna", "user");
var promotedUser = regularUser with { Role = "admin" };

Console.WriteLine(regularUser.Role); // user
Console.WriteLine(promotedUser.Role); // admin

To bardzo wygodny model pracy w aplikacjach webowych i systemach opartych na zdarzeniach. Mogę przekazać dane przez kilka warstw bez obawy, że jedna z nich przypadkiem zmieni stan współdzielonego obiektu. Nie jest to jednak głęboka niemutowalność.

public record Report(string Title, List Tags);

var first = new Report("Raport", new List { "C#" });
var second = first with { Title = "Nowy raport" };

second.Tags.Add(".NET");

Console.WriteLine(first.Tags.Count); // 2

Lista nadal jest tym samym obiektem, więc zmiana wykonana przez second jest widoczna także przez first. Przy danych zagnieżdżonych używam niemutowalnych kolekcji albo tworzę kopie kolekcji podczas budowania rekordu. Sam zapis get i init nie rozwiązuje tego problemu.

Gdzie rekord sprawdza się najlepiej

Najbardziej naturalnym miejscem dla rekordów są kontrakty przesyłane między warstwami. Przykładowy model żądania nie potrzebuje własnej tożsamości ani metod zmieniających stan, dlatego krótka deklaracja jest tu czytelniejsza niż rozbudowana klasa.

public record CreateInvoiceCommand(
    int CustomerId,
    DateOnly IssueDate,
    decimal Amount);

Podobnie wyglądają zdarzenia domenowe i odpowiedzi z API. Ich sens polega na tym, jakie dane opisują, a nie na tym, że są konkretnym obiektem przechowywanym w pamięci.

public record InvoiceCreated(
    Guid InvoiceId,
    DateTimeOffset CreatedAt,
    decimal Amount);

Ostrożniej podchodzę do rekordów jako encji mapowanych przez ORM. Encja w bazie często ma tożsamość, śledzenie zmian i operacje aktualizujące stan. W takim przypadku rekord może utrudnić konfigurację albo zasugerować niemutowalność, której model persystencji wcale nie potrzebuje.

Rekordy dobrze pasują także do wartościowych elementów większych modeli, na przykład adresu, przedziału dat czy parametrów paginacji. Sam wybór typu nie zastępuje jednak dobrego projektu kolekcji. Przy takich decyzjach przydaje się świadomy dobór kolekcji C#, szczególnie gdy rekord zawiera listy, słowniki albo zbiory.

Najczęstsze błędy przy wyborze rekordów

Traktowanie rekordu jak gwarancji pełnej niezmienności

Właściwość może być niezmienna, ale wskazywany przez nią obiekt już nie. Dotyczy to list, tablic, słowników i własnych klas. Przy modelach współdzielonych między wątkami trzeba zadbać również o niemutowalność ich zawartości.

Używanie record struct bez sprawdzenia kosztu kopiowania

Struktura jest kopiowana przy przypisaniu i przekazywaniu według wartości. Dla małego punktu to świetny wybór, ale dla dużego modelu może oznaczać niepotrzebne kopiowanie danych. Rozmiar typu i częstotliwość przekazywania mają tu większe znaczenie niż sama moda na typy wartościowe.

Wkładanie do rekordu logiki zarządzającej cyklem życia

Rekord może mieć metody i dziedziczenie, ale jego mocną stroną pozostaje opis danych. Jeżeli obiekt posiada wiele reguł, zmienia stan etapami i pilnuje własnej spójności, zwykła klasa często komunikuje zamiar lepiej.

Przeczytaj również: Jak definiować elementy w C#? Klasy, metody i #define

Zapominanie o zgodności serializacji

Rekordy dobrze współpracują z popularnymi serializerami, ale warto sprawdzić wymagania konkretnego frameworka. Konstruktor pozycyjny, właściwości init, nazwy parametrów i ustawienia deserializacji mogą mieć znaczenie przy starszym kodzie albo nietypowej konfiguracji.

Record najlepiej działa tam, gdzie liczy się stan danych

Wybieram rekord wtedy, gdy obiekt jest przede wszystkim wartością opisującą dane, a nie właścicielem zmiennego procesu. W takich miejscach dostaję mniej kodu, bezpieczniejsze przekazywanie informacji i naturalne porównywanie instancji.

Najczęściej zaczynam od record class, pamiętam o płytkiej niemutowalności i używam with do tworzenia kolejnych wersji modelu. Po record struct sięgam dopiero wtedy, gdy świadomie potrzebuję typu wartościowego. Taki wybór jest prosty, przewidywalny i dobrze skaluje się w aplikacjach .NET.

FAQ - Najczęstsze pytania

Record class sprawdza się, gdy obiekt przede wszystkim opisuje dane, a nie zarządza zmiennym cyklem życia. Pasuje do DTO, komend, zdarzeń domenowych, identyfikatorów i modeli odpowiedzi API, ponieważ oferuje porównywanie po wartościach, wygodne kopiowanie oraz właściwości z setterem init.

Oba typy są strukturami, czyli typami wartościowymi. Zwykły record struct ma domyślnie zapisywalne właściwości pozycyjne, natomiast readonly record struct lepiej wspiera niemutowalność i pozwala tworzyć małe, niezmienne wartości, takie jak współrzędne czy temperatura.

Wyrażenie with tworzy nową instancję rekordu, ale właściwości referencyjne mogą nadal wskazywać ten sam obiekt. Jeśli rekord zawiera List, zmianę listy zobaczą zarówno oryginał, jak i kopia. Przy zagnieżdżonych danych trzeba używać niemutowalnych kolekcji albo tworzyć ich kopie.

Record struct jest kopiowany przy przypisaniu i przekazywaniu według wartości. Dla małych danych może być dobrym wyborem, ale przy większym modelu i częstym przekazywaniu może powodować niepotrzebne kopiowanie. Dlatego sam potencjalny zysk wydajności nie wystarcza do wyboru struktury.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi

rekordy
niemutowalność
dto
serializacja
record struct
Autor Radosław Krajewski
Radosław Krajewski
Nazywam się Radosław Krajewski i od 6 lat zgłębiam tajniki programowania .NET, chmury Azure oraz sztucznej inteligencji. Moja przygoda z tymi technologiami zaczęła się od fascynacji tym, jak złożone problemy można rozwiązywać za pomocą kodu i innowacyjnych narzędzi. Staram się przekazywać tę wiedzę w sposób zrozumiały, dzieląc się swoimi doświadczeniami i spostrzeżeniami na kursdotnet.pl. W moich artykułach skupiam się na praktycznych aspektach, porównuję różne rozwiązania i analizuję najnowsze trendy, aby dostarczyć Wam rzetelne i aktualne informacje, które pomogą Wam rozwijać się w tej dynamicznie zmieniającej się dziedzinie.

Udostępnij artykuł

Napisz komentarz