Condividi tramite


Struct a predefinizione automatica

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.

Altre informazioni sul processo per l'adozione di speclet di funzionalità nello standard del linguaggio C# sono disponibili nell'articolo sulle specifiche di .

https://github.com/dotnet/csharplang/issues/5737

Sommario

Questa funzionalità fa sì che nei costruttori di struct vengano identificati i campi che non sono stati assegnati in modo esplicito dall'utente prima di restituirli o utilizzarli, e vengano quindi inizializzati implicitamente a default, evitando così errori di assegnazione definiti.

Motivazione

Questa proposta viene sollevata come possibile mitigazione per i problemi di usabilità rilevati in dotnet/csharplang#5552 e dotnet/csharplang#5635, oltre che per affrontare #5563 (tutti i campi devono essere sicuramente assegnati, ma field non è accessibile all'interno del costruttore).


A partire da C# 1.0, i costruttori di struct devono assegnare this in modo definitivo come se fosse un parametro out.

public struct S
{
    public int x, y;
    public S() // error: Fields 'S.x' and 'S.y' must be fully assigned before control is returned to the caller
    {
    }
}

Ciò presenta problemi quando i setter vengono definiti manualmente nelle proprietà semi-automatiche, poiché il compilatore non può considerare l'assegnazione della proprietà come equivalente all'assegnazione del campo sottostante.

public struct S
{
    public int X { get => field; set => field = value; }
    public S() // error: struct fields aren't fully assigned. But caller can only assign 'this.field' by assigning 'this'.
    {
    }
}

Si presuppone che l'introduzione di restrizioni più dettagliate per i setter, ad esempio uno schema in cui il setter non accetta ref this, ma piuttosto accetta out field come parametro, sarà troppo di nicchia e incompleto per alcuni casi d'uso.

Una delle tensioni fondamentali con cui stiamo lottando è che, quando le proprietà degli struct hanno setter implementati manualmente, spesso gli utenti devono ripetere più volte le assegnazioni o replicare la loro logica.

struct S
{
    private int _x;
    public int X
    {
        get => _x;
        set => _x = value >= 0 ? value : throw new ArgumentOutOfRangeException();
    }

    // Solution 1: assign some value in the constructor before "really" assigning through the property setter.
    public S(int x)
    {
        _x = default;
        X = x;
    }

    // Solution 2: assign the field once in the constructor, repeating the implementation of the setter.
    public S(int x)
    {
        _x = x >= 0 ? x : throw new ArgumentOutOfRangeException();
    }
}

Discussione precedente

Un piccolo gruppo ha esaminato questo problema e ha considerato alcune possibili soluzioni:

  1. Richiedere agli utenti di assegnare il this = default quando le proprietà semi-automatiche hanno i setter implementati manualmente. Siamo d'accordo che questa è la soluzione sbagliata perché elimina i valori impostati negli inizializzatori di campo.
  2. Inizializzare in modo implicito tutti i campi di supporto delle proprietà auto/semi-auto.
    • Questo risolve il problema dei "setter di proprietà semi-automatici" e colloca chiaramente i campi dichiarati esplicitamente sotto regole diverse: "non inizializzare i miei campi in modo implicito, ma inizializza in modo implicito le mie proprietà automatiche."
  3. Fornire un modo per assegnare il campo sottostante di una proprietà semi-automatica e richiedere agli utenti di assegnarlo.
    • Questo potrebbe essere complesso rispetto a (2). Una proprietà automatica dovrebbe essere "automatica" e forse questo include l'inizializzazione "automatica" del campo. Potrebbe generare confusione quando il campo sottostante viene assegnato da un'assegnazione alla proprietà e quando viene chiamato il setter della proprietà.

Abbiamo anche ricevuto feedback dagli utenti che vogliono, ad esempio, includere alcuni inizializzatori di campo in structs senza dover esplicitamente assegnare ogni cosa. È possibile risolvere questo problema, nonché la "proprietà semi-automatica con setter implementato manualmente" contemporaneamente.

struct MagnitudeVector3d
{
    double X, Y, Z;
    double Magnitude = 1;
    public MagnitudeVector3d() // error: must assign 'X', 'Y', 'Z' before returning
    {
    }
}

Regolazione dell'assegnazione definita

Anziché eseguire un'analisi di assegnazione definita per fornire errori per i campi non assegnati in this, lo facciamo per determinare quali campi devono essere inizializzati in modo implicito. Tale inizializzazione viene inserita all'inizio del costruttore.

struct S
{
    int x, y;

    // Example 1
    public S()
    {
        // ok. Compiler inserts an assignment of `this = default`.
    }

