Creare tipi di record

I record sono tipi che usano l'uguaglianza basata su valori. C# 10 aggiunge struct di record in modo da poter definire i record come tipi di valore. Due variabili di un tipo di record sono uguali se le definizioni dei tipi di record sono identiche e, se per ogni campo, i valori in entrambi i record sono uguali. Due variabili di un tipo di classe sono uguali se gli oggetti a cui si fa riferimento sono lo stesso tipo di classe e le variabili fanno riferimento allo stesso oggetto. L'uguaglianza basata su valori implica altre funzionalità che probabilmente si desiderano nei tipi di record. Il compilatore genera molti di questi membri quando si dichiara un anziché record un oggetto class. Il compilatore genera gli stessi metodi per record struct i tipi.

Questa esercitazione illustra come:

  • Decidere se si aggiunge il record modificatore a un class tipo.
  • Dichiarare i tipi di record e i tipi di record posizionale.
  • Sostituire i metodi per i metodi generati dal compilatore nei record.

Prerequisiti

Sarà necessario configurare il computer per eseguire .NET 6 o versione successiva, incluso il compilatore C# 10 o versione successiva. Il compilatore C# 10 è disponibile a partire da Visual Studio 2022 o .NET 6 SDK.

Caratteristiche dei record

Per definire un record , dichiarare un tipo con la record parola chiave , modificare una class dichiarazione o struct . Facoltativamente, è possibile omettere la class parola chiave per creare un oggetto record class. Un record segue la semantica di uguaglianza basata su valori. Per applicare la semantica dei valori, il compilatore genera diversi metodi per il tipo di record (sia per i tipi che record struct per record class i tipi):

I record forniscono anche un override di Object.ToString(). Il compilatore sintetizza i metodi per visualizzare i record usando Object.ToString(). Questi membri verranno esaminati durante la scrittura del codice per questa esercitazione. I record supportano with espressioni per abilitare la mutazione non distruttiva dei record.

È anche possibile dichiarare record posizionali usando una sintassi più concisa. Il compilatore sintetizza più metodi quando si dichiarano record posizionali:

  • Costruttore primario i cui parametri corrispondono ai parametri posizionali nella dichiarazione di record.
  • Proprietà pubbliche per ogni parametro di un costruttore primario. Queste proprietà sono solo init per record class tipi e readonly record struct tipi. Per record struct i tipi, sono di lettura/scrittura.
  • Metodo Deconstruct per estrarre le proprietà dal record.

Creare dati sulla temperatura

I dati e le statistiche sono tra gli scenari in cui si vogliono usare i record. Per questa esercitazione si creerà un'applicazione che calcola i giorni di grado per usi diversi. I giorni di grado sono una misura di calore (o mancanza di calore) in un periodo di giorni, settimane o mesi. I giorni di grado tengono traccia e stimano l'utilizzo dell'energia. Più giorni più caldi significa più aria condizionata, e più giorni più freddi significa più utilizzo forno. I giorni di grado aiutano a gestire le popolazioni vegetali e correlare alla crescita delle piante man mano che cambiano le stagioni. I giorni di grado aiutano a tenere traccia delle migrazioni degli animali per le specie che viaggiano per soddisfare il clima.

La formula si basa sulla temperatura media in un determinato giorno e una temperatura di base. Per calcolare i giorni di grado nel tempo, è necessaria la temperatura elevata e bassa ogni giorno per un periodo di tempo.To compute degree days over time, you'll need the high and low temperature each day for a period of time. Per iniziare, creare una nuova applicazione. Creare una nuova applicazione console. Creare un nuovo tipo di record in un nuovo file denominato "DailyTemperature.cs":

public readonly record struct DailyTemperature(double HighTemp, double LowTemp);

Il codice precedente definisce un record posizionale. Il DailyTemperature record è , readonly record structperché non si intende ereditare da esso e deve essere non modificabile. Le HighTemp proprietà e LowTemp sono solo proprietà init, ovvero possono essere impostate nel costruttore o usando un inizializzatore di proprietà. Se si desidera che i parametri posizionali siano di lettura/scrittura, si dichiara un anziché record struct un oggetto readonly record struct. Il DailyTemperature tipo ha anche un costruttore primario con due parametri che corrispondono alle due proprietà. Usare il costruttore primario per inizializzare un DailyTemperature record. Il codice seguente crea e inizializza diversi DailyTemperature record. Il primo usa i parametri denominati per chiarire e HighTempLowTemp. Gli inizializzatori rimanenti usano parametri posizionali per inizializzare HighTemp e LowTemp:

