Condividi tramite


parola chiave field nelle proprietà

Problema del campione: https://github.com/dotnet/csharplang/issues/8635

Sommario

Estendere tutte le proprietà per consentire loro di fare riferimento a un campo sottostante generato automaticamente usando la nuova parola chiave contestuale field. Le proprietà possono ora contenere anche una funzione di accesso senza un corpo insieme a una funzione di accesso con un corpo.

Motivazione

Le proprietà automatiche consentono solo l'impostazione diretta o il recupero del campo sottostante, assegnando un controllo solo inserendo i modificatori di accesso nelle funzioni di accesso. In alcuni casi è necessario avere un controllo aggiuntivo su ciò che accade in uno o entrambi gli accessori, ma questo presenta agli utenti l'onere di dichiarare un campo sottostante. Il nome del campo sottostante deve quindi essere sincronizzato con la proprietà e il campo sottostante ha come ambito l'intera classe, che può comportare il bypass accidentale delle funzioni di accesso dall'interno della classe .

Esistono diversi scenari comuni. All'interno del getter è presente l'inizializzazione pigra e i valori predefiniti quando la proprietà non è mai stata impostata. All'interno del setter viene applicato un vincolo per garantire la validità di un valore o il rilevamento e la propagazione di aggiornamenti, ad esempio generando l'evento INotifyPropertyChanged.PropertyChanged.

In questi casi è sempre necessario creare un campo di istanza e scrivere manualmente l'intera proprietà. Ciò non solo aggiunge una notevole quantità di codice, ma espone anche il campo sottostante al resto dell'ambito della classe, mentre spesso è preferibile che sia disponibile solo ai corpi degli accessori.

Glossario

  • Proprietà auto: abbreviazione di "proprietà implementata automaticamente" (§15.7.4). Le funzioni di accesso in una proprietà automatica non hanno corpo. L'implementazione e l'archiviazione di backup sono entrambi forniti dal compilatore. Le proprietà automatiche hanno { get; }, { get; set; }o { get; init; }.

  • funzione di accesso automatica: breve per "funzione di accesso implementata automaticamente". Si tratta di una funzione di accesso senza corpo. L'implementazione e l'archiviazione di backup sono entrambi forniti dal compilatore. get;, set; e init; sono funzioni di accesso automatico.

  • accessore completo: si tratta di un accessore che ha un corpo. L'implementazione non è fornita dal compilatore, anche se la memoria di supporto può ancora esserlo (come nell'esempio set => field = value;).

  • proprietà supportata dal campo: si tratta di una proprietà che usa la parola chiave field all'interno di un corpo di accesso o di una proprietà automatica.

  • campo di supporto: si tratta della variabile indicata dalla parola chiave field nei metodi di accesso di una proprietà, che viene anche letta o scritta in modo implicito nei metodi di accesso implementati automaticamente (get;, set;o init;).

Progettazione dettagliata

Per le proprietà con una funzione di accesso init, tutti gli elementi che si applicano di seguito a set si applicano invece alla funzione di accesso init.

Esistono due modifiche alla sintassi:

  1. È disponibile una nuova parola chiave contestuale, field, che può essere usata all'interno dei corpi delle funzioni di accesso alle proprietà per accedere a un campo sottostante per la dichiarazione di proprietà (decisione LDM).

  2. Le proprietà possono ora combinare e associare le funzioni di accesso automatico alle funzioni di accesso complete (decisione LDM). "Proprietà automatica" continuerà a indicare una proprietà le cui funzioni di accesso non hanno corpi. Nessuno degli esempi seguenti verrà considerato proprietà automatiche.

Esempi:

{ get; set => Set(ref field, value); }
{ get => field ?? parent.AmbientValue; set; }

Entrambe le funzioni di accesso possono essere funzioni di accesso complete con una o entrambe le funzioni di accesso che usano field:

{ get => field; set => field = value; }
{ get => field; set => throw new InvalidOperationException(); }
{ get => overriddenValue; set => field = value; }
{
    get;
    set
    {
        if (field == value) return;
        field = value;
        OnXyzChanged();
    }
}

Le proprietà con corpo di espressione e quelle con solo un accessor get possono usare anche field:

public string LazilyComputed => field ??= Compute();
public string LazilyComputed { get => field ??= Compute(); }

Anche le proprietà di sola impostazione possono usare field:

{
    set
    {
        if (field == value) return;
        field = value;
        OnXyzChanged(new XyzEventArgs(value));
    }
}

Modifiche dirompenti

L'esistenza della parola chiave contestuale field all'interno dei corpi degli accessor delle proprietà è una modifica che potrebbe avere un grande impatto.

Poiché field è una parola chiave e non un identificatore, può essere "oscurato" solo da un identificatore utilizzando la normale procedura di escape della parola chiave: @field. Tutti gli identificatori denominati field dichiarati all'interno dei corpi delle funzioni di accesso alle proprietà possono evitare interruzioni durante l'aggiornamento da versioni C# precedenti a 14 aggiungendo il @iniziale .

Se una variabile denominata field viene dichiarata in un metodo di accesso alla proprietà, viene segnalato un errore.

Nella versione 14 o successiva del linguaggio, viene segnalato un avviso se un'espressione primaria field fa riferimento al campo di supporto, ma avrebbe fatto riferimento a un simbolo diverso in una versione precedente del linguaggio.

Attributi mirati al campo

Come per le proprietà automatiche, qualsiasi proprietà che usa un campo sottostante in uno dei suoi metodi di accesso potrà usare attributi mirati al campo.

[field: Xyz]
public string Name => field ??= Compute();

[field: Xyz]
public string Name { get => field; set => field = value; }

Un attributo mirato al campo rimarrà non valido a meno che un accessor non utilizzi un campo di supporto.

// ❌ Error, will not compile
[field: Xyz]
public string Name => Compute();

Inizializzatori di proprietà

Le proprietà con inizializzatori possono usare field. Il campo di supporto viene inizializzato direttamente, anziché essere chiamato il setter (decisione LDM).

La chiamata di un setter per un inizializzatore non è un'opzione; gli inizializzatori vengono elaborati prima di chiamare i costruttori di base ed è illegale chiamare qualsiasi metodo di istanza prima che venga chiamato il costruttore di base. Questo è importante anche per l'inizializzazione predefinita e l'assegnazione definita delle struct.

In questo modo si ottiene un controllo flessibile sull'inizializzazione. Se vuoi inizializzare senza chiamare il setter, usa un inizializzatore di proprietà. Se si desidera inizializzare chiamando il setter, è necessario assegnare alla proprietà un valore iniziale nel costruttore.

Ecco un esempio di dove questo è utile. Crediamo che la parola chiave field troverà ampio impiego con i modelli di visualizzazione grazie alla soluzione elegante che offre per il pattern INotifyPropertyChanged. È probabile che i setter di proprietà del modello di visualizzazione siano collegati ai dati dell'interfaccia utente e probabilmente causano il rilevamento delle modifiche o attivano altri comportamenti. Il codice seguente deve inizializzare il valore predefinito di IsActive senza impostare HasPendingChanges su true:

class SomeViewModel
{
    public bool HasPendingChanges { get; private set; }

    public bool IsActive { get; set => Set(ref field, value); } = true;

    private bool Set<T>(ref T location, T value)
    {
        if (RuntimeHelpers.Equals(location, value))
            return false;

        location = value;
        HasPendingChanges = true;
        return true;
    }
}

Questa differenza di comportamento tra un inizializzatore di proprietà e l'assegnazione dal costruttore può essere vista anche con le proprietà automatiche virtuali nelle versioni precedenti del linguaggio:

using System;

// Nothing is printed; the property initializer is not
// equivalent to `this.IsActive = true`.
_ = new Derived();

class Base
{
    public virtual bool IsActive { get; set; } = true;
}

class Derived : Base
{
    public override bool IsActive
    {
        get => base.IsActive;
        set
        {
            base.IsActive = value;
            Console.WriteLine("This will not be reached");
        }
    }
}

Assegnazione del costruttore

Come per le proprietà automatiche, l'assegnazione nel costruttore invoca il setter (potenzialmente virtuale) se esiste e, se non esiste alcun setter, effettua un ripiego assegnando direttamente al campo di supporto.

class C
{
    public C()
    {
        P1 = 1; // Assigns P1's backing field directly
        P2 = 2; // Assigns P2's backing field directly
        P3 = 3; // Calls P3's setter
        P4 = 4; // Calls P4's setter
    }

    public int P1 => field;
    public int P2 { get => field; }
    public int P4 { get => field; set => field = value; }
    public int P3 { get => field; set; }
}

Assegnazione determinata nelle strutture

Anche se non è possibile fare riferimento ai campi di supporto nel costruttore, i campi indicati dalla parola chiave field sono soggetti all'inizializzazione predefinita e agli avvisi di disabilitazione per impostazione predefinita, alle stesse condizioni degli altri campi struct (decisione LDM 1, decisione LDM 2).

Ad esempio, questa diagnostica è invisibile all'utente per impostazione predefinita.

public struct S
{
    public S()
    {
        // CS9020 The 'this' object is read before all of its fields have been assigned, causing preceding implicit
        // assignments of 'default' to non-explicitly assigned fields.
        _ = P1;
    }

    public int P1 { get => field; }
}
public struct S
{
    public S()
    {
        // CS9020 The 'this' object is read before all of its fields have been assigned, causing preceding implicit
        // assignments of 'default' to non-explicitly assigned fields.
        P2 = 5;
    }

    public int P2 { get => field; set => field = value; }
}

Proprietà che restituiscono riferimenti

Analogamente alle proprietà automatiche, la parola chiave field non sarà disponibile per l'utilizzo nelle proprietà che restituiscono ref. Le proprietà che restituiscono riferimenti non possono avere metodi set e, senza un metodo set, il metodo get e l'inizializzatore di proprietà sarebbero le uniche cose che possono accedere al campo sottostante. Poiché non ci sono casi d'uso per questo, ora non è il momento per iniziare a scrivere le proprietà che restituiscono reference come proprietà automatiche.

Nullabilità

Un principio della funzionalità Tipi di riferimento nullable era comprendere i modelli di codifica idiomatici esistenti in C# e richiedere il minor numero possibile di formalità intorno a tali modelli. La proposta di parole chiave field consente a modelli semplici e idiomatici di affrontare scenari ampiamente richiesti, come le proprietà inizializzate in modo pigro. È importante che i tipi di riferimento nullabili si integrino bene a questi nuovi modelli di codifica.

Obiettivi:

  • È consigliabile garantire un livello ragionevole di sicurezza rispetto ai null per vari modelli di utilizzo della funzionalità keyword field.

  • I modelli che usano la parola chiave field dovrebbero sembrare come se fossero sempre parte del linguaggio. Evitare di costringere l'utente a fare salti mortali per abilitare i Tipi di Riferimento Nullable nel codice perfettamente idiomatico per la funzionalità di parola chiave field.

Uno degli scenari principali sono le proprietà inizializzate pigramente.

public class C
{
    public C() { } // It would be undesirable to warn about 'Prop' being uninitialized here

    string Prop => field ??= GetPropValue();
}

Le regole di nullità seguenti si applicano non solo alle proprietà che usano la parola chiave field, ma anche alle proprietà automatiche esistenti.

Nullabilità del campo di supporto

Per le definizioni dei nuovi termini, vedere glossario.

Il campo di supporto ha lo stesso tipo della proprietà. Tuttavia, l'annotazione annullabile può differire dalla proprietà. Per determinare questa annotazione annullabile, introduciamo il concetto di insensibilità ai null. la resilienza Null in modo intuitivo significa che la funzione di accesso get della proprietà mantiene la sicurezza null anche quando il campo contiene il valore default per il relativo tipo.

Una proprietà supportata da campi viene determinata come resiliente null o meno eseguendo un'analisi speciale nullable della funzione di accesso get.

  • Ai fini di questa analisi, si assume temporaneamente che field abbia annotata nullabilità, ad esempio string?. Ciò fa sì che field abbia uno stato iniziale di forse nullo o forse predefinito nella funzione di accesso get, a seconda del tipo.
  • Se quindi l'analisi nullable del getter non restituisce avvisi nullable, la proprietà viene resiliente null. In caso contrario, non è resiliente ai valori null.
  • Se la proprietà non dispone di una funzione di accesso get, è implicitamente resiliente.
  • Se la funzione di accesso get viene implementata automaticamente, la proprietà non tollera valori null.

La nullabilità del campo di base è determinata nel modo seguente:

  • Se nel campo sono presenti attributi di nullità, ad esempio [field: MaybeNull], AllowNull, NotNullo DisallowNull, l'annotazione nullable del campo corrisponde all'annotazione nullable della proprietà.
    • Questo perché quando l'utente inizia ad applicare attributi di nullabilità al campo, non vogliamo più dedurre nulla, vogliamo solo che la nullabilità sia quella che l'utente ha indicato.
  • Se la proprietà contenente ha inconsapevole o annotata nullabilità, allora il campo sottostante ha la stessa nullabilità della proprietà.
  • Se la proprietà contenitore ha non annotata nullabilità (ad esempio, string o T) o ha l'attributo [NotNull], e la proprietà è a prova di null, il campo sottostante ha annotata nullabilità.
  • Se la proprietà contenitore ha non annotato nullability (ad esempio, string o T) o ha l'attributo [NotNull] e la proprietà è nonresiliente null, il campo sottostante ha non annotato null.

Analisi del costruttore

Attualmente, una proprietà automatica viene trattata in modo molto simile a un campo comune in 'analisi del costruttore nullable. Questo trattamento viene esteso a proprietà supportate da campi, trattando ogni proprietà supportata da campi come proxy al relativo campo sottostante.

Aggiorniamo il seguente linguaggio delle specifiche dal precedente approccio proposto per eseguire questa operazione:

In ogni 'return' esplicito o implicito di un costruttore, mostriamo un avviso per ciascun membro il cui stato del flusso risulti incompatibile con le annotazioni e gli attributi di nullabilità. Se il membro è una proprietà supportata dal campo, per questa verifica viene utilizzata l'annotazione nullable del campo sottostante. In caso contrario, viene utilizzata l'annotazione nullable del membro stesso. Un proxy ragionevole per questo è: se l'assegnazione del membro a se stesso al punto di ritorno produce un avviso di nullità, un avviso di nullità verrà generato al punto di ritorno.

