CodeGym /Kursy /C# SELF /Wprowadzenie do record

Wprowadzenie do record i składni pozycyjnej

C# SELF
Poziom 19 , Lekcja 1
Dostępny

1. Problem przekazywania danych

Załóżmy, że mamy apkę dla szkoły i musimy przekazywać info o uczniu między różnymi modułami: imię, rok urodzenia, klasa. Jak zwykle to robimy?

Tworzymy klasę typu:


public class Student
{
    public string Name { get; set; }
    public int YearOfBirth { get; set; }
    public string Class { get; set; }
}
Klasa do przechowywania danych o uczniu (stare podejście)

Wygląda znajomo. Ale taki sposób ma kilka problemów:

  • Żeby porównać dwóch uczniów pod kątem równości, domyślnie porównywane jest tylko referencja do obiektu. Czyli dwóch uczniów z tymi samymi polami, ale innymi obiektami — nie są równe!
  • Klasę można zmienić po utworzeniu, co czasem prowadzi do bugów (szczególnie jeśli obiekt już gdzieś jest używany);
  • Dużo „szablonowego” kodu: konstruktory, metody porównania, kopiowania (klony), ToString.

I jak już pewnie się domyślasz: C# może nas od tego uwolnić! Poznaj — record.

2. Czym jest record?

record — to specjalny typ w C#, zaprojektowany specjalnie do przechowywania danych. Record ma dwie główne cechy:

  1. Niezmienność (immutability): obiekty typu record są domyślnie niezmienne, czyli wartości ich właściwości ustalasz raz przy tworzeniu i już nie zmieniasz (dokładniej: set-settery są prywatne). Można zadeklarować też zmienne recordy, ale domyślnie — niezmienność.
  2. Porównywanie po wartości: jeśli dwa obiekty record mają takie same wartości we wszystkich polach, są uznawane za równe (== i .Equals() działają inaczej!).

Tak naprawdę, record — to idealna otoczka do przekazywania danych między warstwami aplikacji (np. z bazy do kontrolera, z kontrolera do widoku itd.).

3. Składnia record

Najprościej — składnia pozycyjna

Kiedy musimy po prostu przekazać zestaw wartości, deklarujemy typ jedną krótką linijką:


public record Student(string Name, int YearOfBirth, string Class);
Składnia pozycyjna recorda

Co się dzieje pod spodem? Kompilator sam wygeneruje dla nas:

  • Automatyczne właściwości tylko do odczytu (z prywatnym setterem);
  • Konstruktor przyjmujący wszystkie parametry;
  • Metody porównania i kopiowania;
  • Fajny ToString, który ładnie formatuje wyjście!

Użycie pozycyjnego recorda

Spróbujmy użyć tego nowego typu w naszej szkolnej apce:

var student1 = new Student("Ivan", 2008, "8A");
var student2 = new Student("Maria", 2008, "8B");

Dostęp do właściwości jest jak zwykle (tylko nie możesz ich zmieniać):

Console.WriteLine($"{student1.Name}, {student1.YearOfBirth}, {student1.Class}");

Próba zmiany właściwości po utworzeniu

student1.Name = "Piotr"; // Błąd! Właściwość tylko do odczytu.

Jeśli odkomentujesz taką linijkę — kompilator od razu się obrazi: nie można ustawić wartości dla właściwości tylko do odczytu.

Tak wygląda automatycznie wygenerowany ToString

Console.WriteLine(student1); // Wypisze: Student { Name = Ivan, YearOfBirth = 2008, Class = 8A }

Wszystko czytelne i jasne nawet bez ręcznego formatowania!

4. Porównywanie obiektów record

Przypomnę: jeśli utworzysz dwa różne obiekty klasycznej klasy z tymi samymi danymi, i tak nie będą sobie równe:

var a = new Student("Ivan", 2008, "8A");
var b = new Student("Ivan", 2008, "8A");

Console.WriteLine(a == b); // Dla klasy: false

Jeśli jednak Student to record, równość działa tak, jak byś chciał:


public record Student(string Name, int YearOfBirth, string Class);

var a = new Student("Ivan", 2008, "8A");
var b = new Student("Ivan", 2008, "8A");

Console.WriteLine(a == b); // Dla record: true!
Porównywanie obiektów record po wartości

Czyli dwa recordy z tymi samymi polami są sobie równe nawet jeśli to dwa różne obiekty w pamięci.

5. Jak to wygląda w środku

Studenci często się dziwią, ile kompilator robi za nas w przypadku recorda. Porównajmy dla jasności, jaki kod trzeba by pisać ręcznie dla klasy i co robi record.

Stara dobra klasa napisana ręcznie

public class Student
{
    public string Name { get; }
    public int YearOfBirth { get; }
    public string Class { get; }

    public Student(string name, int yearOfBirth, string @class)
    {
        Name = name;
        YearOfBirth = yearOfBirth;
        Class = @class; //referencja do własnej klasy
    }

    public override bool Equals(object? obj)
    {
        if (obj is not Student other) return false;
        return Name == other.Name && YearOfBirth == other.YearOfBirth && Class == other.Class;
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(Name, YearOfBirth, Class);
    }

    public override string ToString()
    {
        return $"Student {{ Name = {Name}, YearOfBirth = {YearOfBirth}, Class = {Class} }}";
    }
}

Nic dziwnego, że programiści stają się paranoikami — przecież piszemy to samo po 10 razy!

Record — jedna linijka


public record Student(string Name, int YearOfBirth, string Class);
Record — to samo, ale w jednej linijce!

6. Record i niezmienność: co wolno, a czego nie

Recordy domyślnie mają właściwości tylko do odczytu i to świetnie zapobiega masie bugów. Ale jeśli bardzo chcesz (np. piszesz pod bardzo stare API), możesz zadeklarować też zmienne recordy:

public record MutableStudent
{
    public string Name { get; set; }
    public int YearOfBirth { get; set; }
    public string Class { get; set; }
}

Teraz te pola można zmieniać, ale tracisz część zalet (np. bezpieczeństwo).

7. Destrukturyzacja recorda

Ponieważ składnia pozycyjna bardzo przypomina tuple, można łatwo destrukturyzować record:


var student = new Student("Ivan", 2008, "8A");

var (name, year, className) = student;

Console.WriteLine($"{name} - {year}, {className}"); // Ivan - 2008, 8A
Destrukturyzacja pozycyjnego recorda

Kompilator generuje metodę Deconstruct dla każdego pozycyjnego recorda i to ułatwia pracę z LINQ, switch-patternami i ogólnie uprzyjemnia życie.

8. Record — to klasa czy struct?

Domyślnie record — to typ referencyjny, jak klasa. Czyli wszystkie cechy zachowania typów referencyjnych (przechowywanie na stercie, kopiowanie referencji itd.) działają normalnie.

Jeśli chcesz typ wartościowy (value type), to w C# też jest opcja — zawsze możesz napisać:


public record struct Point(int X, int Y);
Record struct — wartościowy typ recorda

Ale do przekazywania danych prawie zawsze używa się klasycznej formy recordów, czyli typ referencyjny. Więcej o record struct — w kolejnych wykładach :P

Porównanie class, struct, record

Typ Niezmienność domyślnie Porównanie po wartości Łatwa destrukturyzacja Auto ToString
class Nie Nie (po referencji) Nie Nie
struct Nie Tak Nie Nie
record Tak Tak Tak Tak

9. Cechy i typowe błędy

Wielu początkujących gubi się w niuansach używania recordów. Na przykład czasem oczekują, że zmiana pola w jednym obiekcie record zmieni drugi (jak w klasach z kopiowaniem referencji). Nie! Poza tym, gdy piszesz with, pamiętaj, że zawsze tworzona jest kopia, a nie zmieniany oryginał. Recordy są idealne do logiki, gdzie twoja apka dba o czystość danych i przewidywalność.

Tak, jeśli zadeklarujesz pola z init zamiast set, też możesz je ustawić tylko przy tworzeniu lub przez with, ale nie później.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION