CodeGym /Kursy /C# SELF /Różnice między record, class i struct

Różnice między record, class i struct

C# SELF
Poziom 19 , Lekcja 4
Dostępny

1. Po co w ogóle powstały record

W C# (i .NET ogólnie) przez długi czas głównymi budulcami były klasy (class) i struktury (struct). Ale każdy z nich jest niepełny do pewnych zadań. Klasy — referencyjne, mutowalne i porównywane po referencji (z bardzo rzadkimi wyjątkami). Struktury — typy wartościowe (kopiowane przy przekazaniu), domyślnie porównywane bajt po bajcie i najczęściej mutowalne (dopóki nie pojawiły się readonly struct).

A jeśli chciałeś prosty i naturalny zapis danych, który łatwo porównać po wartości, szybko sklonować i nie martwić się, że ktoś gdzieś zmienił twój obiekt — trzeba było kombinować na własną rękę albo korzystać z bibliotek typu ValueTuple czy nawet System.Tuple. Ale to wszystko nie jest tak fajne i czytelne, jak by się chciało.

Dlatego w C# 9 pojawił się record — typ danych, który łączy zwięzłość deklaracji, bezpieczeństwo niezmienności i zachowanie nastawione na porównywanie po wartości.

2. Wszystkie cztery typy na jednej stronie

class struct record record struct
Kategoria Typ referencyjny Typ wartościowy Typ referencyjny Typ wartościowy
Mutowalność Domyślnie — tak Domyślnie — tak Domyślnie — nie (init) Domyślnie — nie (init)
Porównanie Po referencji (==) Po wartości (pola) Po wartości (fields/properties) Po wartości (fields)
Klonowanie Tylko ręcznie Tylko ręcznie Jest wsparcie wbudowane (with) Jest wsparcie wbudowane (with)
Dziedziczenie Tak Nie Tak Nie
Niezmienność Trzeba zaimplementować Trzeba zaimplementować Bardzo łatwo zaimplementować Bardzo łatwo zaimplementować
Składnia Najdłuższa Krótka Najkrótsza (pozycyjna) Dość krótka
Użycie w kolekcjach Po referencji Kopie Po referencji Kopie

Schemat wizualny

+----------------+     +----------------+     +--------------------+
|    class       |     |    struct      |     |      record        |
+----------------+     +----------------+     +--------------------+
| Reference Type |     | Value Type     |     | Reference Type     |
| Mutable        |     | Mutable        |     | Immutable (init)   |
| == : Reference |     | == : By Fields |     | == : By Value      |
+----------------+     +----------------+     +--------------------+

3. Istota różnic między record, class i struct

Zachowanie w pamięci: referencja czy wartość?

  • class i record — to typy referencyjne. Gdy przekazujesz je do funkcji, kopiowana jest referencja do obiektu.
  • struct i record struct — typy wartościowe. Zawsze kopiowane bajt po bajcie (chyba że jawnie przekażesz je przez referencję).

class PointClass { public int X; public int Y; }
struct PointStruct { public int X; public int Y; }
record PointRecord(int X, int Y);
record struct PointRecordStruct(int X, int Y);

void Demo()
{
    var pc = new PointClass { X = 1, Y = 2 };
    var ps = new PointStruct { X = 1, Y = 2 };
    var pr = new PointRecord(1, 2);
    var prs = new PointRecordStruct(1, 2);

    ChangeY(pc);    // pc.Y się zmieni!
    ChangeY(ps);    // ps.Y się nie zmieni — kopia!
    ChangeY(pr);    // pr.Y się zmieni!
    ChangeY(prs);   // prs.Y się nie zmieni — kopia!
}

void ChangeY(dynamic p) { p.Y = 99; }

Jeśli wydaje ci się, że dynamic to jakaś magia — tak, to tylko dla przykładu, żebyś zobaczył, że przy pracy ze strukturami wartość się nie zmieni, a przy pracy z klasą lub record (referencyjnym) — zmieni się.

Porównanie: jak sprawdzić, że dwa obiekty są równe?

  • class: porównywane po referencji (== — true tylko jeśli to ten sam obiekt w pamięci), jeśli nie nadpiszesz Equals.
  • struct: porównywane po wartości wszystkich pól (domyślnie).
  • record: porównywane po wartości wszystkich pól/właściwości zadanych w głównym konstruktorze.
class Foo { public int A; public int B; }
record Bar(int A, int B);

var foo1 = new Foo { A = 42, B = 1 };
var foo2 = new Foo { A = 42, B = 1 };
var bar1 = new Bar(42, 1);
var bar2 = new Bar(42, 1);

Console.WriteLine(foo1 == foo2); // False! Różne obiekty
Console.WriteLine(bar1 == bar2); // True! Wartości się zgadzają

Ciekawostka:
Z record struct jest jeszcze ciekawiej: mają porównanie "po wartości", jak zwykłe struct, ale ze składnią i możliwościami record.

4. Niezmienność: kto ile gwarantuje?

