Condividi tramite


Archivio

Nota

Questo articolo è una specifica delle funzionalità. La specifica funge da documento di progettazione per la funzionalità. Include le modifiche specifiche proposte, insieme alle informazioni necessarie durante la progettazione e lo sviluppo della funzionalità. Questi articoli vengono pubblicati fino a quando le modifiche specifiche proposte non vengono completate e incorporate nella specifica ECMA corrente.

Potrebbero verificarsi alcune discrepanze tra la specifica di funzionalità e l'implementazione completata. Tali differenze vengono acquisite nelle note language design meeting (LDM) pertinenti.

Puoi trovare maggiori informazioni sul processo di adozione delle specifiche delle funzionalità nello standard del linguaggio C# nell'articolo sulle specifiche di .

Questa proposta segue la specifica per la funzionalità dei record di C# 9, come concordato dal team di progettazione del linguaggio C#.

La sintassi per un record è la seguente:

record_declaration
    : attributes? class_modifier* 'partial'? 'record' identifier type_parameter_list?
      parameter_list? record_base? type_parameter_constraints_clause* record_body
    ;

record_base
    : ':' class_type argument_list?
    | ':' interface_type_list
    | ':' class_type argument_list? ',' interface_type_list
    ;

record_body
    : '{' class_member_declaration* '}' ';'?
    | ';'
    ;

I tipi di record sono tipi di riferimento, simili a una dichiarazione di classe. È un errore per un record fornire un record_baseargument_list se il record_declaration non contiene un parameter_list. Al massimo una dichiarazione di tipo parziale di un record parziale può fornire un parameter_list.

I parametri dei record non possono usare ref, out o modificatori di this (ma sono consentiti in e params).

Eredità

I record non possono ereditare dalle classi, a meno che la classe non sia objecte le classi non possano ereditare dai record. I record possono ereditare da altri record.

Membri di un tipo di record

Oltre ai membri dichiarati nel corpo del record, un tipo di record ha membri sintetizzati aggiuntivi. I membri vengono sintetizzati a meno che non venga dichiarato un membro con una firma "corrispondente" nel corpo del record o non venga ereditato un membro concreto non virtuale accessibile con una firma "corrispondente". Un membro corrispondente impedisce al compilatore di generare tale membro, non altri membri sintetizzati. Due membri sono considerati equivalenti se hanno la stessa firma oppure sono considerati "nascosti" in uno scenario di ereditarietà. È un errore che un membro di un record venga chiamato "Clone". È un errore che un campo di istanza di un record abbia un tipo puntatore di livello superiore. È consentito un tipo di puntatore annidato, ad esempio una matrice di puntatori.

I membri sintetizzati sono i seguenti:

Membri per l'uguaglianza

Se il record è derivato da object, il tipo di record include una proprietà sintetizzata di sola lettura equivalente a una proprietà dichiarata come mostrato di seguito:

Type EqualityContract { get; }

La proprietà è private se il tipo di record è sealed. In caso contrario, la proprietà è virtual e protected. La proprietà può essere dichiarata in modo esplicito. Si tratta di un errore se la dichiarazione esplicita non corrisponde alla firma o all'accessibilità prevista oppure se la dichiarazione esplicita non consente di eseguirne l'override in un tipo derivato e il tipo di record non è sealed.

Se il tipo di record è derivato da un tipo di record di base Base, il tipo di record include una proprietà di sola lettura sintetizzata, equivalente a una proprietà dichiarata come segue:

protected override Type EqualityContract { get; }

