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;
einit;
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;
oinit;
).
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:
È 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).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 chiavefield
.
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 esempiostring?
. Ciò fa sì chefield
abbia uno stato iniziale di forse nullo o forse predefinito nella funzione di accessoget
, 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
,NotNull
oDisallowNull
, 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
oT
) 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
oT
) 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
,set
einit
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:
- un accessor con solo un punto e virgola nel corpo
- '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 virgolaget
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 chiavefield
all'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 consideratoreadonly
(§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 sottostantedi sola lettura della proprietà.Una proprietà automatica non può avere solo un accessore che sia solo un punto e virgola
set
senza un accessoreget
.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
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:
- La variabile
field
ha sempre la stessa annotazione nullable della proprietà. - Gli attributi di nullabilità
[field: MaybeNull, AllowNull]
, ecc., possono essere usati per personalizzare la nullabilità del campo sottostante. - Le proprietà basate su campo vengono controllate per l'inizializzazione nei costruttori in base all'annotazione e agli attributi nullable del campo.
- 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 aggiungereMaybeNull
. 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 chiavefield
, o anche sui campi in generale, per consentire la scrittura dei valori Null anche nella variabile, come seAllowNull
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?
- sempre
- solo espressioni primarie
- 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.
-
{ set; }
- Non consentito oggi, continuare a vietarlo { set => field = value; }
{ get => unrelated; set => field = value; }
{ get => unrelated; set; }
-
{ set { if (field == value) return; field = value; SendEvent(nameof(Prop), value); } }
-
{ 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 chiavefield
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 initonly
e 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.
C# feature specifications