Porównujemy bezpieczeństwo obiektów:

  • class: Domyślnie łatwo zmienialny, jeśli nie zrobisz wszystkich pól readonly.
  • struct: To samo, ale możesz zadeklarować jako readonly struct, wtedy ani pola, ani właściwości nie można zmieniać.
  • record: Zazwyczaj pola są z modyfikatorem init, czyli można je ustawić tylko w konstruktorze lub inicjalizatorze obiektu (with). To wygodne do bezpiecznego przekazywania danych.
  • record struct: To samo, możesz zrobić readonly record struct i mieć niezmienność typów wartościowych z darmowymi bonusami record.

record Person(string Name, int Age);

var p1 = new Person("Aleksiej", 23);
// p1.Age = 24; // Błąd! Tylko init

var p2 = p1 with { Age = 24 }; // Działa! Tworzy kopię z nowymi danymi

Gdy masz duży projekt i dziesiątki entity-obiektów, record pozwolą uniknąć masy bugów związanych z "ktoś gdzieś zmienił pole — i wszystko się posypało".

5. Składnia: jak wyglądają deklaracje i jak się nie pogubić


// class
public class Product
{
    public int Id { get; init; }
    public string Name { get; init; }
}

// struct
public struct Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

// record
public record Product(int Id, string Name);

// record struct
public record struct Point(int X, int Y);
Porównanie składni deklaracji różnych typów

Jak widzisz, zapis jest najkrótszy dla typu record. Dostajesz konstruktor, destruktor, porównanie, ToString() i wiele więcej "z pudełka".

Składnia Dostajesz za darmo Czy można dziedziczyć
class nic (prawie) Tak
struct nic (prawie) Nie
record ToString, Equals, Deconstruct, with Tak
record struct ToString, Equals, Deconstruct, with Nie

6. Klonowanie i operator with

Tylko record i record struct mają specjalny operator with, który pozwala bez bólu kopiować obiekt ze zmianą wybranych właściwości.


record User(string Name, int Age);

var user1 = new User("Irina", 28);
var user2 = user1 with { Age = 29 }; // user2.Name == "Irina", user2.Age == 29

W przypadku klasy (class) trzeba zaimplementować metodę kopiowania ręcznie, a jeśli zapomnisz skopiować któreś pole — witaj, bugi.

7. Dziedziczenie: kto kogo

  • class: Wspiera standardowe dziedziczenie (hierarchie klas, metody wirtualne, abstrakcje itd.).
  • struct: Nie wspiera (może tylko implementować interfejsy).
  • record: Wspiera dziedziczenie, ale z pewnymi ograniczeniami (np. dziedziczenie możliwe tylko między typami record, nie z class).
  • record struct: Nie wspiera, jak zwykłe struct.
record Animal(string Name);
record Dog(string Name, string Breed) : Animal(Name); // OK!

class Vehicle { }
class Car : Vehicle { } // OK!

// struct nie może dziedziczyć po struct

Więcej o dziedziczeniu w kolejnym poziomie :P

8. Gdzie jakie typy stosować w praktyce

  • class: Duże obiekty z bogatym zachowaniem, długim cyklem życia, zmiennym stanem, hierarchią (np. logika biznesowa, komponenty UI).
  • struct: Małe obiekty-wartości, gdzie ważne jest szybkie kopiowanie, brak GC i minimalny narzut (np. współrzędne, kolory, sumy — coś, co łatwo i szybko sklonować).
  • record: DTO (Data Transfer Object), obiekty-parametry, parametry konfiguracji, niezmienne stany, wyniki obliczeń, które wygodnie porównywać po zawartości.
  • record struct: Typy wartościowe, gdzie potrzebna jest niezmienność i zachowanie jak w records, ale bez zbędnych alokacji na stercie.

9. Typowe błędy i pułapki

Czasem programiści myślą, że record to po prostu "zamiennik klasy". To nieprawda! Jeśli tworzysz obiekt, który ma zmieniać swój stan w trakcie życia — użyj class.
Jeśli chcesz porównywać obiekty po referencji (np. w wzorcu "singleton" albo gdy cykl życia obiektu jest krytyczny) — użyj class.
Jeśli robisz obiekt-wartość, który ma się zachowywać jak liczba lub punkt na siatce współrzędnych — struct albo record struct.
Jeśli pracujesz z niezmiennymi, łatwo porównywalnymi obiektami, które często przekazujesz między warstwami aplikacji, trzymasz w kolekcjach, logujesz i serializujesz — twój wybór to record.

Pamiętaj też: jeśli zadeklarowałeś record-klasę z tylko do odczytu właściwościami, ale zapomniałeś o zagnieżdżonych obiektach — zagnieżdżone pola nadal mogą być zmienione, jeśli same są mutowalne.


record Student(string Name, List<int> Grades);

var s1 = new Student("Anton", new List<int>() {5,5,5});
var s2 = s1 with { };

s2.Grades.Add(2); // Oba obiekty "przeklęte" dwójką! s1.Grades == s2.Grades
1
Ankieta/quiz
Problem z DTO, poziom 19, lekcja 4
Niedostępny
Problem z DTO
DTO, record i with
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION