Wstęp

Rekordy weszły do C# w wersji 9. Obok struktur i klas stały się trzecim „ogólnym” typem. Chociaż są podobne zarówno do klas jak i struktur, to jednak dostarczają pewnych mechanizmów, które mogą przyspieszyć i ułatwić pisanie.

Zanim przeczytasz ten artykuł, powinieneś znać dokładne różnice między klasami i strukturami, stosem i stertą. Na szczęście opisałem je w tym artykule 🙂

Czym właściwie jest rekord?

Rekord to typ danych, który może zachowywać się tak jak struktura (być typem wartościowym) lub klasa (być typem referencyjnym). Jednak jasno musimy to określić podczas jego deklaracji. Rekordy rządzą się też pewnymi prawami. Gdy tworzymy rekord, kompilator dodaje automatycznie kilka elementów. O tym za chwilę. Najpierw spójrz jak rekordy się deklaruje.

Jest kilka możliwości deklaracji rekordu, najprostszą z nich jest:

public record MyRecord(int Id, string Name);

I tyle. Jedna linijka. Czy to nie jest piękne? Gdyby to przetłumaczyć na deklarację klasy, otrzymalibyśmy mniej więcej coś takiego:

public class MyRecord
{
    public int Id { get; init; }
    public string Name { get; init; }

    public MyRecord(int id, string name)
    {
        Id = id;
        Name = name;
    }

    public static bool operator==(MyRecord x, MyRecord y)
    {
         //o tym później
    }

    public static bool operator!=(MyRecord x, MyRecord y)
    {
        return !(x == y);
    }

    public override bool Equals(object? obj)
    {
        //o tym później
    }

    public void Deconstruct(out int id, out string name)
    {
         id = Id;
         name = Name;
        //o tym później
    }
}

A to i tak tylko szkielet 🙂

Jak można z tego korzystać? Dokładnie tak samo jak z każdej innej klasy:

static void Main(string[] args)
{
    var rec = new MyRecord(5, "Adam");
}

Jednak, jak wspomniałem wcześniej, rekord to coś więcej niż tylko cukier składniowy na klasę, czy strukturę. Gdy deklarujemy typ rekordowy, kompilator dokłada co nieco od siebie:

Porównanie wartościowe

Rekordy są automatycznie wyposażone w operatory ==, != i metodę Equals. Te elementy porównują dwa rekordy wartościowo. Tzn., że rekordy są takie same wtedy i tylko wtedy, gdy:

  • są tego samego typu
  • WARTOŚCI wszystkich właściwości są takie same.

Zwróć uwagę na słowo „WARTOŚCI”. Jeśli porównujesz ze sobą dwa obiekty, one domyślnie są porównywane referencyjnie. Czyli dwa obiekty będą takie same, jeśli wskazują na to samo miejsce w pamięci. Przy rekordach porównywane są domyślnie WARTOŚCI wszystkich pól.

Jednak pamiętaj, że to muszą być typy wartościowe. Jeśli rekord będzie zawierał klasę, to referencje obiektów tych klas zostaną ze sobą porównane (można powiedzieć, że wartością klasy jest referencja). Lepiej to wygląda na przykładzie. Porównajmy rekordy, które mają te same dane, których wartości można porównać:

var rec = new MyRecord(5, "Adam");
var rec2 = new MyRecord(5, "Adam");

Console.WriteLine(rec == rec2); // TRUE

Dla przykładu, jeśli to byłyby klasy, zostałyby porównane referencyjnie, a nie wartościowo:

var c1 = new MyClass //KLASA
{
    Name = "Adam"
};

var c2 = new MyClass //KLASA
{
    Name = "Adam"
};

Console.WriteLine(c1 == c2); // FALSE

Jednak gdy teraz dodamy klasę (typ referencyjny) do rekordu i będziemy chcieli porównać:

public record MyRecord(MyClass data);

//

var c1 = new MyClass
{
    Name = "Adam"
};

var c2 = new MyClass
{
    Name = "Adam"
};

var rec1 = new MyRecord(c1);
var rec2 = new MyRecord(c2);
Console.WriteLine(rec1 == rec2); //FALSE

No to obiekty c1 i c2 zostaną jednak porównanie referencyjnie i te dwa rekordy będą różne.

Czyli – 2 rekordy są takie same, gdy są tego samego typu i WARTOŚCI ich pól są takie same. W przypadku klasy, można powiedzieć, że jej wartością jest ADRES, na jaki wskazuje na stercie.

Niezmienialność (Immutability)

Rekordy są domyślnie niezmienialne (jest od tego wyjątek). Jeśli tak zadeklarujesz rekord:

public record Person(string Name);

to po utworzeniu obiektu tego rekordu nie będziesz mógł już go zmienić. Czyli wszystkie dane przekazujesz w konstruktorze i dalej nie można już niczego modyfikować.

Ale to nie dotyczy rekordów „strukturowych”, o których za chwilę.

Dekonstrukcja rekordów

Deklarując rekord, kompilator automatycznie tworzy też specjalną metodę Deconstruct, która w C# ma konkretne znaczenie. Służy do… dekonstrukcji obiektu 🙂 Dekonstrukcja jest znana z Tupli. O co chodzi? Spójrz na to:

public record struct Person(int Id, string FirstName, string LastName);

//
var p1 = new Person(1, "Adam", "Jachocki");
var (id, fName, lName) = p1;

Przyjrzyj się temu kodowi, bo jeśli nie słyszałeś o dekonstrukcji, to może wydawać się błędny. Ale jest zupełnie poprawny. Teraz wszystkie wartości z obiektu trafią do poszczególnych zmiennych: id, fName i lName. Osobiście bardzo uwielbiam dekonstrukcję.

Metoda ToString()

Rekordy mają również predefiniowaną metodę ToString, która zwraca wszystkie wartości w takiej postaci:

Person { Name = Adam, Age = 38 }

Różne rodzaje rekordów

Rekordy ze względu na swoją specyfikę, mogą zachowywać się na trzy różne sposoby. Chociaż wszystkie gwarantują porównywanie wartościowe i inne te rzeczy, o których pisałem wyżej, to jednak są pewne minimalne różnice.

Rekord klasowy (record)

Deklarujesz go w ten sposób:

public record Person(string Name);

//od C#10 możesz napisać również
//public record class Person(string Name);

Wyróżnia się tym, że jest tworzony na stercie. Jest domyślnie niezmienialny. Ma tylko jeden konstruktor.

Rekord strukturowy (record struct)

Deklarujesz go w taki sposób:

public record struct Person(string Name);

Wyróżnia się tym, że jest tworzony na stosie. Domyślnie jest zmienialny – możesz nadpisywać właściwości ile tylko chcesz. Ma też dwa konstruktory – ten z parametrami i domyślny – bezparametrowy.

Rekord strukturowy tylko do odczytu (readonly record struct)

Deklarujesz go tak:

public readonly record struct Person(string Name);

Wyróżnia się tym, że jest tworzony na stosie. Domyślnie jest NIEZMIENIALNY. Jednak kompilator tworzy dla niego dwa konstruktory – domyślny (bezparametrowy) i ten z parametrami.

Jeśli używasz konstruktora bezparametrowego, jedyną opcją na przypisanie wartości poszczególnym polom, jest przypisane podczas tworzenia obiektu:

var p = new Person { Name = "Adam" };

Dodatkowe pola w rekordzie

W rekordach możesz umieszczać dodatkowe pola i metody. Robisz to w sposób standardowy:

//Deklaracja
public readonly record struct Person(string Name)
{
    public int Age { get; init; }   
}

//użycie
var p1 = new Person("Adam") { Age = 38 };

Age jest zadeklarowane z setterm inicjującym. Dlatego możesz nadać mu wartość tylko w trakcie tworzenia obiektu.

Rekordy mogą mieć też normalne metody.

Dziedziczenie

Rekordy mogą dziedziczyć po innych rekordach. Ale nie mogą dziedziczyć po klasach, a klasy nie mogą dziedziczyć po rekordach. Dotyczy to jednak tylko rekordów klasowych. Rekordy strukturowe nie dziedziczą. Rekordy klasowe mogą też implementować interfejsy:

public interface IIface
{
    void Foo();
}
public record Person(string Name);
public record Emloyee(int DeptId, string Name) : Person(Name), IIface
{
    public void Foo()
    {
        throw new NotImplementedException();
    }
}

Kopiowanie rekordów

Fajną sprawą jest kopiowanie rekordów. W bardzo łatwy sposób można stworzyć rekord na podstawie istniejącego. Służy do tego słowo with:

var p1 = new Person("Adam") { Age = 38 };

var p2 = p1 with { Name = "Janek" };

Teraz obiekt p2 będzie miał Name ustawiony na Janek, a Age na 38.

Pamiętaj jednak, że jeśli w rekordzie jest pole z klasą, to wtedy zostanie skopiowana referencja do tego obiektu, a nie jego wartości. Czyli mając taki program:

//deklaracja
public class MyClass
{
    public int Id { get; set; }
}
public readonly record struct Person(string Name, MyClass Data)
{
    public int Age { get; init; }   
}

//użycie
var c1 = new MyClass { Id = 1 };
var p1 = new Person("Adam", c1) { Age = 38};

var p2 = p1 with { Name = "Janek" };
c1.Id = 10;

pamiętaj, że klasa w strukturach wskazuje na to samo miejsce na stercie. Czyli p2.Data.Id będzie również równe 10.

Kiedy używać rekordów

Z zasady, jeśli potrzebujesz danych, które są:

  • porównywane wartościowo
  • niezmienialne

Rekordy są idealnym kandydatem dla bindingu w kontrolerach. Chociaż ja głównie ich używam, jeśli mój serwis musi zwrócić do kontrolera więcej niż jedną wartość.


To tyle, jeśli chodzi o rekordy. Dzięki za przeczytanie artykułu. Mam nadzieję, że teraz już znasz różnice między typami rekordów i, że będziesz ich używał bardziej świadomie. Albo w ogóle zaczniesz 🙂

Jeśli czegoś nie rozumiesz lub znalazłeś błąd w artykule, koniecznie daj znać w komentarzu 🙂

Podziel się artykułem na: