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:
1234567891011121314151617using 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 schimbateConsole.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.