Cuprins >> Obiecte > Înregistrări (records)
Articol de importanță mică, sau discuție

Una din trăsăturile de limbaj importante care au fost introduse în C# versiunea 9.0 (.NET 5.0) sunt înregistrările sau clasele de înregistrări. Înregistrările sunt a treia modalitate de declarare a tipurilor de date personalizate, celelalte două fiind clasele și structuri. Dar, înainte de a vi le prezenta, să analizăm motivele care au dus la crearea lor.

După cum vom afla mai târziu, există numeroase momente când va trebui să creăm un tip de date personalizat, care nu va face altceva decât să mențină informații. Exemplu:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System;
 
namespace BunaLume
{
    public class Student
    {
        public string Nume { get; set; }
        public string Prenume { get; set; }
        public int Varsta { get; set; }
    }
    public class Program
    {
        static void Main(string[] args)
        {
            Student student = new Student();
            student.Nume = "Popescu";
            student.Prenume = "Ion";
            student.Varsta = 18;
            
            Console.ReadLine();
        }
    }
}

Dacă vă uitați atent la clasa Student, observați că este un obiect "prost", nu conține metode sau funcții (cu alte cuvinte, funcționalitate), ci doar câteva proprietăți care pot stoca date, precum numele sau vârsta unui student. Această varietate de tipuri este denumită în mod obișnuit DTO (Data Transfer Object - obiect de transferat date). Singurul lor scop este acela de a păstra date și de a le transfera între diferitele părți ale unui program - în loc să apelăm o funcție cu trei parametri distincți, cum ar fi vârsta, numele și prenumele, îi putem "grupa" într-un singur tip student și transmite pe acesta ca parametru. Aceste tipuri de obiecte sunt adesea folosite împreună cu bazele de date - coloanele dintr-un tabel ar trebui să oglindească tipul și numele proprietăților dintr-un DTO, astfel încât fiecare "rând" din acel tabel al bazei de date să poată fi stocat într-o instanță a respectivului DTO.

Această abordare funcționează bine, dar are câteva dezavantaje:

  • Trebuie să creăm o nouă clasă pentru fiecare dintre aceste tipuri, și o mare parte din cod poate fi duplicat. Acest lucru este agravat dacă urmăm îndrumarea de a plasa fiecare clasă în propriul ei fișier
  • Sintaxa este foarte întinsă, cu toate getter-ele și setter-ele fiecărei proprietăți. Crearea unor astfel de obiecte este consumatoare de timp și nu este esențială.
  • Verbozitatea crește dacă dorim să facem proprietățile imuabile (pot fi setate inițial, dar nu modificate ulterior), deoarece trebuie să creăm un constructor cu parametri obligatorii și să atribuim manual valorile parametrilor la proprietățile read-only, inițializându-le.

Acum, să facem același lucru, folosind înregistrări:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;
 
namespace BunaLume
{
    public record Student(string Nume, string Prenume, int Varsta);
    class Program
    {
        static void Main(string[] args)
        {
            Student student = new Student("Popescu", "Ion", 18);
            
            Console.ReadLine();
        }
    }
}

Trebuie să fiți de acord că sintaxa este mult mai concisă și codul surplus este minim. Cu toate acestea, ar trebui să știți că există câteva mici diferențe între utilizarea unei clase și a unei înregistrări:

  • Înregistrările sunt imuabile în mod implicit. Acest exemplu nu se va compila:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    using System;
     
    namespace BunaLume
    {
        public record Student(string Nume, string Prenume, int Varsta);
        class Program
        {
            static void Main(string[] args)
            {
                Student student = new Student("Popescu", "Ion", 18);
                student.Varsta = 20; // va cauza o eroare, proprietatile doar-initiere nu pot fi schimbate
                
                Console.ReadLine();
            }
        }
    }
  • Ele implementează egalitatea în mod implicit. Dacă creez două instanțe de Student, cu aceleași valori, acestea sunt considerate egale, dar nu sunt aceeași instanță.

Înregistrările pot fi actualizate sau clonate folosind cuvântul cheie with:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;
 
namespace BunaLume
{
    public record Student(string Nume, string Prenume, int Varsta);
    class Program
    {
        static void Main(string[] args)
        {
            Student student = new Student("Popescu", "Ion", 18);
            student = student with { Varsta = 20 }; // actualizeaza valoarea proprietatii Varsta pe aceeasi instanta de inregistrare
            var altStudent = student with { }; // cloneaza instanta de inregistrare intr-una noua
            
            Console.ReadLine();
        }
    }
}

În mod implicit, o înregistrare este tratată ca o clasă, fiind un tip de referință stocat în memoria Heap. De la versiunea C# 10.0, puteți declara înregistrările ca structuri, care sunt tipuri de valoare și sunt stocate în memoria Stack (stivă):