Si noti che si tratta essenzialmente di un'analisi interprocedurale vincolata. Prevediamo che per analizzare un costruttore, sarà necessario eseguire l'analisi dell'associazione e della resilienza al valore nullo su tutti gli accessor di get applicabili nello stesso tipo, che usano la parola chiave contestuale field e hanno una nullabilità non annotata . Si ipotizza che questo non sia eccessivamente costoso perché i corpi getter in genere non sono molto complessi e che l'analisi "con resilienza null" deve essere eseguita una sola volta indipendentemente dal numero di costruttori nel tipo.

Analisi del setter

Per semplicità, si usano i termini "setter" e "set accessor" per fare riferimento a un accessore set o init.

È necessario verificare che i setter delle proprietà con campo di supporto inizializzino effettivamente il campo sottostante.

class C
{
    string Prop
    {
        get => field;

        // getter is not null-resilient, so `field` is not-annotated.
        // We should warn here that `field` may be null when exiting.
        set { }
    }

    public C()
    {
        Prop = "a"; // ok
    }

    public static void Main()
    {
        new C().Prop.ToString(); // NRE at runtime
    }
}

Lo stato iniziale del flusso del campo sottostante nel setter di una proprietà basata su campi viene determinato come segue:

  • Se la proprietà ha un inizializzatore, lo stato del flusso iniziale corrisponde allo stato del flusso della proprietà dopo aver visitato l'inizializzatore.
  • In caso contrario, lo stato del flusso iniziale è uguale allo stato del flusso specificato da field = default;.

In ogni 'return' esplicito o implicito nel setter, viene segnalato un avviso se lo stato del flusso del campo sottostante non è compatibile con le relative annotazioni e attributi di nullità.

Osservazioni

Questa formulazione è intenzionalmente molto simile ai campi standard nei costruttori. Essenzialmente, poiché solo le funzioni di accesso alle proprietà possono effettivamente fare riferimento al campo sottostante, il setter viene considerato come un "mini-costruttore" per il campo sottostante.

Analogamente ai campi ordinari, in genere si sa che la proprietà è stata inizializzata nel costruttore perché è stata impostata, ma non necessariamente. Semplicemente eseguire un'operazione di ritorno all'interno di un ramo dove Prop != null era vero è sufficiente per la nostra analisi del costruttore, poiché comprendiamo che potrebbero essere stati utilizzati meccanismi non monitorati per impostare la proprietà.

Sono state prese in considerazione alternative; vedere la sezione Alternative di Nullability.

nameof

Nei punti in cui field è una parola chiave, nameof(field) non riuscirà a compilare (decisione LDM), ad esempio nameof(nint). Non è come nameof(value), che rappresenta la soluzione da adottare quando i setter delle proprietà lanciano un'eccezione ArgumentException, come accade in alcune librerie di .NET Core. Al contrario, nameof(field) non ha casi d'uso previsti.

Sostituzioni

L'override delle proprietà può utilizzare field. Tali utilizzi di field fanno riferimento al campo sottostante per la proprietà di override, separato dal campo sottostante della proprietà di base, se presente. Non esiste un'ABI per esporre il campo sottostante di una proprietà di base alle classi che sovrascrivono, poiché ciò romperebbe l'incapsulamento.

Analogamente alle proprietà automatiche, le proprietà che usano la parola chiave field e sovrascrivono una proprietà di base devono sovrascrivere tutti gli accessor (decisione LDM).

Cattura

field devono essere acquisiti nelle funzioni locali e nelle espressioni lambda e i riferimenti a field dall'interno di funzioni locali e espressioni lambda sono consentiti anche se non sono presenti altri riferimenti (decisione LDM 1, decisione LDM 2):

public class C
{
    public static int P
    {
        get
        {
            Func<int> f = static () => field;
            return f();
        }
    }
}

Avvisi di utilizzo dei campi

Quando la parola chiave field viene usata in una funzione di accesso, l'analisi esistente del compilatore di campi non assegnati o non letti includerà tale campo.

  • CS0414: viene assegnato il campo sottostante per la proprietà 'Xyz', ma il relativo valore non viene mai usato
  • CS0649: il campo sottostante per la proprietà 'Xyz' non viene mai assegnato e avrà sempre il valore predefinito

Modifiche alle specifiche

Sintassi

Quando si esegue la compilazione con la versione 14 o successiva del linguaggio, field viene considerata una parola chiave quando viene usata come espressione primaria (decisione LDM) nelle posizioni seguenti (decisione LDM):

  • Nei corpi dei metodi di get, sete init funzioni di accesso nelle proprietà ma non negli indicizzatori
  • Negli attributi applicati a tali accessori
  • Nelle espressioni lambda annidate e nelle funzioni locali e nelle espressioni LINQ in tali funzioni di accesso

In tutti gli altri casi, tra cui durante la compilazione con la versione 12 o precedente del linguaggio, field viene considerato un identificatore.

primary_no_array_creation_expression
    : literal
+   | 'field'
    | interpolated_string_expression
    | ...
    ;

Proprietà

§15.7.1Proprietà - Generale

È possibile specificare un property_initializer solo per una proprietà automaticamente implementata euna proprietà con un campo di supporto che verrà generato. Il property_initializer causa l'inizializzazione del campo sottostante di tali proprietà con il valore specificato dall'espressione .

§15.7.4proprietà implementate automaticamente

Una proprietà implementata automaticamente (o proprietà automatica in breve) è una proprietà non astratta, non extern, non di tipo ref con definizioni degli accessori solo punto e virgola. Le proprietà automatiche devono avere un metodo get e facoltativamente possono avere un metodo set.o entrambi:

  1. un accessor con solo un punto e virgola nel corpo
  2. 'utilizzo della parola chiave contestuale field all'interno delle funzioni di accesso o del corpo dell'espressionedella proprietà

Quando una proprietà viene specificata come proprietà implementata automaticamente, un nascosto campo sottostante viene automaticamente disponibile per la proprietà e le funzioni di accesso vengono implementate per leggere e scrivere in tale campo sottostante. Per le proprietà automatiche, ogni accessor solo punto e virgola get viene implementato per leggere da, e ogni accessor solo punto e virgolaset è implementato per scrivere nel relativo campo sottostante.

Il campo sottostante nascosto non è accessibile, può essere letto e scritto solo tramite le funzioni di accesso alle proprietà implementate automaticamente, anche all'interno del tipo contenitore.È possibile fare riferimento direttamente al campo sottostante usando la parola chiave fieldall'interno di tutte le funzioni di accesso e all'interno del corpo dell'espressione di proprietà. Poiché il campo è senza nome, non può essere usato in un'espressionenameof.

Se la proprietà automatica ha nessuna funzione di accesso impostatasolo una funzione di accesso get con punto e virgola, il campo sottostante viene considerato readonly (§15.5.3). Proprio come un campo , è possibile assegnare anche una proprietà automatica di sola lettura (senza una funzione di accesso set o una funzione di accesso init) a nel corpo di un costruttore della classe contenitore. Tale assegnazione viene assegnata direttamente al campo sottostante di sola lettura della proprietà.

Una proprietà automatica non può avere solo un accessore che sia solo un punto e virgola set senza un accessore get.

Una proprietà automatica può facoltativamente includere un property_initializer, che viene applicato direttamente al campo sottostante come variable_initializer (§17.7).

L'esempio seguente:

// No 'field' symbol in scope.
public class Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

equivale alla dichiarazione seguente:

// No 'field' symbol in scope.
public class Point
{
    public int X { get { return field; } set { field = value; } }
    public int Y { get { return field; } set { field = value; } }
}

equivalente a:

// No 'field' symbol in scope.
public class Point
{
    private int __x;
    private int __y;
    public int X { get { return __x; } set { __x = value; } }
    public int Y { get { return __y; } set { __y = value; } }
}

L'esempio seguente:

// No 'field' symbol in scope.
public class LazyInit
{
    public string Value => field ??= ComputeValue();
    private static string ComputeValue() { /*...*/ }
}

equivale alla dichiarazione seguente:

// No 'field' symbol in scope.
public class Point
{
    private string __value;
    public string Value { get { return __value ??= ComputeValue(); } }
    private static string ComputeValue() { /*...*/ }
}

Alternative

Alternative di Nullabilità

Oltre all'approccio di resilienza null descritto nella sezione Nullability, il gruppo di lavoro ha suggerito le alternative seguenti per la considerazione del modello LDM:

Non fare nulla

Qui non potremmo introdurre alcun comportamento speciale. In effetti:

  • Trattare una proprietà supportata da campi allo stesso modo in cui le proprietà automatiche vengono trattate oggi, devono essere inizializzate nel costruttore tranne quando sono contrassegnate come obbligatorie e così via.
  • Nessun trattamento speciale della variabile di campo durante l'analisi delle funzioni di accesso alle proprietà. Si tratta semplicemente di una variabile con lo stesso tipo e la stessa nullabilità della proprietà.

Si noti che questo comporta avvisi indesiderati per gli scenari di "proprietà differita", nel qual caso gli utenti potrebbero dover applicare null! o valori simili per silenziare gli avvisi del costruttore.
Una "subalternativa" che è possibile considerare è ignorare completamente anche le proprietà usando la parola chiave field per l'analisi del costruttore nullable. In questo caso, non ci sarebbero avvisi da nessuna parte sull'esigenza dell'utente di inizializzare qualcosa, ma anche nessun disturbo per l'utente, indipendentemente dal modello di inizializzazione che potrebbe essere in uso.

Poiché prevediamo di distribuire solo la funzionalità per la parola chiave field nella versione linguistica di anteprima in .NET 9, ci aspettiamo di avere la possibilità di modificare il comportamento dei valori nullable per la funzionalità in .NET 10. Pertanto, si potrebbe prendere in considerazione l'adozione di una soluzione "a basso costo" come questa a breve termine e crescere fino a una delle soluzioni più complesse a lungo termine.

field- attributi di nullabilità mirati

È possibile introdurre le seguenti impostazioni predefinite, ottenendo un livello ragionevole di sicurezza null, senza coinvolgere alcuna analisi interproedurale:

  1. La variabile field ha sempre la stessa annotazione nullable della proprietà.
  2. Gli attributi di nullabilità [field: MaybeNull, AllowNull], ecc., possono essere usati per personalizzare la nullabilità del campo sottostante.
  3. Le proprietà basate su campo vengono controllate per l'inizializzazione nei costruttori in base all'annotazione e agli attributi nullable del campo.
  4. i setter nelle proprietà supportate dal campo controllano l'inizializzazione di field in modo analogo ai costruttori.

Ciò significa che lo "scenario lazy little-l" sarà simile al seguente:

class C
{
    public C() { } // no need to warn about initializing C.Prop, as the backing field is marked nullable using attributes.

    [field: AllowNull, MaybeNull]
    public string Prop => field ??= GetPropValue();
}

Un motivo per cui abbiamo evitato l'uso degli attributi di nullability qui è che quelli che abbiamo sono davvero orientati intorno alla descrizione di input e output di dichiarazioni. Sono ingombranti da usare per descrivere la nullabilità delle variabili di lunga durata.

  • In pratica, [field: MaybeNull, AllowNull] è necessario per fare in modo che il campo si comporti "ragionevolmente" come variabile nullable, che fornisce uno stato di flusso iniziale forse null e consente la scrittura di possibili valori Null. Sembra complesso chiedere agli utenti di fare qualcosa per scenari 'lazy' piuttosto comuni.
  • Se perseguissimo questo approccio, prenderemmo in considerazione l'aggiunta di un avviso quando viene usato [field: AllowNull], suggerendo anche di aggiungere MaybeNull. Ciò è dovuto al fatto che AllowNull da solo non soddisfa le esigenze degli utenti di una variabile nullable: presuppone che il campo sia inizialmente non null quando non abbiamo ancora visto nulla essere scritto su di essa.
  • È anche possibile modificare il comportamento di [field: MaybeNull] sulla parola chiave field, o anche sui campi in generale, per consentire la scrittura dei valori Null anche nella variabile, come se AllowNull fossero presenti in modo implicito.

Risposte alle domande LDM

Posizioni di sintassi per le parole chiave

Nelle funzioni di accesso in cui field e value possono essere associati a un campo sottostante sintetizzato o a un parametro setter implicito, in quali percorsi di sintassi gli identificatori devono essere considerati parole chiave?

  1. sempre
  2. solo espressioni primarie
  3. mai

I primi due casi sono cambiamenti radicali.

Se gli identificatori sono sempre parole chiave considerate, si tratta di una modifica che causa un'interruzione per quanto segue, ad esempio:

class MyClass
{
    private int field;
    public int P => this.field; // error: expected identifier

    private int value;
    public int Q
    {
        set { this.value = value; } // error: expected identifier
    }
}

Se gli identificatori sono parole chiave quando vengono usate come solo espressioni primarie, la modifica di rilievo è più piccola. L'interruzione più comune può essere l'uso non qualificato di un membro esistente denominato field.

class MyClass
{
    private int field;
    public int P => field; // binds to synthesized backing field rather than 'this.field'
}

Si verifica anche un'interruzione quando field o value viene rideclarato in una funzione nidificata. Può trattarsi dell'unica interruzione per value per le espressioni primarie .

class MyClass
{
    private IEnumerable<string> _fields;
    public bool HasNotNullField
    {
        get => _fields.Any(field => field is { }); // 'field' binds to synthesized backing field
    }
    public IEnumerable<string> Fields
    {
        get { return _fields; }
        set { _fields = value.Where(value => Filter(value)); } // 'value' binds to setter parameter
    }
}

Se gli identificatori sono mai considerate parole chiave, gli identificatori verranno associati solo a un campo sottostante sintetizzato o al parametro implicito quando non sono legati ad altri membri. Non ci sono modifiche significative per questo caso.

Risposta