La proprietà può essere dichiarata in modo esplicito. Si tratta di un errore se la dichiarazione esplicita non corrisponde alla firma o all'accessibilità prevista oppure se la dichiarazione esplicita non consente di eseguirne l'override in un tipo derivato e il tipo di record non è sealed. Si tratta di un errore se la proprietà sintetizzata o dichiarata in modo esplicito non sovrascrive una proprietà con questa firma nel tipo di record Base (ad esempio, se la proprietà non è presente nell'Base, oppure se è sigillata, non virtuale, ecc.). La proprietà sintetizzata restituisce typeof(R) dove R è il tipo di record.

Il tipo di record implementa System.IEquatable<R> e include un overload fortemente tipizzato e sintetizzato di Equals(R? other), dove R è il tipo di record. Il metodo è publice il metodo è virtual a meno che il tipo di record non sia sealed. Il metodo può essere dichiarato in modo esplicito. Si tratta di un errore se la dichiarazione esplicita non corrisponde alla firma o all'accessibilità prevista oppure la dichiarazione esplicita non consente di eseguirne l'override in un tipo derivato e il tipo di record non è sealed.

Se Equals(R? other) è definito dall'utente (non sintetizzato) ma non è GetHashCode, viene generato un avviso.

public virtual bool Equals(R? other);

Il Equals(R?) sintetizzato restituisce true se e solo se ciascuno dei seguenti è true:

  • other non è null,
  • Per ogni campo dell'istanza fieldN nel tipo di record che non è ereditato, il valore di System.Collections.Generic.EqualityComparer<TN>.Default.Equals(fieldN, other.fieldN) dove TN è il tipo di campo e
  • Se è presente un tipo di record di base, il valore di base.Equals(other) (una chiamata non virtuale a public virtual bool Equals(Base? other)); in caso contrario, il valore di EqualityContract == other.EqualityContract.

Il tipo di record include operatori sintetizzati, == e !=, che sono equivalenti agli operatori dichiarati come segue:

public static bool operator==(R? left, R? right)
    => (object)left == right || (left?.Equals(right) ?? false);
public static bool operator!=(R? left, R? right)
    => !(left == right);

Il metodo Equals chiamato dall'operatore == è il metodo Equals(R? other) specificato in precedenza. L'operatore != delega all'operatore ==. Si tratta di un errore se gli operatori vengono dichiarati in modo esplicito.

Se il tipo di record è derivato da un tipo di record di base Base, il tipo di record include una sovrascrittura sintetizzata equivalente a un metodo dichiarato come segue:

public sealed override bool Equals(Base? other);

Si tratta di un errore se l'override viene dichiarato in modo esplicito. Si tratta di un errore se il metodo non esegue l'override di un metodo con la stessa firma nel tipo di record Base (ad esempio, se il metodo non è presente nel Base, o sigillato, o non virtuale, ecc.). L'override sintetizzato restituisce Equals((object?)other).

Il tipo di record include un override sintetizzato equivalente a un metodo dichiarato come segue:

public override bool Equals(object? obj);

Si tratta di un errore se l'override viene dichiarato in modo esplicito. Si tratta di un errore se il metodo non esegue l'override di object.Equals(object? obj) (ad esempio, a causa dell'oscuramento nei tipi di base intermedi, eccetera). L'override sintetizzato restituisce Equals(other as R) dove R è il tipo di record.

Il tipo di record include un override sintetizzato equivalente a un metodo dichiarato come segue:

public override int GetHashCode();

Il metodo può essere dichiarato in modo esplicito. Si verifica un errore se la dichiarazione esplicita non permette di eseguire l'override in un tipo derivato e il tipo di record non è sealed. Si tratta di un errore se un metodo, sia sintetizzato che dichiarato esplicitamente, non esegue l'override di object.GetHashCode() (ad esempio, a causa dell'ombreggiatura nei tipi di base intermedi, ecc.).

Viene riportato un avviso se uno tra Equals(R?) e GetHashCode() è dichiarato in modo esplicito, ma l'altro metodo non è esplicito.

L'override sintetizzato di GetHashCode() restituisce un int risultato della combinazione dei valori seguenti:

  • Per ogni campo dell'istanza fieldN nel tipo di record che non è ereditato, il valore di System.Collections.Generic.EqualityComparer<TN>.Default.GetHashCode(fieldN) dove TN è il tipo di campo e
  • Se esiste un tipo di record di base, si usa il valore di base.GetHashCode(); altrimenti, si usa il valore di System.Collections.Generic.EqualityComparer<System.Type>.Default.GetHashCode(EqualityContract).

Si considerino ad esempio i tipi di record seguenti:

record R1(T1 P1);
record R2(T1 P1, T2 P2) : R1(P1);
record R3(T1 P1, T2 P2, T3 P3) : R2(P1, P2);

Per questi tipi di record, i membri di uguaglianza sintetizzati saranno simili ai seguenti:

class R1 : IEquatable<R1>
{
    public T1 P1 { get; init; }
    protected virtual Type EqualityContract => typeof(R1);
    public override bool Equals(object? obj) => Equals(obj as R1);
    public virtual bool Equals(R1? other)
    {
        return !(other is null) &&
            EqualityContract == other.EqualityContract &&
            EqualityComparer<T1>.Default.Equals(P1, other.P1);
    }
    public static bool operator==(R1? left, R1? right)
        => (object)left == right || (left?.Equals(right) ?? false);
    public static bool operator!=(R1? left, R1? right)
        => !(left == right);
    public override int GetHashCode()
    {
        return HashCode.Combine(EqualityComparer<Type>.Default.GetHashCode(EqualityContract),
            EqualityComparer<T1>.Default.GetHashCode(P1));
    }
}

class R2 : R1, IEquatable<R2>
{
    public T2 P2 { get; init; }
    protected override Type EqualityContract => typeof(R2);
    public override bool Equals(object? obj) => Equals(obj as R2);
    public sealed override bool Equals(R1? other) => Equals((object?)other);
    public virtual bool Equals(R2? other)
    {
        return base.Equals((R1?)other) &&
            EqualityComparer<T2>.Default.Equals(P2, other.P2);
    }
    public static bool operator==(R2? left, R2? right)
        => (object)left == right || (left?.Equals(right) ?? false);
    public static bool operator!=(R2? left, R2? right)
        => !(left == right);
    public override int GetHashCode()
    {
        return HashCode.Combine(base.GetHashCode(),
            EqualityComparer<T2>.Default.GetHashCode(P2));
    }
}

class R3 : R2, IEquatable<R3>
{
    public T3 P3 { get; init; }
    protected override Type EqualityContract => typeof(R3);
    public override bool Equals(object? obj) => Equals(obj as R3);
    public sealed override bool Equals(R2? other) => Equals((object?)other);
    public virtual bool Equals(R3? other)
    {
        return base.Equals((R2?)other) &&
            EqualityComparer<T3>.Default.Equals(P3, other.P3);
    }
    public static bool operator==(R3? left, R3? right)
        => (object)left == right || (left?.Equals(right) ?? false);
    public static bool operator!=(R3? left, R3? right)
        => !(left == right);
    public override int GetHashCode()
    {
        return HashCode.Combine(base.GetHashCode(),
            EqualityComparer<T3>.Default.GetHashCode(P3));
    }
}

Copiare e clonare i membri

Un tipo di record contiene due membri coinvolti nella copia:

  • Costruttore che accetta un unico argomento di tipo record. Viene definito "costruttore di copia".
  • Metodo sintetizzato "clone" di un'istanza pubblica senza parametri con un nome riservato dal compilatore

Lo scopo del costruttore di copia è copiare lo stato dal parametro alla nuova istanza creata. Questo costruttore non esegue nessun inizializzatore di campo/proprietà dell'istanza presente nella dichiarazione di record. Se il costruttore non è dichiarato in modo esplicito, un costruttore verrà sintetizzato dal compilatore. Se il record è sealed, il costruttore sarà privato; altrimenti, sarà protetto. Un costruttore di copia dichiarato in modo esplicito deve essere pubblico o protetto, a meno che il record non sia sigillato. La prima cosa che il costruttore deve fare è chiamare un costruttore di copia della base o un costruttore di oggetti senza parametro se il record eredita dall'oggetto . Viene segnalato un errore se un costruttore di copia definito dall'utente usa un inizializzatore di costruttore implicito o esplicito che non soddisfa questo requisito. Dopo che è stato richiamato un costruttore di copia di base, un costruttore di copia sintetizzato copia i valori di tutti i campi dell'istanza dichiarati implicitamente o esplicitamente all'interno del tipo di record. La sola presenza, esplicita o implicita, di un costruttore di copia non impedisce l'aggiunta automatica di un costruttore di istanza predefinito.

Se nel record di base è presente un metodo "clone" virtuale, il metodo "clone" sintetizzato lo sovrascrive e il tipo di ritorno del metodo è il tipo contenitore corrente. Se il metodo clone del record di base è sealed, viene generato un errore. Se un metodo virtuale "clone" non è presente nel record di base, il tipo restituito del metodo clone è il tipo contenitore e il metodo diventa virtuale, salvo che il record non sia sigillato o astratto. Se il record contenitore è astratto, anche il metodo clone sintetizzato è astratto. Se il metodo "clone" non è astratto, restituisce il risultato di una chiamata a un costruttore di copia.

Membri da stampare: metodi PrintMembers e ToString

Se il record è derivato da object, il record include un metodo sintetizzato equivalente a un metodo dichiarato come segue:

bool PrintMembers(System.Text.StringBuilder builder);

Il metodo è private se il tipo di record è sealed. In caso contrario, il metodo è virtual e protected.

Metodo:

  1. chiama il metodo System.Runtime.CompilerServices.RuntimeHelpers.EnsureSufficientExecutionStack() se il metodo è presente e il record dispone di membri stampabili.
  2. per ogni membro stampabile del record (campo pubblico non statico e proprietà leggibili), aggiunge il nome del membro seguito da " = " seguito dal valore del membro separato con ", "
  3. restituisce true se il record dispone di membri stampabili.

Per un membro con un tipo valore, il relativo valore verrà convertito in una rappresentazione di stringa usando il metodo più efficiente disponibile per la piattaforma di destinazione. Attualmente significa chiamare ToString prima di passare a StringBuilder.Append.

Se il tipo di record è derivato da un record di base Base, il record include un override sintetizzato equivalente a un metodo dichiarato come segue:

protected override bool PrintMembers(StringBuilder builder);

Se il record non dispone di membri stampabili, il metodo chiama il metodo di base PrintMembers con un argomento (il relativo parametro builder) e restituisce il risultato.

In caso contrario, il metodo :

  1. chiama il metodo PrintMembers di base con un argomento (il suo parametro builder),
  2. se il metodo PrintMembers ha restituito true, aggiungere ", " al generatore,
  3. per ognuno dei membri stampabili del record, aggiunge il nome del membro seguito da " = " seguito dal valore del membro: this.member (o this.member.ToString() per i tipi valore), separati con ", ",
  4. ritorna true.

Il metodo PrintMembers può essere dichiarato in modo esplicito. Si tratta di un errore se la dichiarazione esplicita non corrisponde alla firma o all'accessibilità prevista oppure se la dichiarazione esplicita non consente di eseguirne l'override in un tipo derivato e il tipo di record non è sealed.

Il record include un metodo sintetizzato equivalente a un metodo dichiarato come segue:

public override string ToString();

Il metodo può essere dichiarato in modo esplicito. Si tratta di un errore se la dichiarazione esplicita non corrisponde alla firma o all'accessibilità prevista oppure se la dichiarazione esplicita non consente di eseguirne l'override in un tipo derivato e il tipo di record non è sealed. Si tratta di un errore se un metodo, sia sintetizzato che dichiarato esplicitamente, non esegue l'override di object.ToString() (ad esempio, a causa dell'ombreggiatura nei tipi di base intermedi, ecc.).

Metodo sintetizzato:

  1. crea un'istanza di StringBuilder,
  2. aggiunge il nome del record al builder, seguito da " { "
  3. richiama il metodo PrintMembers del record assegnando al generatore il generatore, seguito da " " se ha restituito true,
  4. aggiunge "}"
  5. restituisce il contenuto del generatore con builder.ToString().

Si considerino ad esempio i tipi di record seguenti:

record R1(T1 P1);
record R2(T1 P1, T2 P2, T3 P3) : R1(P1);

Per questi tipi di record, i membri di stampa sintetizzati sarebbero qualcosa come:

class R1 : IEquatable<R1>
{
    public T1 P1 { get; init; }
    
    protected virtual bool PrintMembers(StringBuilder builder)
    {
        builder.Append(nameof(P1));
        builder.Append(" = ");
        builder.Append(this.P1); // or builder.Append(this.P1.ToString()); if T1 is a value type
        
        return true;
    }
    
    public override string ToString()
    {
        var builder = new StringBuilder();
        builder.Append(nameof(R1));
        builder.Append(" { ");

        if (PrintMembers(builder))
            builder.Append(" ");

        builder.Append("}");
        return builder.ToString();
    }
}

class R2 : R1, IEquatable<R2>
{
    public T2 P2 { get; init; }
    public T3 P3 { get; init; }
    
    protected override bool PrintMembers(StringBuilder builder)
    {
        if (base.PrintMembers(builder))
            builder.Append(", ");
            
        builder.Append(nameof(P2));
        builder.Append(" = ");
        builder.Append(this.P2); // or builder.Append(this.P2); if T2 is a value type
        
        builder.Append(", ");
        
        builder.Append(nameof(P3));
        builder.Append(" = ");
        builder.Append(this.P3); // or builder.Append(this.P3); if T3 is a value type
        
        return true;
    }
    
    public override string ToString()
    {
        var builder = new StringBuilder();
        builder.Append(nameof(R2));
        builder.Append(" { ");

        if (PrintMembers(builder))
            builder.Append(" ");

        builder.Append("}");
        return builder.ToString();
    }
}

Membri record posizionale

Oltre ai membri precedenti, i record con un elenco di parametri ("record posizionali") sintetizzano membri aggiuntivi con le stesse condizioni dei membri precedenti.

Costruttore primario

Un tipo di record ha un costruttore pubblico la cui firma corrisponde ai parametri di valore della dichiarazione di tipo. Questo metodo viene chiamato costruttore primario per il tipo e fa sì che il costruttore della classe predefinito dichiarato in modo implicito, se presente, venga eliminato. È un errore avere un costruttore primario e un costruttore con la stessa firma già presente nella classe.

In fase di esecuzione il costruttore primario

  1. esegue gli inizializzatori di istanza visualizzati nel corpo della classe

  2. richiama il costruttore della classe base con gli argomenti forniti nella clausola record_base, se presente

Se un record ha un costruttore primario, qualsiasi costruttore definito dall'utente, ad eccezione del "costruttore di copia", deve avere un inizializzatore del costruttore this esplicito.

I parametri del costruttore primario e i membri del record sono inclusi nell'ambito della argument_list della clausola record_base e all'interno di inizializzatori di campi o proprietà dell'istanza. I membri dell'istanza causerebbero un errore in queste posizioni (analogamente a come i membri dell'istanza si trovano nell'ambito degli inizializzatori di costruttori regolari oggi, ma il loro utilizzo comporta un errore), mentre i parametri del costruttore primario sarebbero nell'ambito e utilizzabili e oscurerebbero i membri. I membri statici sarebbero anche utilizzabili, in modo analogo a come le chiamate di base e gli inizializzatori funzionano oggi nei costruttori ordinari.

Se un parametro del costruttore primario non viene letto, viene generato un avviso.

Le variabili di espressione dichiarate nel argument_list sono nello scope del argument_list. Si applicano le stesse regole di "shadowing" come all'interno di un elenco di argomenti di un inizializzatore di costruttore ordinario.

Proprietà

Per ogni parametro di record di una dichiarazione di tipo record esiste una proprietà pubblica corrispondente, il cui nome e tipo vengono ricavati dalla dichiarazione del parametro di valore.

Per stabilire un record:

  • Viene creata una proprietà automatica pubblica get e init (vedere la specifica separata dell'accessor init). Una proprietà abstract ereditata con tipo corrispondente viene sostituita. Si tratta di un errore se la proprietà ereditata non dispone di public sottoponibili a override e di get e init come funzioni di accesso. Si tratta di un errore se la proprietà ereditata è nascosta.
    La proprietà automatica viene inizializzata sul valore del parametro del costruttore primario corrispondente. Gli attributi possono essere applicati alla proprietà automatica sintetizzata e al relativo campo di supporto utilizzando come destinazioni property: o field: per gli attributi applicati sintatticamente al parametro di record corrispondente.

Decostruire

Un record posizionale con almeno un parametro sintetizza un metodo di istanza public void-returning denominato Deconstruct con una dichiarazione di parametro out per ogni parametro della dichiarazione del costruttore primario. Ogni parametro del metodo Deconstruct ha lo stesso tipo del parametro corrispondente della dichiarazione del costruttore primario. Il corpo del metodo assegna a ogni parametro del metodo Deconstruct il valore della proprietà dell'istanza con lo stesso nome. Il metodo può essere dichiarato in modo esplicito. Si tratta di un errore se la dichiarazione esplicita non corrisponde alla firma o all'accessibilità prevista o è statica.

L'esempio seguente mostra un record posizionale R con il relativo metodo di Deconstruct sintetizzato dal compilatore, insieme al relativo utilizzo:

public record R(int P1, string P2 = "xyz")
{
    public void Deconstruct(out int P1, out string P2)
    {
        P1 = this.P1;
        P2 = this.P2;
    }
}

class Program
{
    static void Main()
    {
        R r = new R(12);
        (int p1, string p2) = r;
        Console.WriteLine($"p1: {p1}, p2: {p2}");
    }
}

espressione with

Un'espressione with è una nuova espressione usando la sintassi seguente.

with_expression
    : switch_expression
    | switch_expression 'with' '{' member_initializer_list? '}'
    ;

member_initializer_list
    : member_initializer (',' member_initializer)*
    ;

member_initializer
    : identifier '=' expression
    ;

Un'espressione with non è consentita come dichiarazione.

Un'espressione with consente la "mutazione non distruttiva", progettata per produrre una copia dell'espressione ricevitore con modifiche nelle assegnazioni nel member_initializer_list.

Un'espressione valida di with ha un ricevitore con un tipo non void. Il tipo di ricevitore deve essere un record.

Nella parte destra dell'espressione with si trova un member_initializer_list con una sequenza di assegnazioni per l'identificatore , che deve essere un campo o una proprietà di istanza accessibile del tipo del destinatario.

In primo luogo, viene richiamato il metodo "clone" del ricevitore (specificato in precedenza) e il relativo risultato viene convertito nel tipo del ricevitore. Ogni member_initializer viene quindi elaborato allo stesso modo di un'assegnazione a un campo o all'accesso alle proprietà del risultato della conversione. Le assegnazioni vengono elaborate in ordine lessicale.