1
2
public record Student(string Nume, string Prenume, int Varsta); // inregistrare ca si clasa
public record struct Punct(int X, int Y); // inregistrare ca si structura

Înregistrările pot avea proprietățile definite și în mod explicit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using System;
 
namespace BunaLume
{
    public record Student
    {
        public string Nume { get; set; }
        public string Prenume { get; set; }
        public int Varsta { get; set; }
        
        public Student(string Nume, string Prenume, int Varsta)
        {
            this.Nume = Nume;
            this.Prenume = Prenume;
            this.Varsta = Varsta;
        }
    }
    public class Program
    {
        static void Main(string[] args)
        {
            Student student = new Student("Popescu", "Ion", 18);
            student.Varsta = 20; // acest lucru este permis acum
            
            Console.ReadLine();
        }
    }
}

Caz în care arată exact ca o clasă și nu mai sunt imuabile, deoarece am specificat manual un setter pe proprietăți. Diferența comparativ cu o clasă este că putem folosi în continuare egalitatea implementată automat.

Trebuie remarcat faptul că imuabilitatea, chiar și pe înregistrări triviale, se aplică doar pe tipuri de date primitive, simple. Dacă una dintre proprietățile înregistrării este o instanță a unei alte clase, membrii acelei clase sunt în continuare modificabili:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System;
 
namespace BunaLume
{
    public class Baterie
    {
        public int Incarcare { get; set; }
    }
    public record Telefon(string Model, Baterie Baterie);
    public class Program
    {
        static void Main(string[] args)
        {
            Telefon telefon = new Telefon("Nokia", new Baterie() { Incarcare = 50 });
            telefon.Model = "Blackberry"; // acest lucru NU este permis, proprietatea Model este imuabila
            telefon.Baterie = new Baterie(); // acest lucru NU este permis, proprietatea Baterie este imuabila
            telefon.Baterie.Incarcare = 20; // acest lucru ESTE permis, membrii proprietatii Baterie nu sunt imuabili
            
            Console.ReadLine();
        }
    }
}

Înregistrările suportă moștenirea, despre care vom afla în viitor. Regulile de împachetare și despachetare tot se aplică, la fel ca în cazul claselor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System;
 
namespace BunaLume
{
    public record Persoana(string Nume, int Varsta);
    public record Angajat(string Nume, int Varsta, decimal Salariu) : Persoana(Nume, Varsta);
    public class Program
    {
        static void Main(string[] args)
        {
            Persoana persoana = new Angajat("Ion", 50, 2000);
            
            bool estePersoana = persoana is Persoana; // adevarat
            bool esteAngajat = persoana is Angajat; // tot adevarat
            
            Console.ReadLine();
        }
    }
}

Spre deosebire de moștenire, înregistrările pot fi marcate ca sigilate (sealed), ceea ce înseamnă că nu pot declara subtipuri (nu pot fi moștenite):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;
 
namespace BunaLume
{
    public sealed record Persoana(string Nume, int Varsta);
    public record Angajat(string Nume, int Varsta, decimal Salariu) : Persoana(Nume, Varsta); // nu se va compila
    public class Program
    {
        static void Main(string[] args)
        {
            Console.ReadLine();
        }
    }
}

Deși nu am explicat încă acest concept, înregistrările pot fi și marcate ca abstracte, caz în care nu pot fi instanțiate în mod direct. Ele trebuie implementate de tipuri concrete, care mai apoi pot fi instanțiate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System;
 
namespace BunaLume
{
    public abstract record Persoana(string Nume, int Varsta);
    public record Angajat(int Varsta, decimal Salariu) : Persoana("Ion", Varsta);
    public class Program
    {
        static void Main(string[] args)
        {
            Persoana oPersoana = new Persoana("Ion", 50); // nepermis, e abstracta
            Persoana altaPersoana = new Angajat(50, 2000); // permis
            
            bool estePersoana = altaPersoana is Persoana; // adevarat
            bool esteAngajat = altaPersoana is Angajat; // tot adevarat
            
            Console.ReadLine();
        }
    }
}

În cele din urmă, când ar trebui să folosiți înregistrările? Potrivit însuși Microsoft, "Pentru înregistrări, utilizarea fundamentală este stocarea datelor. Pentru clase, utilizarea intenționată ar trebui să fie definirea responsabilităților." Cu alte cuvinte, atunci când aveți obiecte stupide, care doar rețin date, și mai ales când fluxul acelor date este unidirecțional (cum ar fi obținerea datelor dintr-o bază de date, stocarea lor în înregistrari și neschimbarea lor ulterioară, ci doar livrarea acestora către părțile programului care au nevoie de ele) iar datele ar trebui să fie imuabile, folosiți înregistrări, pe cât posibil. Când aveți nevoie de logică și funcționalitate, folosiți clase.