field è una parola chiave nei metodi di accesso appropriati quando viene utilizzata solo come espressione primaria ; value non è mai considerata una parola chiave.

Scenari simili a { set; }

{ set; } non è attualmente consentito e questo ha senso: il campo che crea non può mai essere letto. Esistono ora nuovi modi per trovarsi in una situazione in cui il setter introduce un campo di supporto che non viene mai letto, ad esempio l'espansione di { set; } in { set => field = value; }.

Quali di questi scenari devono essere consentiti di compilare? Immagina che l'avviso "field is never read" venga applicato allo stesso modo di un campo dichiarato manualmente.

  1. { set; } - Non consentito oggi, continuare a vietarlo
  2. { set => field = value; }
  3. { get => unrelated; set => field = value; }
  4. { get => unrelated; set; }
  5. {
        set
        {
            if (field == value) return;
            field = value;
            SendEvent(nameof(Prop), value);
        }
    }
    
  6. {
        get => unrelated;
        set
        {
            if (field == value) return;
            field = value;
            SendEvent(nameof(Prop), value);
        }
    }
    

Risposta

Non consentire ciò che è già consentito oggi nelle proprietà automatiche, l'set;senza corpo.

field nella funzione di accesso agli eventi

Deve field essere una parola chiave in una funzione di accesso agli eventi e il compilatore deve generare un campo sottostante?

class MyClass
{
    public event EventHandler E
    {
        add { field += value; }
        remove { field -= value; }
    }
}

raccomandazione: field non è una parola chiave all'interno di una funzione di accesso agli eventi e non viene generato alcun campo sottostante.

Risposta

Raccomandazione presa. field non è una parola chiave all'interno di una funzione di accesso agli eventi e non viene generato alcun campo sottostante.

Nullabilità di field

Dovrebbe essere accettata la proposta di nullità di field? Vedere la sezione Nullability e la domanda aperta all'interno.

Risposta

È stata adottata una proposta generale. Un comportamento specifico richiede ancora una revisione maggiore.

field nell'inizializzatore di proprietà

Deve field essere una parola chiave in un inizializzatore di proprietà e vincolarsi al campo sottostante?

class A
{
    const int field = -1;

    object P1 { get; } = field; // bind to const (ok) or backing field (error)?
}

Esistono scenari utili per fare riferimento al campo sottostante nell'inizializzatore?

class B
{
    object P2 { get; } = (field = 2);        // error: initializer cannot reference instance member
    static object P3 { get; } = (field = 3); // ok, but useful?
}

Nell'esempio precedente, l'associazione al campo sottostante dovrebbe generare un errore: "L'inizializzatore non può fare riferimento a un campo non statico".

Risposta

Associeremo l'inizializzatore come nelle versioni precedenti di C#. Il campo sottostante non verrà inserito nell'ambito né verrà impedito di fare riferimento ad altri membri denominati field.

Interazione con proprietà parziali

Inizializzatori

Quando una proprietà parziale usa field, quali parti devono essere autorizzate a avere un inizializzatore?

partial class C
{
    public partial int Prop { get; set; } = 1;
    public partial int Prop { get => field; set => field = value; } = 2;
}
  • Sembra chiaro che deve verificarsi un errore quando entrambe le parti hanno un inizializzatore.
  • È possibile considerare i casi d'uso in cui la definizione o la parte di implementazione potrebbe voler impostare il valore iniziale del field.
  • Sembra che se consentiamo l'inizializzatore nella parte di definizione, stiamo effettivamente costringendo l'implementatore a usare field affinché il programma sia valido. Va bene?
  • Riteniamo che sia comune che i generatori usino field ogni volta che è necessario un campo sottostante dello stesso tipo nell'implementazione. Ciò è in parte dovuto al fatto che i generatori spesso vogliono consentire agli utenti di usare gli attributi di destinazione [field: ...] nella parte di definizione della proprietà. L'uso della parola chiave field salva l'implementatore del generatore dal problema di "inoltrare" tali attributi a qualche campo generato e sopprimere gli avvisi sulla proprietà. È probabile che questi stessi generatori vogliano consentire all'utente di specificare un valore iniziale per il campo.

Raccomandazione: consentire un inizializzatore in una delle due componenti di una proprietà parziale quando la parte di implementazione usa field. Segnalare un errore se entrambe le parti hanno un inizializzatore.

Risposta

Raccomandazione accettata. Dichiarare o implementare le posizioni delle proprietà può utilizzare un inizializzatore, ma non entrambi allo stesso tempo.

Funzioni di accesso automatico

Come originariamente progettato, l'implementazione parziale della proprietà deve avere un corpo per ciascun accessore. Tuttavia, le iterazioni recenti della funzionalità di parola chiave field hanno incluso il concetto di "funzioni di accesso automatico". Le implementazioni di proprietà parziali devono essere in grado di usare tali funzioni di accesso? Se vengono usati esclusivamente, sarà indistinguibile da una dichiarazione di definizione.

partial class C
{
    public partial int Prop0 { get; set; }
    public partial int Prop0 { get => field; set => field = value; } // this is equivalent to the two "semi-auto" forms below.

    public partial int Prop1 { get; set; }
    public partial int Prop1 { get => field; set; } // is this a valid implementation part?

    public partial int Prop2 { get; set; }
    public partial int Prop2 { get; set => field = value; } // what about this? will there be disagreement about which is the "best" style?

    public partial int Prop3 { get; }
    public partial int Prop3 { get => field; } // it will only be valid to use at most 1 auto-accessor, when a second accessor is manually implemented.

raccomandazione: non consentire le funzioni di accesso automatico nelle implementazioni parziali delle proprietà, perché le limitazioni relative a quando sarebbero utilizzabili sono più confuse da seguire rispetto al vantaggio di consentire loro.

Risposta

Almeno una funzione di accesso che implementa deve essere implementata manualmente, ma l'altra funzione di accesso può essere implementata automaticamente.

Campo di sola lettura

Quando il campo sottostante sintetizzato deve essere considerato di sola lettura?

struct S
{
    readonly object P0 { get => field; } = "";         // ok
    object P1          { get => field ??= ""; }        // ok
    readonly object P2 { get => field ??= ""; }        // error: 'field' is readonly
    readonly object P3 { get; set { _ = field; } }     // ok
    readonly object P4 { get; set { field = value; } } // error: 'field' is readonly
}

Quando il campo sottostante viene considerato di sola lettura, il campo emesso nei metadati viene contrassegnato initonlye viene segnalato un errore se field viene modificato a meno che non sia in un inizializzatore o in un costruttore.

raccomandazione: il campo sottostante sintetizzato è di sola lettura quando il tipo contenitore è un struct e la proprietà o il tipo contenitore è dichiarato readonly.

Risposta

La raccomandazione viene accettata.

Contesto di sola lettura e set

È consigliabile consentire una funzione di accesso set in un contesto di readonly per una proprietà che usa field?

readonly struct S1
{
    readonly object _p1;
    object P1 { get => _p1; set { } }   // ok
    object P2 { get; set; }             // error: auto-prop in readonly struct must be readonly
    object P3 { get => field; set { } } // ok?
}

struct S2
{
    readonly object _p1;
    readonly object P1 { get => _p1; set { } }   // ok
    readonly object P2 { get; set; }             // error: auto-prop with set marked readonly
    readonly object P3 { get => field; set { } } // ok?
}

Risposta

Potrebbero esserci scenari in cui si sta implementando una funzione di accesso set in uno struct readonly e passandolo o generando un'eccezione. Questo sarà consentito.

codice [Conditional]

Il campo sintetizzato deve essere generato quando field viene usato solo nelle chiamate omesse ai metodi condizionali ?

Ad esempio, è necessario generare un campo di supporto per quello che segue in una build non-DEBUG?

class C
{
    object P
    {
        get
        {
            Debug.Assert(field is null);
            return null;
        }
    }
}

A titolo di riferimento, i campi dei parametri del costruttore primario vengono generati in casi simili. Vedere sharplab.io.

Raccomandazione: un campo di supporto viene generato solo quando field è utilizzato nelle chiamate omesse ai metodi condizionali .

Risposta

Conditional codice può avere effetti sul codice non condizionato, come il cambiamento della nullabilità con Debug.Assert. Sarebbe strano se field non abbia avuto effetti simili. È anche improbabile che appaia nella maggior parte del codice, quindi faremo la cosa semplice e accetteremo la raccomandazione.

Proprietà dell'interfaccia e funzioni di accesso automatico

Una combinazione di funzioni di accesso implementate manualmente e automaticamente viene riconosciuta per una proprietà interface in cui la funzione di accesso implementata automaticamente fa riferimento a un campo sottostante sintetizzato?

Per una proprietà di istanza, verrà segnalato un errore che indica che i campi dell'istanza non sono supportati.

interface I
{
           object P1 { get; set; }                           // ok: not an implementation
           object P2 { get => field; set { field = value; }} // error: instance field

           object P3 { get; set { } } // error: instance field
    static object P4 { get; set { } } // ok: equivalent to { get => field; set { } }
}

raccomandazione: le funzioni di accesso automatico vengono riconosciute nelle proprietà interface e le funzioni di accesso automatico fanno riferimento a un campo sottostante sintetizzato. Per una proprietà di istanza, viene segnalato un errore che indica che i campi dell'istanza non sono supportati.

Risposta

Standardizzare attorno al fatto che il campo dell'istanza sia la causa dell'errore è coerente con le proprietà parziali nelle classi, e ci piace questo risultato. La raccomandazione è accettata.