CodeGym /Corsi /C# SELF /Differenze tra record, class e struct

Differenze tra record, class e struct

C# SELF
Livello 19, Lezione 4
Disponibile

1. Perché sono nati i record?

In C# (e in .NET in generale) per tanto tempo i mattoncini base erano classi (class) e struct (struct). Ma ognuno di loro è incompleto per certi task. Le classi sono reference type, mutabili e il confronto è per riferimento (tranne rari casi). Le struct sono value type (si copiano quando le passi), di default si confrontano byte a byte e quasi sempre sono mutabili (almeno finché non sono arrivate le readonly struct).

Ma se volevi una semplice e naturale rappresentazione dei dati, facile da confrontare per valore, clonare al volo e senza preoccuparti che qualcuno ti cambi l’oggetto da qualche parte — dovevi inventarti mille workaround o usare librerie tipo ValueTuple o anche System.Tuple. Ma nessuna di queste era davvero bella e chiara come avresti voluto.

Ecco perché in C# 9 è arrivato record — un tipo di dato che unisce dichiarazione compatta, sicurezza dell’immutabilità e comportamento orientato al confronto per valore.

2. Tutti e quattro i tipi su una sola pagina

class struct record record struct
Categoria Reference type Value type Reference type Value type
Mutabilità Di default — sì Di default — sì Di default — no (init) Di default — no (init)
Confronto Per riferimento (==) Per valore (campi) Per valore (fields/properties) Per valore (fields)
Clonazione Solo a mano Solo a mano Supporto built-in (with) Supporto built-in (with)
Ereditarietà Sì No Sì No
Immutabilità Devi implementarla Devi implementarla Facilissimo da fare Facilissimo da fare
Sintassi La più lunga Corta La più corta (posizionale) Abbastanza corta
Uso nelle collezioni Per riferimento Copie Per riferimento Copie

Schema visivo

+----------------+     +----------------+     +--------------------+
|    class       |     |    struct      |     |      record        |
+----------------+     +----------------+     +--------------------+
| Reference Type |     | Value Type     |     | Reference Type     |
| Mutable        |     | Mutable        |     | Immutable (init)   |
| == : Reference |     | == : By Fields |     | == : By Value      |
+----------------+     +----------------+     +--------------------+

3. Il succo delle differenze tra record, class e struct

Comportamento in memoria: riferimento o valore?

  • class e record sono reference type. Quando li passi a una funzione, si copia solo il riferimento all’oggetto.
  • struct e record struct sono value type. Si copiano sempre byte a byte (a meno che non li passi esplicitamente per riferimento).

class PointClass { public int X; public int Y; }
struct PointStruct { public int X; public int Y; }
record PointRecord(int X, int Y);
record struct PointRecordStruct(int X, int Y);

void Demo()
{
    var pc = new PointClass { X = 1, Y = 2 };
    var ps = new PointStruct { X = 1, Y = 2 };
    var pr = new PointRecord(1, 2);
    var prs = new PointRecordStruct(1, 2);

    ChangeY(pc);    // pc.Y cambierà!
    ChangeY(ps);    // ps.Y non cambierà — è una copia!
    ChangeY(pr);    // pr.Y cambierà!
    ChangeY(prs);   // prs.Y non cambierà — è una copia!
}

void ChangeY(dynamic p) { p.Y = 99; }

Se ti sembra che dynamic qui sia magia — sì, è solo per esempio, così puoi vedere che con le struct il valore non cambia, mentre con class o record (reference) sì.

Confronto: come capire se due oggetti sono uguali?

  • class: si confrontano per riferimento (== — true solo se è lo stesso oggetto in memoria), a meno che non fai override di Equals.
  • struct: si confrontano per valore di tutti i campi (di default).
  • record: si confrontano per valore di tutti i campi/proprietà dichiarati nel costruttore primario.
class Foo { public int A; public int B; }
record Bar(int A, int B);

var foo1 = new Foo { A = 42, B = 1 };
var foo2 = new Foo { A = 42, B = 1 };
var bar1 = new Bar(42, 1);
var bar2 = new Bar(42, 1);

Console.WriteLine(foo1 == foo2); // False! Oggetti diversi
Console.WriteLine(bar1 == bar2); // True! I valori coincidono

Curiosità:
Con record struct è ancora più figo: il confronto è "per valore", come le struct normali, ma con la sintassi e le feature dei record.

4. Immutabilità: chi garantisce cosa?

