facebook

Nel precedente articolo, abbiamo analizzato i Record Posizionali che sono la vera innovazione di questa nuova funzionalità di C# 9. Abbiamo scoperto che, dietro le quinte, un record non è altro che una classe con dei comportamenti specifici predefiniti, tra i quali l’immutabilità e l’uguaglianza dei valori.

Sarebbe quindi possibile scrivere un record con una sintassi simile a quella di una classe? Certo che sì! Vediamolo:

    public record CustomRecordPerson
    {
        public string Name { get; set; }
        public string Surname { get; set; }

        public CustomRecordPerson(string name, string surname)
        {
            Name = name;
            Surname = surname;
        }

        public CustomRecordPerson()
        {
        }
    }

Come possiamo vedere, l’unica differenza con la definizione di una classe, consiste nell’utilizzo della keyword record al posto di quella class.

Possiamo quindi creare un’istanza del nostro record tramite il costruttore:

var customRecordPerson = new CustomRecordPerson(name: "Francesco", surname:"Vas"); 

Oppure tramite un inizializzatore di oggetti, il quale vi ricordo, possiamo utilizzare solo se abbiamo a disposizione anche un costruttore senza parametri:

var customRecordPerson2 = new CustomRecordPerson 
{ 
    Name = "Francesco", 
    Surname = "Vas" 
}; 

Proviamo ora a modificare una delle proprietà del nostro record:

customRecordPerson.Name = "Adolfo"; 

Sappiamo che le proprietà di un record sono immutabili e, per qualcuno potrebbe quindi essere una sorpresa che il compilatore non ci segnali un errore. Nonostante questa supposizione, possiamo modificare il valore della proprietà senza problemi!

Non dobbiamo infatti pensare erroneamente che un record sia una struttura dati sempre immutabile. Le proprietà passate come argomento nel caso dei record posizionali, sono sempre immutabili.

Come abbiamo visto infatti, queste vengono impostate di default come proprietà di sola inizializzazione. La stessa cosa occorre fare nel nostro caso per ottenere l’immutabilità:

public record CustomRecordPerson  
{ 
    public string Name { get; init; } 
    public string Surname { get; init; } 

    //... 
} 

Al posto del classico metodo accessore di tipo setter, abbiamo bisogno di utilizzare il metodo accessore setter di tipo init-only per le proprietà che vogliamo rendere immutabili.

Abbiamo già parlato della keyword init e sappiamo che ci permette di specificare che il valore di quella proprietà può essere settato solo quando l’oggetto viene inizializzato, ma non può essere modificato successivamente.

Approfondiamo ancor di più dicendo che questa nuova funzionalità ci permette di ottenere un backing-field di sola lettura per la proprietà, che il compilatore genera per noi dietro le quinte:

[CompilerGenerated] 
[DebuggerBrowsable(DebuggerBrowsableState.Never)] 
private readonly string <Name>k__BackingField; 

Quindi, in pratica, utilizzare la keyword init per una proprietà di una classe o di un record, equivale a scrivere:

public class Person 
{ 
    private readonly string _name; 

    public Person(string name) 
    { 
        Name = name; 
    } 

    public string Name 
    { 
        get => _name; 
        init => _name = value; 
    } 
} 

Se provassimo a sostituire la keyword init con quella set, il compilatore ci segnalerebbe un errore:

Questo perché i metodi accessori di tipo init-only sono abilitati a modificare i campi read-only.

Ecco spiegato tutto il funzionamento che c’è dietro l’immutabilità introdotta in C# 9!

A prescindere dall’utilizzo o meno dell’immutabilità e quindi della keyword init, ovviamente anche un record scritto con questa sintassi alternativa ci fornisce alcune delle funzionalità che abbiamo già visto per i record posizionali. Prendiamo quindi in considerazione la seguente definizione di un record, nella quale abbiamo specificato solo due proprietà:

public record CustomRecordPerson  
{ 
    public string Name { get; init; } 
    public string Surname { get; init; } 
} 

Ed analizziamo il codice IL che viene generato:

public class CustomRecordPerson : IEquatable<CustomRecordPerson> 
{ 
    protected virtual Type EqualityContract 
    { 
        [System.Runtime.CompilerServices.NullableContext(1)] 
        [CompilerGenerated] 
        get 
        { 
            return typeof(CustomRecordPerson); 
        } 
    } 
 
    public string Name { get; init; } 
 
    public string Surname { get; init; } 
 