private static DailyTemperature[] data = [
    new DailyTemperature(HighTemp: 57, LowTemp: 30), 
    new DailyTemperature(60, 35),
    new DailyTemperature(63, 33),
    new DailyTemperature(68, 29),
    new DailyTemperature(72, 47),
    new DailyTemperature(75, 55),
    new DailyTemperature(77, 55),
    new DailyTemperature(72, 58),
    new DailyTemperature(70, 47),
    new DailyTemperature(77, 59),
    new DailyTemperature(85, 65),
    new DailyTemperature(87, 65),
    new DailyTemperature(85, 72),
    new DailyTemperature(83, 68),
    new DailyTemperature(77, 65),
    new DailyTemperature(72, 58),
    new DailyTemperature(77, 55),
    new DailyTemperature(76, 53),
    new DailyTemperature(80, 60),
    new DailyTemperature(85, 66) 
];

È possibile aggiungere proprietà o metodi personalizzati ai record, inclusi i record posizionali. Sarà necessario calcolare la temperatura media per ogni giorno. È possibile aggiungere tale proprietà al DailyTemperature record:

public readonly record struct DailyTemperature(double HighTemp, double LowTemp)
{
    public double Mean => (HighTemp + LowTemp) / 2.0;
}

Assicurarsi di poter usare questi dati. Aggiungere il codice seguente al metodo Main:

foreach (var item in data)
    Console.WriteLine(item);

Eseguire l'applicazione e verrà visualizzato un output simile al seguente (diverse righe rimosse per lo spazio):

DailyTemperature { HighTemp = 57, LowTemp = 30, Mean = 43.5 }
DailyTemperature { HighTemp = 60, LowTemp = 35, Mean = 47.5 }


DailyTemperature { HighTemp = 80, LowTemp = 60, Mean = 70 }
DailyTemperature { HighTemp = 85, LowTemp = 66, Mean = 75.5 }

Il codice precedente mostra l'output dell'override di ToString sintetizzato dal compilatore. Se si preferisce testo diverso, è possibile scrivere la propria versione di ToString che impedisce al compilatore di sintetizzare una versione.

Giorni di grado di calcolo

Per calcolare i giorni di grado, si prende la differenza da una temperatura di base e dalla temperatura media in un determinato giorno. Per misurare il calore nel tempo, si eliminano tutti i giorni in cui la temperatura media è inferiore alla linea di base. Per misurare il freddo nel tempo, si eliminano tutti i giorni in cui la temperatura media è superiore alla baseline. Ad esempio, usa 65F come base per i giorni di riscaldamento e raffreddamento. Questa è la temperatura in cui non è necessario riscaldamento o raffreddamento. Se un giorno ha una temperatura media di 70F, quel giorno è di cinque giorni di raffreddamento e zero giorni di riscaldamento. Viceversa, se la temperatura media è 55F, quel giorno è di 10 giorni di riscaldamento e 0 giorni di raffreddamento.

È possibile esprimere queste formule come una piccola gerarchia di tipi di record: un tipo di giorno di grado astratto e due tipi di cemento per i giorni del grado di riscaldamento e giorni di raffreddamento. Questi tipi possono anche essere record posizionali. Accettano una temperatura di base e una sequenza di record di temperatura giornalieri come argomenti per il costruttore primario:

public abstract record DegreeDays(double BaseTemperature, IEnumerable<DailyTemperature> TempRecords);

public sealed record HeatingDegreeDays(double BaseTemperature, IEnumerable<DailyTemperature> TempRecords)
    : DegreeDays(BaseTemperature, TempRecords)
{
    public double DegreeDays => TempRecords.Where(s => s.Mean < BaseTemperature).Sum(s => BaseTemperature - s.Mean);
}

public sealed record CoolingDegreeDays(double BaseTemperature, IEnumerable<DailyTemperature> TempRecords)
    : DegreeDays(BaseTemperature, TempRecords)
{
    public double DegreeDays => TempRecords.Where(s => s.Mean > BaseTemperature).Sum(s => s.Mean - BaseTemperature);
}

Il record astratto DegreeDays è la classe base condivisa per i HeatingDegreeDays record e CoolingDegreeDays . Le dichiarazioni del costruttore primario nei record derivati mostrano come gestire l'inizializzazione dei record di base. Il record derivato dichiara i parametri per tutti i parametri nel costruttore primario del record di base. Il record di base dichiara e inizializza tali proprietà. Il record derivato non li nasconde, ma crea e inizializza solo le proprietà per i parametri che non sono dichiarati nel record di base. In questo esempio i record derivati non aggiungono nuovi parametri del costruttore primario. Testare il codice aggiungendo il codice seguente al Main metodo :

var heatingDegreeDays = new HeatingDegreeDays(65, data);
Console.WriteLine(heatingDegreeDays);

var coolingDegreeDays = new CoolingDegreeDays(65, data);
Console.WriteLine(coolingDegreeDays);

Verrà visualizzato un output simile al seguente:

HeatingDegreeDays { BaseTemperature = 65, TempRecords = record_types.DailyTemperature[], DegreeDays = 85 }
CoolingDegreeDays { BaseTemperature = 65, TempRecords = record_types.DailyTemperature[], DegreeDays = 71.5 }

Definire metodi sintetizzati dal compilatore

Il codice calcola il numero corretto di giorni di riscaldamento e raffreddamento in quel periodo di tempo. In questo esempio viene illustrato il motivo per cui è possibile sostituire alcuni dei metodi sintetizzati per i record. È possibile dichiarare la propria versione di uno dei metodi sintetizzati dal compilatore in un tipo di record, ad eccezione del metodo clone. Il metodo clone ha un nome generato dal compilatore e non è possibile fornire un'implementazione diversa. Questi metodi sintetizzati includono un costruttore di copia, i membri dell'interfaccia System.IEquatable<T> , i test di uguaglianza e disuguaglianza e GetHashCode(). A questo scopo, si esegue la sintesi PrintMembersdi . È anche possibile dichiarare il proprio ToString, ma PrintMembers offre un'opzione migliore per gli scenari di ereditarietà. Per fornire una versione personalizzata di un metodo sintetizzato, la firma deve corrispondere al metodo sintetizzato.

L'elemento TempRecords nell'output della console non è utile. Visualizza il tipo, ma nient'altro. È possibile modificare questo comportamento fornendo la propria implementazione del metodo sintetizzato PrintMembers . La firma dipende dai modificatori applicati alla record dichiarazione:

  • Se un tipo di record è sealedo record struct, la firma è private bool PrintMembers(StringBuilder builder);
  • Se un tipo di record non sealed è e deriva da object (ovvero non dichiara un record di base), la firma è protected virtual bool PrintMembers(StringBuilder builder);
  • Se un tipo di record non sealed è e deriva da un altro record, la firma è protected override bool PrintMembers(StringBuilder builder);

Queste regole sono più semplici da comprendere attraverso la comprensione dello scopo di PrintMembers. PrintMembers aggiunge informazioni su ogni proprietà di un tipo di record a una stringa. Il contratto richiede record di base per aggiungere i membri alla visualizzazione e presuppone che i membri derivati aggiungeranno i relativi membri. Ogni tipo di record sintetizza un ToString override simile all'esempio seguente per HeatingDegreeDays:

public override string ToString()
{
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.Append("HeatingDegreeDays");
    stringBuilder.Append(" { ");
    if (PrintMembers(stringBuilder))
    {
        stringBuilder.Append(" ");
    }
    stringBuilder.Append("}");
    return stringBuilder.ToString();
}

Si dichiara un PrintMembers metodo nel DegreeDays record che non stampa il tipo della raccolta:

protected virtual bool PrintMembers(StringBuilder stringBuilder)
{
    stringBuilder.Append($"BaseTemperature = {BaseTemperature}");
    return true;
}

La firma dichiara un virtual protected metodo in modo che corrisponda alla versione del compilatore. Non preoccuparti se le funzioni di accesso non sono errate; la lingua applica la firma corretta. Se si dimenticano i modificatori corretti per qualsiasi metodo sintetizzato, il compilatore genera avvisi o errori che consentono di ottenere la firma corretta.

In C# 10 e versioni successive è possibile dichiarare il ToString metodo come sealed in un tipo di record. Ciò impedisce ai record derivati di fornire una nuova implementazione. I record derivati conterranno comunque l'override PrintMembers . Se non si vuole visualizzare il tipo di runtime del record, si potrebbe bloccare ToString . Nell'esempio precedente si perderanno le informazioni sulla posizione in cui il record misurava i giorni di riscaldamento o raffreddamento.

Mutazione non distruttiva