Confrontiamo la sicurezza degli oggetti:

  • class: Di default facilmente mutabile, a meno che non rendi tutti i campi readonly.
  • struct: Stessa cosa, ma puoi dichiarare readonly struct, così né campi né proprietà si possono cambiare.
  • record: Di solito i campi sono dichiarati con init, cioè li puoi settare solo nel costruttore o nell’inizializzatore (with). Comodo per passare dati in modo sicuro.
  • record struct: Idem, puoi fare readonly record struct e avere immutabilità dei value type con i bonus dei record.

record Person(string Name, int Age);

var p1 = new Person("Aleksej", 23);
// p1.Age = 24; // Errore! Solo init

var p2 = p1 with { Age = 24 }; // Funziona! Crea una copia con i nuovi dati

Quando hai un progetto grosso e decine di entity, record ti evitano un sacco di bug tipo "qualcuno ha cambiato un campo da qualche parte — e tutto si è rotto".

5. Sintassi: come si dichiarano e come non confondersi


// class
public class Product
{
    public int Id { get; init; }
    public string Name { get; init; }
}

// struct
public struct Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

// record
public record Product(int Id, string Name);

// record struct
public record struct Point(int X, int Y);
Confronto della sintassi di dichiarazione dei vari tipi

Come vedi, la dichiarazione più corta è quella del tipo record. Ottieni costruttore, deconstruct, uguaglianza, ToString() e molto altro "out of the box".

Sintassi Cosa ottieni gratis Si può ereditare?
class quasi niente Sì
struct quasi niente No
record ToString, Equals, Deconstruct, with Sì
record struct ToString, Equals, Deconstruct, with No

6. Clonazione e operatore with

Solo record e record struct hanno l’operatore speciale with, che ti permette di copiare un oggetto cambiando solo alcune proprietà senza sbatti.


record User(string Name, int Age);

var user1 = new User("Irina", 28);
var user2 = user1 with { Age = 29 }; // user2.Name == "Irina", user2.Age == 29

Con una classe (class) devi implementare il metodo di copia a mano, e se ti dimentichi di copiare un campo — ciao bug.

7. Ereditarietà: chi eredita da chi

  • class: Supporta ereditarietà standard (gerarchie di classi, metodi virtuali, astrazioni ecc).
  • struct: Non supporta (può solo implementare interfacce).
  • record: Supporta ereditarietà, ma con alcune limitazioni (ad esempio, solo tra record, non tra record e class).
  • record struct: Non supporta, come le struct normali.
record Animal(string Name);
record Dog(string Name, string Breed) : Animal(Name); // Ok!

class Vehicle { }
class Car : Vehicle { } // Ok!

// struct non può ereditare da struct

Più dettagli sull’ereditarietà al prossimo livello :P

8. Dove si usano questi tipi nella pratica

  • class: Oggetti grandi con tanto comportamento, ciclo di vita lungo, stato mutabile, gerarchie (tipo logica di business, componenti UI).
  • struct: Oggetti piccoli-value, dove conta la copia super veloce, niente GC e overhead minimo (tipo coordinate, colori, somme — roba che cloni al volo).
  • record: DTO (Data Transfer Object), oggetti-parametro, parametri di configurazione, stati immutabili, risultati di calcoli che vuoi confrontare per contenuto.
  • record struct: Value type dove vuoi immutabilità e comportamento da record, ma senza allocazioni inutili sull’heap.

9. Errori tipici e trappole

A volte i dev pensano che record sia solo "un sostituto della classe". Non è così! Se scrivi un oggetto che deve cambiare stato durante la vita — usa class.
Se vuoi confrontare oggetti per riferimento (tipo nel pattern "singleton" o quando il ciclo di vita è critico) — usa class.
Se fai un oggetto-value che deve comportarsi come un numero o un punto su una griglia — struct o record struct.
Se lavori con oggetti immutabili, facilmente confrontabili, che passano spesso tra layer dell’app, stanno in collezioni, vengono loggati e serializzati — la tua scelta è record.

Ricorda anche: se dichiari un record-class con proprietà di sola lettura, ma ti dimentichi dei campi annidati — i campi annidati possono comunque essere cambiati se sono mutabili.


record Student(string Name, List<int> Grades);

var s1 = new Student("Anton", new List<int>() {5,5,5});
var s2 = s1 with { };

s2.Grades.Add(2); // Entrambi gli oggetti sono "maledetti" dal 2! s1.Grades == s2.Grades
2
Compito
C# SELF, livello 19, lezione 4
Bloccato
Creazione di un record e confronto di oggetti
Creazione di un record e confronto di oggetti
2
Compito
C# SELF, livello 19, lezione 4
Bloccato
Utilizzo dell'operatore `with` per scrivere dati
Utilizzo dell'operatore `with` per scrivere dati
1
Sondaggio/quiz
Problema con DTO, livello 19, lezione 4
Non disponibile
Problema con DTO
DTO, record e with
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION