1. Wprowadzenie
Pozycjonująca składnia record-klasy jest naprawdę wygodna w prostych przypadkach:
public record User(string Name, int Age);
Ale czasem masz ogromną ochotę dodać do record dodatkowe metody, niestandardowe właściwości, zmienić modyfikatory dostępu, dorzucić logikę do konstruktora (np. walidację albo "automatyczną" konwersję danych). Niestety, w pozycjonującej deklaracji nie ma gdzie tego wrzucić! Czas przejść do jawnej składni ciała, jeśli:
- Musisz rozszerzyć record o metody, właściwości lub dodatkową logikę.
- Chcesz kontrolować zachowanie właściwości (settery, gettery, inits, walidacja).
- Potrzebujesz kilku różnych konstruktorów do różnych sposobów inicjalizacji.
- Chcesz dodać interfejsy lub zaimplementować specjalne metody.
Budujemy record z ciałem
Składnia jest bardzo podobna do klasy. Kojarzysz taki zapis:
public class User
{
/* ... */
}
Tylko teraz to jest record:
public record User
{
// Jawnie zdefiniowane właściwości
public string Name { get; init; }
public int Age { get; init; }
// Dodatkowa logika
public string GetGreeting()
{
return $"Cześć, mam na imię {Name} i mam {Age} lat!";
}
// Własny konstruktor
public User(string name, int age)
{
Name = name;
if (age < 0)
throw new ArgumentException("Wiek nie może być ujemny!");
Age = age;
}
}
Ciekawostka: Jeśli jawnie definiujesz swoje właściwości, kompilator nie tworzy automatycznie właściwości z pozycjonującej składni. Wszystko jawnie, wszystko pod kontrolą.
Wersja mieszana: hybrydowa składnia
C# pozwala połączyć oba światy: możesz zadeklarować pozycjonujący record i dodać mu ciało:
public record User(string Name, int Age)
{
public string GetGreeting()
{
return $"Jestem {Name}, mam {Age} lat!";
}
}
W tym przypadku właściwości Name i Age są nadal automatycznie tworzone przez pozycjonującą składnię, a twoje dodatkowe metody wygodnie siedzą w ciele.
2. Różnice względem zwykłych record — niuanse ciała
- Jawny record pozwala w pełni kontrolować konstruktory, właściwości, metody.
- Możesz zaimplementować interfejsy albo dodać własną logikę porównywania, jeśli masz złożone zasady identyfikacji obiektu.
- W przeciwieństwie do prostego record z pozycjonującą składnią, żeby dodać nowe właściwości wystarczy je zadeklarować w ciele recorda.
// Błąd początkujących!
public record User(string Name, int Age)
{
public string Name { get; init; } // ← konflikt! Powtórna deklaracja właściwości
}
Błędy początkujących: Niektórzy studenci próbują jednocześnie pisać public record User(string Name, int Age) i dodawać do ciała właściwość public string Name { get; init; }, myśląc, że to dwie różne zmienne. Nie! To będzie konflikt (powtórna deklaracja). Albo używasz w pełni pozycjonującej składni, albo jawnie definiujesz właściwości — nie mieszaj.
Praktyczne przykłady
Kontynuujemy pisanie aplikacji konsolowej, w której użytkownik tworzy zamówienia. Załóżmy, że mamy klasę zamówienia Order, która do tej pory wyglądała tak:
public record Order(string Product, int Quantity, double Price);
Załóżmy, że teraz potrzebujemy walidacji ilości (quantity nie może być mniejsze niż 1) i dodatkowej właściwości — licznika całkowitego kosztu:
public record Order
{
public string Product { get; init; }
public int Quantity { get; init; }
public double Price { get; init; }
public double TotalCost => Quantity * Price;
public Order(string product, int quantity, double price)
{
Product = product ?? throw new ArgumentNullException(nameof(product));
if (quantity < 1)
throw new ArgumentException("Ilość musi być co najmniej 1!");
Quantity = quantity;
Price = price;
}
public override string ToString()
=> $"Produkt: {Product}, Ilość: {Quantity}, Suma: {TotalCost}";
}
Zwróć uwagę, że jawnie zdefiniowaliśmy właściwości, zrobiliśmy je z init-setterem (obiekty niezmienne), dodaliśmy automatyczne liczenie kosztu — kod jest bardziej elastyczny!
Wywołanie w programie:
var order = new Order("Rower", 2, 15000);
Console.WriteLine(order); // Produkt: Rower, Ilość: 2, Suma: 30000
3. record struct
Ewolucja struct-ów
Przed pojawieniem się record struct struktury w C# były "prostymi wołami roboczymi" — szybko kopiowane, trzymane na stosie, świetne do krótkich “Parcel-i” danych (np. współrzędnych albo kolorów). Ale nie miały wszystkich bajerów records: nie było pozycjonującej składni, with-wyrażeń, porównywania po wartości domyślnie i innych “słodkich bułeczek”.
Teraz w C# można deklarować struktury w stylu record:
public record struct Point(int X, int Y);
Co to w ogóle daje?
- Automatyczna implementacja Equals, GetHashCode, ToString — twoja struktura umie się ładnie porównywać i drukować!
- Składnia with-klonowania: var p2 = p1 with { X = 10 };
- Możliwość użycia pozycjonującej lub jawnej składni.
Porównanie: klasyczny struct VS record struct
| struct | record struct | |
|---|---|---|
| Pozycjonująca składnia | Nie | Tak |
| with-wyrażenie | Nie | Tak |
| Niezmienność | Nie (domyślnie) | Tak (init) |
| Porównanie po wartościach | Nie (domyślnie) | Tak |
| ToString | Standardowy | Ładny |
Jawna składnia ciała dla record struct
Wszystko tak samo jak w zwykłym record, tylko struct:
public record struct Rectangle
{
public int Width { get; init; }
public int Height { get; init; }
public int Area => Width * Height;
public Rectangle(int width, int height)
{
Width = width > 0 ? width : throw new ArgumentException("Szerokość > 0");
Height = height > 0 ? height : throw new ArgumentException("Wysokość > 0");
}
public void Print()
{
Console.WriteLine($"Wymiary: {Width} x {Height}, pole: {Area}");
}
}
Przykład użycia:
var rect = new Rectangle(10, 7);
rect.Print(); // Wymiary: 10 x 7, pole: 70
// Klonujemy i zmieniamy szerokość, nie ruszając oryginału
var wideRect = rect with { Width = 20 };
wideRect.Print(); // Wymiary: 20 x 7, pole: 140
Specyfika record struct
- To wciąż struct — value type. Kopiuje się przy przypisaniu!
- Wszystkie plusy records: porównanie po wartościach, with-klonowanie, ładny ToString.
- Zalecane do małych, kompaktowych niezmiennych zbiorów danych, gdzie ważne jest unikanie alokacji na stercie.
- Możesz zadeklarować pozycjonujące parametry albo jawnie “rozwinąć” ciało.
4. Typowe błędy i pułapki
Nadmierna mutowalność: record struct nie robi pól automatycznie niezmiennymi, jeśli zadeklarujesz je jako zwykłe (np. public int Value;). Używaj init-setterów dla truly immutable struct!
Porównywanie: Jeśli dodajesz nowe pola ręcznie (nie w pozycjonującej składni), pamiętaj: tylko te pola, które są w konstruktorze lub mają init-setter, biorą udział w auto-porównywaniu po wartości.
Kopiowanie: To struct, więc... wszystko się kopiuje! Nie myl z referencyjnymi recordami.
Zamieszanie z with-wyrażeniami: One zawsze robią shallow copy, czyli NIE robią głębokiego kopiowania zagnieżdżonych obiektów.
GO TO FULL VERSION