I membri sintetizzati in una classe di record posizionale non modificano lo stato del record. L'obiettivo è che è possibile creare più facilmente record non modificabili. Tenere presente che si dichiara un oggetto readonly record struct per creare uno struct di record non modificabile. Esaminare di nuovo le dichiarazioni precedenti per HeatingDegreeDays e CoolingDegreeDays. I membri aggiunti eseguono calcoli sui valori per il record, ma non modificano lo stato. I record posizionali semplificano la creazione di tipi riferimento non modificabili.

La creazione di tipi riferimento non modificabili significa che si vuole usare una mutazione non distruttiva. Si creano nuove istanze di record simili alle istanze di record esistenti usando with espressioni. Queste espressioni sono una costruzione di copia con assegnazioni aggiuntive che modificano la copia. Il risultato è una nuova istanza di record in cui ogni proprietà è stata copiata dal record esistente e, facoltativamente, modificata. Il record originale rimane invariato.

Aggiungere un paio di funzionalità al programma che illustrano with le espressioni. Creare prima di tutto un nuovo record per calcolare i giorni di gradi in crescita usando gli stessi dati. I giorni di gradi in crescita usano in genere 41F come baseline e misurano le temperature al di sopra della baseline. Per usare gli stessi dati, è possibile creare un nuovo record simile a coolingDegreeDays, ma con una temperatura di base diversa:

// Growing degree days measure warming to determine plant growing rates
var growingDegreeDays = coolingDegreeDays with { BaseTemperature = 41 };
Console.WriteLine(growingDegreeDays);

È possibile confrontare il numero di gradi calcolati con i numeri generati con una temperatura di base superiore. Tenere presente che i record sono tipi riferimento e queste copie sono copie superficiali. La matrice per i dati non viene copiata, ma entrambi i record fanno riferimento agli stessi dati. Questo è un vantaggio in un altro scenario. Per i giorni in crescita, è utile tenere traccia del totale per i cinque giorni precedenti. È possibile creare nuovi record con dati di origine diversi usando with espressioni. Il codice seguente compila una raccolta di questi accumuli, quindi visualizza i valori:

// showing moving accumulation of 5 days using range syntax
List<CoolingDegreeDays> movingAccumulation = new();
int rangeSize = (data.Length > 5) ? 5 : data.Length;
for (int start = 0; start < data.Length - rangeSize; start++)
{
    var fiveDayTotal = growingDegreeDays with { TempRecords = data[start..(start + rangeSize)] };
    movingAccumulation.Add(fiveDayTotal);
}
Console.WriteLine();
Console.WriteLine("Total degree days in the last five days");
foreach(var item in movingAccumulation)
{
    Console.WriteLine(item);
}

È anche possibile usare with espressioni per creare copie di record. Non specificare alcuna proprietà tra le parentesi graffe per l'espressione with . Ciò significa creare una copia e non modificare le proprietà:

var growingDegreeDaysCopy = growingDegreeDays with { };

Eseguire l'applicazione completata per visualizzare i risultati.

Riepilogo

In questa esercitazione sono stati illustrati diversi aspetti dei record. I record forniscono una sintassi concisa per i tipi in cui l'uso fondamentale archivia i dati. Per le classi orientate agli oggetti, l'uso fondamentale è la definizione delle responsabilità. Questa esercitazione è incentrata sui record posizionali, in cui è possibile usare una sintassi concisa per dichiarare le proprietà per un record. Il compilatore sintetizza diversi membri del record per la copia e il confronto dei record. È possibile aggiungere qualsiasi altro membro necessario per i tipi di record. È possibile creare tipi di record non modificabili sapendo che nessuno dei membri generati dal compilatore cambierebbe lo stato. E with le espressioni semplificano il supporto di mutazioni non distruttive.

I record aggiungono un altro modo per definire i tipi. Le definizioni vengono usate class per creare gerarchie orientate agli oggetti che si concentrano sulle responsabilità e sul comportamento degli oggetti. È possibile creare struct tipi per strutture di dati che archiviano i dati e sono sufficientemente piccoli da copiare in modo efficiente. I tipi vengono creati record quando si desidera eseguire l'uguaglianza e il confronto basati su valori, non si vogliono copiare i valori e si vogliono usare le variabili di riferimento. I tipi vengono creati record struct quando si desiderano le funzionalità dei record per un tipo sufficientemente piccolo da copiare in modo efficiente.

Per altre informazioni sui record, vedere l'articolo di riferimento sul linguaggio C# per il tipo di record e la specifica del tipo di record proposta e la specifica dello struct di record.