    // Example 2
    public S()
    {
        // ok. Compiler inserts an assignment of `y = default`.
        x = 1;
    }

    // Example 3
    public S()
    {
        // valid since C# 1.0. Compiler inserts no implicit assignments.
        x = 1;
        y = 2;
    }

    // Example 4
    public S(bool b)
    {
        // ok. Compiler inserts assignment of `this = default`.
        if (b)
            x = 1;
        else
            y = 2;
    }

    // Example 5
    void M() { }
    public S(bool b)
    {
        // ok. Compiler inserts assignment of `y = default`.
        x = 1;
        if (b)
            M();

        y = 2;
    }
}

Negli esempi (4) e (5), il codegen risultante a volte ha "assegnazioni doppie" di campi. Ciò è in genere corretto, ma per gli utenti interessati da tali assegnazioni doppie, è possibile generare ciò che è stato usato per definire la diagnostica degli errori di assegnazione come disabilitato per impostazione predefinita diagnostica degli avvisi.

struct S
{
    int x;
    public S() // warning: 'S.x' is implicitly initialized to 'default'.
    {
    }
}

Gli utenti che impostano la gravità di questa diagnostica su "errore" opteranno per il comportamento pre-C# 11. Tali utenti sono essenzialmente "esclusi" dalle proprietà semi-automatiche di programmazione con setter implementati manualmente.

struct S
{
    public int X
    {
        get => field;
        set => field = field < value ? value : field;
    }

    public S() // error: backing field of 'S.X' is implicitly initialized to 'default'.
    {
        X = 1;
    }
}

A prima vista, questo sembra un "buco" nella funzionalità, ma è effettivamente la cosa giusta da fare. Abilitando la diagnostica, l'utente indica che non vuole che il compilatore inizializzi in modo implicito i campi nel costruttore. In questo caso non è possibile evitare l'inizializzazione implicita, quindi la soluzione consiste nell'usare un modo diverso per inizializzare il campo rispetto a un setter implementato manualmente, ad esempio dichiarando manualmente il campo e assegnandolo o includendo un inizializzatore di campo.

Attualmente, il JIT non elimina le archiviazioni inutili tramite refs, ciò significa che queste inizializzazioni implicite hanno un costo reale. Ma questo potrebbe essere risolvibile. https://github.com/dotnet/runtime/issues/13727

Vale la pena notare che l'inizializzazione di singoli campi invece dell'intera istanza è davvero solo un'ottimizzazione. Il compilatore dovrebbe probabilmente essere libero di implementare qualsiasi euristica desiderata, purché soddisfi l'invariante che i campi che non sono assegnati in modo definitivo a tutti i punti di ritorno o prima di qualsiasi accesso a un membro non-campo di this vengano inizializzati implicitamente.

Ad esempio, se una struct ha 100 campi e solo uno di essi viene inizializzato in modo esplicito, potrebbe essere più opportuno eseguire un initobj sull'intero struct, piuttosto che generare in modo implicito initobj per gli altri 99 campi. Tuttavia, un'implementazione che genera in modo implicito initobj per gli altri 99 campi sarebbe ancora valida.

Modifiche alle specifiche del linguaggio

La sezione seguente dello standard viene modificata:

https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12814-this-access

Se la dichiarazione del costruttore non dispone di inizializzatore del costruttore, la variabile this si comporta esattamente come un parametro out del tipo di struct. In particolare, ciò significa che la variabile deve essere sicuramente assegnata in ogni percorso di esecuzione del costruttore di istanza.

Questa lingua viene modificata in modo da leggere:

Se la dichiarazione del costruttore non dispone di inizializzatore del costruttore, la variabile this si comporta in modo analogo a un parametro out del tipo struct, ad eccezione del fatto che non è un errore quando non vengono soddisfatti i requisiti di assegnazione definiti (§9.4.1). Verranno invece introdotti i comportamenti seguenti:

  1. Quando la variabile this stessa non soddisfa i requisiti, tutte le variabili di istanza non assegnate all'interno di this in tutti i punti in cui i requisiti vengono violati vengono inizializzati in modo implicito al valore predefinito (§9,3) in una fase di inizializzazione prima dell'esecuzione di qualsiasi altro codice nel costruttore.
  2. Quando una variabile di istanza v all'interno di this non soddisfa i requisiti o qualsiasi variabile di istanza a qualsiasi livello di annidamento all'interno di v non soddisfa i requisiti, v viene inizializzata in modo implicito al valore predefinito in una fase di inizializzazione prima dell'esecuzione di qualsiasi altro codice nel costruttore.

Riunioni di design

https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-14.md#definite-assignment-in-structs