record și imutabilitate în C# (.NET)

OOP modern și programare asincronă C# 9.0+ (record struct în C# 10+)

Definește într-o singură linie modele de date imutabile cu egalitate bazată pe valoare, afișare automată și clonare nedistructivă (with).

Sintaxă C#
public record Produs(int Id, string Nume, decimal Pret); | var p2 = p1 with { Pret = 99m };

Ce face record și imutabilitate

Tipurile record (introduse în C# 9 pentru tipuri referință record class și extinse în C# 10 cu readonly record struct pentru tipuri valoare) sunt concepute special pentru modelarea datelor imutabile: obiecte DTO pentru API-uri REST, evenimente, mesaje din cozi sau rezultate ale interogărilor. O declarație pozițională de o singură linie precum public record Coordonata(double Lat, double Lon); generează automat proprietăți { get; init; }, constructor, deconstructor și metode de comparare.

Spre deosebire de o clasă obișnuită (unde a == b verifică dacă ambele variabile indică aceeași adresă de memorie pe heap), două instanțe de record sunt considerate egale (a == b returnează true și au același GetHashCode()) dacă toate proprietățile lor au valori egale (egalitate bazată pe valoare / structurală). Pentru a actualiza o proprietate fără a modifica obiectul existent, expresia with creează o copie nouă doar cu proprietățile specificate modificate.

Parametri, metode și opțiuni

Parametru / Metodă Descriere
public record Nume(T1 Prop1, T2 Prop2); Declară un record pozițional imutabil cu proprietăți init, egalitate pe valoare și ToString() generat automat.
instanta with { Prop = valNoua } Mutație nedistructivă: creează o nouă instanță copiind toate proprietățile existente, cu excepția celor suprascrise în acolade.
a == b / a.Equals(b) Compară valoarea tuturor proprietăților (egalitate structurală), nu referința din memorie.
var (p1, p2) = instanta; Deconstruiește automat un record pozițional într-un tuplu de variabile locale.
public readonly record struct Nume(...) Definește un record ca tip valoare imutabil pe stivă (fără alocare pe heap, C# 10+).

Exemple

Exemplul 1: Egalitate bazată pe valoare, ToString() automat și expresia with

Observă cum două instanțe diferite cu aceleași date sunt egale la compararea cu == și cum with creează o versiune actualizată fără a altera originalul.

var abonament1 = new Abonament("Pro", 49.90m, UtilizatoriMax: 5);
var abonament2 = new Abonament("Pro", 49.90m, UtilizatoriMax: 5);

Console.WriteLine($"Sunt egale valoric (==): {abonament1 == abonament2}");
Console.WriteLine($"Aceeași referință în memorie: {ReferenceEquals(abonament1, abonament2)}");

// Creăm o variantă cu reducere folosind 'with' (abonament1 rămâne neschimbat!):
var abonamentPromo = abonament1 with { PretLunar = 39.90m };

Console.WriteLine($"Original: {abonament1}");
Console.WriteLine($"Promo:    {abonamentPromo}");

public record Abonament(string Plan, decimal PretLunar, int UtilizatoriMax);

Ieșire în consolă

Sunt egale valoric (==): True
Aceeași referință în memorie: False
Original: Abonament { Plan = Pro, PretLunar = 49.90, UtilizatoriMax = 5 }
Promo:    Abonament { Plan = Pro, PretLunar = 39.90, UtilizatoriMax = 5 }

Exemplul 2: Deconstrucția unui record și adăugarea de membri calculați

Un record poate avea corp cu acolade în care poți defini proprietăți calculate, metode sau validări suplimentare.

var dreptunghi = new Dreptunghi(Latime: 12.5, Inaltime: 4.0);

// Deconstrucție pozițională automată:
var (lat, inalt) = dreptunghi;
Console.WriteLine($"Dimensiuni: {lat} x {inalt} -> Arie: {dreptunghi.Arie}, Perimetru: {dreptunghi.Perimetru}");

public record Dreptunghi(double Latime, double Inaltime)
{
    public double Arie => Latime * Inaltime;
    public double Perimetru => 2 * (Latime + Inaltime);
}

Ieșire în consolă

Dimensiuni: 12.5 x 4 -> Arie: 50, Perimetru: 33

Greșeli frecvente

  • Includerea unui tablou T[] sau a unei List<T> într-un record așteptând ca a == b să compare conținutul listelor element cu element — egalitatea generată automat de compilator apelează Equals pe fiecare proprietate, iar pentru List<T> și T[] metoda Equals compară doar referința listei!
  • Folosirea unui record pentru entități EF Core urmărite de Change Tracker — entitățile EF Core se bazează pe identitatea referinței și mutabilitatea proprietăților, deci pentru ele este recomandat class obișnuit, în timp ce record este ideal pentru DTO-uri și Value Objects.

Funcții și concepte înrudite