    public override string ToString() 
    { 
        StringBuilder stringBuilder = new StringBuilder(); 
        stringBuilder.Append("CustomRecordPerson"); 
        stringBuilder.Append(" { "); 
        if (PrintMembers(stringBuilder)) 
        { 
            stringBuilder.Append(" "); 
        } 
        stringBuilder.Append("}"); 
        return stringBuilder.ToString(); 
    } 
 
    protected virtual bool PrintMembers(StringBuilder builder) 
    { 
        builder.Append("Name"); 
        builder.Append(" = "); 
        builder.Append((object?)Name); 
        builder.Append(", "); 
        builder.Append("Surname"); 
        builder.Append(" = "); 
        builder.Append((object?)Surname); 
        return true; 
    } 
 
    [System.Runtime.CompilerServices.NullableContext(2)] 
    public static bool operator !=(CustomRecordPerson? r1, CustomRecordPerson? r2) 
    { 
        return !(r1 == r2); 
    } 
 
    [System.Runtime.CompilerServices.NullableContext(2)] 
    public static bool operator ==(CustomRecordPerson? r1, CustomRecordPerson? r2) 
    { 
        return (object)r1 == r2 || (r1?.Equals(r2) ?? false); 
    } 
 
    public override int GetHashCode() 
    { 
        return (EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295 + 
        EqualityComparer<string>.Default.GetHashCode(Name)) * -1521134295 + 
        EqualityComparer<string>.Default.GetHashCode(Surname); 
    } 
 
    public override bool Equals(object? obj) 
    { 
        return Equals(obj as CustomRecordPerson); 
    } 
 
    public virtual bool Equals(CustomRecordPerson? other) 
    { 
         return (object)other != null && EqualityContract == other!.EqualityContract &&  
         EqualityComparer<string>.Default.Equals(Name, other!.Name) && 
         EqualityComparer<string>.Default.Equals(Surname, other!.Surname); 
     } 
 
    public virtual CustomRecordPerson <Clone>$() 
    { 
        return new CustomRecordPerson(this); 
    } 
 
    protected CustomRecordPerson(CustomRecordPerson original) 
    { 
        Name = original.Name; 
        Surname = original.Surname; 
    } 
 
    public CustomRecordPerson() 
    { 
    } 
} 

Non ci soffermeremo ad analizzare tutte le funzionalità, che abbiamo già discusso nel precedente articolo sui record posizionali . Ci limiteremo ad analizzare le differenze tra le funzionalità offerte in modo automatico dai due diversi modi di scrivere un record.

Come vediamo in questo caso non otteniamo in automatico il costruttore parametrizzato e quindi proprio come per una normale classe ci è stato fornito il costruttore di default. Abbiamo ancora tutti i metodi e gli override a supporto dell’uguaglianza strutturale e l’override del metodo ToString() per la rappresentazione testuale del tipo e dei valori delle proprietà del record.

Manca invece il decostruttore.

Per entrambe le sintassi sono presenti un costruttore protected e un metodo virtual Clone() dei quali non abbiamo parlato precedentemente, ma su di loro si basa il funzionamento della mutazione non distruttiva implementata dai record, ossia il funzionamento della keyword with.

A prescindere dalla sintassi utilizzata, i record supportano l’ereditarietà. Un record infatti può ereditare da un altro record e può essere anche di tipo abstract

Ma un record non può ereditare da una classe e una classe non può ereditare da un record

Inoltre possiamo utilizzare i vincoli generici e anche se non esiste alcun vincolo generico specifico per i record, i record soddisfano il vincolo class

Questo vuol dire che potremmo utilizzare ad esempio, i vicoli generici in questi modi: 

public record GenericRecord<T> where T : class 
{ 
} 
public class GenericClass<T> where T : CustomRecordPerson 
{ 
} 

Con questo terminiamo l’approfondimento sui record.

Abbiamo già parlato dei possibili campi di utilizzo dei record, abbiamo imparato che in realtà non sono altro che delle classi con dei comportamenti predefiniti e che esistono due tipi di sintassi per poterli scrivere che ci forniscono più o meno le stesse funzionalità.

Onestamente, a meno di particolari esigenze in termini di personalizzazione, ritengo che la sintassi dei record posizionali sia molto più utile, perché ci permette un evidente risparmio di quantità di codice scritto e quindi di tempo di sviluppo.

Quindi quando è consigliabile utilizzare un record al posto di una classe?

Principalmente, se vogliamo definire un modello che dipenda dall’uguaglianza dei valori e se vogliamo definire un tipo di riferimento per il quale gli oggetti non siano modificabili.

Spero abbiate trovato l’argomento interessante, al prossimo articolo!

Stay Tuned!