MySQL Java JavaScript PHP Python HTML-CSS C-sharp

Recorduri

Recordurile reprezintă un nou tip referențial, care a apărut în C# 9. Caracteristica principală a recordurilor este că pot reprezenta un tip imuabil (immutable) care, în mod implicit, are o serie de avantaje față de clase și structuri. De ce avem nevoie de tipuri imuabile?

Aceste tipuri sunt mai sigure în situațiile în care trebuie să garantăm că datele unui obiect nu vor fi modificate. În .NET există deja tipuri imuabile, de exemplu, String.

Este important de menționat că, începând cu versiunea C# 10, a fost adăugată suportul pentru structuri record, astfel putem crea clase record și structuri record.

Pentru a defini recorduri se folosește cuvântul cheie record. Dacă se definește o clasă record, cuvântul cheie class poate fi omis la definirea tipului:

public record Person
{
   public string Name { get; set; }
   public Person(string name) => Name = name;
}

Sau astfel:

public record class Person
{
   public string Name { get; set; }
   public Person(string name) => Name = name;
}

La definirea unei structuri record, trebuie folosit cuvântul cheie struct:

public record struct Person
{
   public string Name { get; set; }
   public Person(string name) => Name = name;
}

Deși recordurile sunt destinate creării tipurilor imuabile, folosirea simplă a cuvântului cheie record nu garantează imuabilitatea obiectelor record. Ele sunt imuabile doar în anumite condiții. De exemplu, putem scrie astfel:

var person = new Person("Tom");
person.Name = "Bob";
Console.WriteLine(person.Name); // Bob - datele au fost modificate

public record Person
{
   public string Name { get; set; }
   public Person(string name) => Name = name;
}

La executarea acestui cod nu va apărea nicio eroare, putem modifica valorile proprietăților obiectului Person. Pentru a-l face cu adevărat imuabil, trebuie să folosim modificatorul init în loc de setterele obișnuite pentru proprietăți.

var person = new Person("Tom");
person.Name = "Bob";    // ! eroare - proprietatea nu poate fi modificată

public record Person
{
   public string Name { get; init; }
   public Person(string name) => Name = name;
}

În acest caz, vom primi o eroare la încercarea de a modifica valorile proprietăților obiectului Person.

Recordurile sunt similare cu clasele și structurile obișnuite în multe privințe, de exemplu, pot fi abstracte, pot fi moștenite sau putem interzice moștenirea cu ajutorul operatorului sealed. Cu toate acestea, există și diferențe. Să examinăm câteva dintre principalele diferențe între recorduri și clasele și structurile standard.

Compararea pentru egalitate

La definirea unui record, compilatorul generează metoda Equals() pentru compararea cu alt obiect. Compararea a două recorduri se face pe baza valorilor acestora. De exemplu:

var person1 = new Person("Tom");
var person2 = new Person("Tom");
Console.WriteLine(person1.Equals(person2)); // true

var user1 = new User("Tom");
var user2 = new User("Tom");
Console.WriteLine(user1.Equals(user2));     // false

public record Person
{
   public string Name { get; init; }
   public Person(string name) => Name = name;
}

public class User
{
   public string Name { get; init; }
   public User(string name) => Name = name;
}

În acest caz, la compararea a două obiecte record Person, vom vedea că acestea sunt egale, deoarece valorile lor (proprietatea Name) sunt egale. Însă, în cazul obiectelor clasei User, care au aceleași valori, vom vedea că nu sunt egale, deoarece compararea recordurilor se face pe baza valorii.

În plus, pentru recorduri sunt implementați implicit operatorii == și !=, care compară două recorduri pe baza valorii:

var person1 = new Person("Tom");
var person2 = new Person("Tom");
Console.WriteLine(person1 == person2); // true

var user1 = new User("Tom");
var user2 = new User("Tom");
Console.WriteLine(user1 == user2);     // false

Operatorul with

Spre deosebire de clase, recordurile suportă inițializarea folosind operatorul with. Acesta permite crearea unui record pe baza altui record:

var tom = new Person("Tom", 37);
var sam = tom with { Name = "Sam" };
Console.WriteLine($"{sam.Name} - {sam.Age}"); // Sam - 37

public record Person
{
   public string Name { get; init; }
   public int Age { get; init; }
   public Person(string name, int age)
   {
       Name = name; Age = age;
   }
}

După recordul ale cărui valori dorim să le copiem, se utilizează operatorul with, după care, între acolade, se specifică valorile pentru proprietățile pe care dorim să le modificăm. În acest caz, variabila sam primește valoarea proprietății Age din tom, iar proprietatea Name este modificată.

Această caracteristică poate fi deosebit de utilă dacă recordul pe care dorim să-l copiem are multe proprietăți, dintre care dorim să schimbăm una-două.

Dacă trebuie să copiem valorile tuturor proprietăților, putem lăsa acoladele goale:

var person1 = new Person("Tom", 37);
var person2 = person1 with { };

Recorduri pozițional

Recordurile pot primi date pentru proprietăți prin constructor, și în acest caz putem scurta definiția lor. De exemplu, să presupunem că avem următorul record Person:

public record Person
{
   public string Name { get; init; }
   public int Age { get; init; }
   public Person(string name, int age)
   {
       Name = name; Age = age;
   }
   public void Deconstruct(out string name, out int age) => (name, age) = (Name, Age);
}

Pe lângă constructor, aici este implementat și un deconstructor, care permite descompunerea obiectului Person într-un tuple de valori. Am putea folosi aceasta, de exemplu, astfel:

var person = new Person("Tom", 37);
Console.WriteLine(person.Name); // Tom

var (personName, personAge) = person;

Console.WriteLine(personAge);     // 37
Console.WriteLine(personName);    // Tom

Recordul Person definit mai sus poate fi redus la un record pozițional:

public record Person(string Name, int Age);

Aceasta este toată definiția tipului. Adică, spunem că pentru tipul Person va fi creat un constructor care primește doi parametri și le atribuie valorile corespunzătoare proprietăților Name și Age, și că va fi creat automat și un deconstructor. Utilizarea sa va fi similară:

var person = new Person("Tom", 37);
Console.WriteLine(person); // Tom

var (personName, personAge) = person;

Console.WriteLine(personAge);     // 37
Console.WriteLine(personName);    // Tom

public record Person(string Name, int Age);

Dacă este necesar, putem combina definiția standard a proprietăților cu definiția proprietăților prin constructor:

var person = new Person("Tom", 37) { Company = "Google" };
Console.WriteLine(person.Company); // Google
person.Company = "Microsoft";
Console.WriteLine(person.Company); // Microsoft

public record Person(string Name, int Age)
{
   public string Company { get; set; } = "";
}

Structuri poziționale pentru citire

Este important să menționăm diferența dintre clasele și structurile record poziționale. Proprietățile clasei record, care sunt setate prin parametrii constructorului, vor avea implicit modificatorul init.

Adică, după setarea valorilor prin constructor, nu vom mai putea modifica aceste valori:

var person = new Person("Tom", 37);
person.Name = "Bob";    // ! Eroare - valoarea nu poate fi modificată

public record Person(string Name, int Age);

Este de menționat că acest lucru se aplică doar proprietăților care sunt setate prin constructor.

Totuși, pentru structurile record poziționale, proprietățile vor avea setterele standard, permițând modificarea valorilor:

var person = new Person("Tom", 37);
person.Name = "Bob";
Console.WriteLine(person.Name); // Bob - valoarea a fost modificată

// structura record
public record struct Person(string Name, int Age);

Pentru a folosi modificatorul init în locul setterelor standard pentru astfel de proprietăți ale structurii record, trebuie să definim structura cu cuvântul cheie readonly:

var person = new Person("Tom", 37);
person.Name = "Bob";    // ! Eroare - valoarea proprietății nu poate fi modificată

// structura record doar pentru citire
public readonly record struct Person(string Name, int Age);

Metoda ToString()

Un mic avantaj al tipurilor record este că pentru ele este implementată implicit metoda ToString(), care afișează starea obiectului într-un format structurat:

var person = new Person("Tom", 37);
Console.WriteLine(person); // Person { Name = Tom, Age = 37 }

public record Person(string Name, int Age);

Moștenirea

Ca și clasele obișnuite, recordurile pot fi moștenite:

var tom = new Person("Tom", 37);
var bob = new Employee("Bob", 41, "Microsoft");
Console.WriteLine(tom); // Person { Name = Tom, Age = 37 }
Console.WriteLine(bob); // Person { Name = Bob, Age = 41, Company = Microsoft }

public record Person(string Name, int Age);
public record Employee(string Name, int Age, string Company) : Person(Name, Age);

În acest caz, clasa record Employee moștenește de la Person.

← Lecția anterioară Lecția următoare →