Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Annotazioni
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 riportate nelle note pertinenti della riunione di progettazione linguistica (LDM) .
Ulteriori dettagli sul processo di adozione delle specifiche di funzionalità nello standard del linguaggio C# sono disponibili nell'articolo sulle specifiche .
Questione prioritaria: https://github.com/dotnet/csharplang/issues/9662
Sommario
Unioni è un set di funzionalità collegate che combinano per fornire il supporto C# per i tipi di unione:
-
Tipi di unione: gli struct e le classi con un
[Union]attributo vengono riconosciuti come tipi di unione e supportano i comportamenti di unione. - Tipi di maiuscole/minuscole: i tipi union hanno un set di tipi case, che vengono forniti dai parametri ai costruttori e ai metodi factory.
-
Comportamenti dell'unione: i tipi di unione supportano i comportamenti di unione seguenti:
- Conversioni di unioni: esistono conversioni di unione implicite da ogni tipo di case a un tipo di unione.
- Corrispondenza unione: la corrispondenza dei criteri rispetto ai valori di unione in modo implicito "annulla il wrapping" del contenuto, applicando invece il modello al valore sottostante.
- Esaustività dell'unione: le espressioni di cambio sui valori di unione sono esaustive quando tutti i tipi di maiuscole e minuscole sono corrispondenti, senza la necessità di un caso di fallback.
- Nullbility dell'unione: l'analisi dei valori Null ha migliorato il rilevamento dello stato Null del contenuto di un'unione.
- Modelli di unione: tutti i tipi di unione seguono un modello di unione di base, ma esistono modelli facoltativi aggiuntivi per scenari specifici.
- Dichiarazioni di unione: una sintassi abbreviata consente direttamente la dichiarazione di tipi di unione. L'implementazione è "opinionated" - una dichiarazione di struct che segue il modello di unione di base e archivia il contenuto come un singolo campo di riferimento.
- Interfacce di unione: alcune interfacce sono note dal linguaggio e usate nell'implementazione delle dichiarazioni di unione.
Motivazione
Le unioni sono una funzionalità C# richiesta a lungo termine, che consente di esprimere valori da un set chiuso di tipi in modo che i criteri di ricerca possano considerare attendibili per essere esaustivi.
La separazione tra i tipi di unione e le dichiarazioni di unione consente a C# di avere una sintassi di dichiarazione di unione con semantica con opinioni, consentendo anche ai tipi o ai tipi esistenti con altre opzioni di implementazione di acconsentire esplicitamente ai comportamenti di unione.
Le unioni proposte in C# sono unioni di tipi e non "discriminate" o "contrassegnate". Le "unioni discriminate" possono essere espresse in termini di "unioni di tipi" usando dichiarazioni di tipo fresco come tipi di maiuscole e minuscole. In alternativa, è possibile implementare come gerarchia chiusa, che è un'altra funzionalità C# correlata, in programma, incentrata sull'esaustività.
Progettazione dettagliata
Tipi unione
Qualsiasi tipo di classe o struct con un System.Runtime.CompilerServices.UnionAttribute attributo è considerato un tipo di unione:
namespace System.Runtime.CompilerServices
{
[AttributeUsage(Class | Struct, AllowMultiple = false)]
public class UnionAttribute : Attribute;
}
Un tipo di unione deve seguire un determinato modello di membri dell'unione pubblica, che deve essere dichiarato nel tipo di unione stesso o delegato a un "provider di membri dell'unione".
Alcuni membri dell'unione sono obbligatori e altri sono facoltativi.
Un tipo di unione ha un set di tipi di maiuscole e minuscole stabiliti in base alle firme di determinati membri dell'unione.
È possibile accedere al contenuto di un valore di unione tramite una Value proprietà . Il linguaggio presuppone che Value contenga solo un valore di uno dei tipi di maiuscole e minuscole o null (vedere Formato corretto).
Provider di membri dell'unione
Per impostazione predefinita, i membri dell'unione vengono trovati nel tipo di unione stesso. Tuttavia, se il tipo di unione contiene direttamente una dichiarazione di un'interfaccia denominata IUnionMembers , l'interfaccia funge da provider di membri dell'unione. In tal caso, i membri dell'unione vengono trovati solo nel provider di membri dell'unione, non nel tipo di unione stessa.
Un'interfaccia del provider membro unione deve essere pubblica e il tipo di unione deve implementarlo come interfaccia.
Viene usato il termine tipo di definizione dell'unione per il tipo in cui vengono trovati i membri dell'unione: il provider di membri dell'unione, se esistente, e il tipo di unione stesso in caso contrario.
Membri dell'Unione
I membri dell'unione vengono cercati in base al nome e alla firma nel tipo di definizione dell'unione. Non devono essere dichiarati direttamente nel tipo di definizione dell'unione, ma possono essere ereditati.
Si tratta di un errore per qualsiasi membro dell'unione che non sia pubblico.
I membri di creazione e la Value proprietà sono obbligatori e sono collettivamente definiti modello di unione di base.
I HasValue membri e TryGetValue vengono collettivamente definiti modello di accesso di unione non boxing.
I diversi membri dell'unione sono descritti di seguito.
Membri della creazione dell'unione
I membri di creazione dell'unione vengono usati per creare nuovi valori di unione da un valore di tipo case.
Se il tipo di definizione dell'unione è il tipo di unione stesso, ogni costruttore con un singolo parametro è un costruttore di unione. I tipi case dell'unione vengono identificati come set di tipi compilati dai tipi di parametro di questi costruttori nel modo seguente:
- Se il tipo di parametro è un tipo nullable (se un valore o un riferimento), il tipo case è il tipo sottostante
- In caso contrario, il tipo case è il tipo di parametro.
// Union constructor making `Dog` a case type
public Pet(Dog value) { ... }
// Union constructor making `int` a case type
public Union(int? value) { ... }
// Union constructor making `string` a case type
public Union(string? value) { ... }
Se il tipo di definizione dell'unione è un provider di membri dell'unione, ogni metodo statico Create con un singolo parametro e un tipo restituito che è identity-convertibile nel tipo di unione stesso è un metodo union factory.
I tipi case dell'unione vengono identificati come set di tipi compilati dai tipi di parametro di questi metodi factory nel modo seguente:
- Se il tipo di parametro è un tipo nullable (se un valore o un riferimento), il tipo case è il tipo sottostante
- In caso contrario, il tipo case è il tipo di parametro.
// Union factory method making `Cat` a case type
public static Pet Create(Cat value) { ... }
// Union factory method making `int` a case type
public static Union Create(int? value) { ... }
// Union factory method making `string` a case type
public static Union Create(string? value) { ... }
I costruttori di unione e i metodi di union factory vengono definiti collettivamente membri della creazione dell'unione.
Il singolo parametro di un membro di creazione dell'unione deve essere un parametro per valore o in .
Un tipo di unione deve avere almeno un membro di creazione dell'unione e quindi almeno un tipo di case.
Proprietà Value
La Value proprietà consente l'accesso al valore contenuto in un'unione, indipendentemente dal tipo di case.
Ogni tipo che definisce l'unione deve dichiarare una Value proprietà di tipo object? o object. La proprietà deve avere una funzione di accesso e può facoltativamente avere una getinit funzione di accesso o set , che può essere di qualsiasi accessibilità e non viene usata dal compilatore.
// Union 'Value' property
public object? Value { get; }
Membri di accesso non boxing
Un tipo di unione può scegliere di implementare anche il modello di accesso unione non boxing, che consente l'accesso condizionale fortemente tipizzato a ogni tipo di case, nonché un modo per verificare la presenza di valori Null.
Ciò consente al compilatore di implementare i criteri di ricerca in modo più efficiente quando i tipi case sono tipi valore e archiviati come tali all'interno dell'unione.
I membri di accesso non boxing sono:
- Proprietà
HasValuedi tipoboolcon una funzione di accesso pubblicaget. Facoltativamente, può avere unainitfunzione di accesso oset, che può essere di qualsiasi accessibilità e non viene usata dal compilatore. - Metodo
TryGetValueper ogni tipo di case. Il metodo restituisceboole accetta un singolo parametro out di un tipo che è identity-convertibile nel tipo case.
// Non-boxing access members
public bool HasValue { get { ... } }
public bool TryGetValue(out Dog value) { ... }
HasValue è previsto che restituisca true se e solo se l'unione Value non è Null.
TryGetValue è previsto che restituisca true se e solo se l'unione Value è del tipo di case specificato e, in tal caso, recapitare tale valore nel parametro out del metodo.
Ben definito
Il linguaggio e il compilatore fanno una serie di presupposti comportamentali sui tipi di unione. Se un tipo viene qualificato come tipo di unione ma non soddisfa tali presupposti, i comportamenti di unione potrebbero non funzionare come previsto.
-
Suono: la
Valueproprietà restituisce sempre null o un valore di un tipo case. Questo vale anche per il valore predefinito del tipo di unione. -
Stabilità: se un valore di unione viene creato da un tipo case, la
Valueproprietà corrisponderà al tipo di case o null. Se un valore di unione viene creato da unnullvalore, laValueproprietà sarànull. - Equivalenza della creazione: se un valore è convertibile in modo implicito in due tipi di case diversi, il membro di creazione per uno di questi tipi di case ha lo stesso comportamento osservabile quando viene chiamato con tale valore.
-
Coerenza dei criteri di accesso: il comportamento dei membri di
HasValueaccesso eTryGetValuenon boxing, se presente, equivale in modo osservabile a quello di controllare direttamente laValueproprietà.
Esempi di tipi di unione
Pet implementa il modello di unione di base sul tipo di unione stesso:
[Union] public record struct Pet
{
// Creation members = case types are 'Dog' and 'Cat'
public Pet(Dog value) => Value = value;
public Pet(Cat value) => Value = value;
// 'Value' property
public object? Value { get; }
}
IntOrBool implementa il modello di accesso non boxing sul tipo di unione stesso:
public record struct IntOrBool
{
private bool _isBool;
private int _value;
public IntOrBool(int value) => (_isBool, _value) = (false, value);
public IntOrBool(bool value) => (_isBool, _value) = (true, value ? 1 : 0);
public object Value => _isBool ? _value is 1 : _value;
public bool HasValue => true;
public bool TryGetValue(out int value)
{
value = _value;
return !_isBool;
}
public bool TryGetValue(out bool value)
{
value = _isBool && _value is 1;
return _isBool;
}
}
Nota: Questo è solo un esempio di come potrebbe essere implementato il modello di accesso non boxing. Il codice utente può archiviare il contenuto in qualsiasi modo. In particolare, non impedisce l'implementazione di boxing! Nel non-boxing nome si intende consentire all'implementazione di criteri di ricerca del compilatore di accedere a ogni tipo di case in modo fortemente tipizzato, anziché alla object?proprietà tipizzata Value .
Result<T> implementa il modello di base tramite un provider di membri dell'unione:
public record class Result<T> : Result<T>.IUnionMembers
{
object? _value;
public interface IUnionMembers
{
public static Result<T> Create(T value) => new() { _value = value };
public static Result<T> Create(Exception value) => new() { _value = value };
public object? Value { get; }
}
object? IUnionMembers.Value => _value;
}
Comportamenti dell'unione
I comportamenti di unione vengono in genere implementati tramite il modello di unione di base. Se l'unione offre il modello di accesso non boxing, la corrispondenza dei criteri di unione ne userà preferibilmente l'uso.
Conversioni di unioni
Una conversione unione converte in modo implicito in un tipo di unione da ognuno dei relativi tipi case. In particolare, è presente una conversione unione in un tipo di unione da un tipo U o un'espressione E se è presente una conversione implicita standard da E a un tipo C ed C è un tipo di parametro di un membro di creazione dell'unione di U.
Se il tipo U di unione è uno struct, è presente una conversione unione nel tipo U? da un tipo o un'espressione E se è presente una conversione implicita standard da E a un tipo C ed C è un tipo di parametro di un membro di creazione dell'unione di U.
Una conversione unione non è una conversione implicita standard. Potrebbe pertanto non partecipare a una conversione implicita definita dall'utente o a un'altra conversione dell'unione.
Non esistono conversioni di unione esplicite oltre le conversioni di unione implicite. Pertanto, anche se è presente una conversione esplicita dal E tipo Ccase a un'unione , che non significa che sia presente una conversione esplicita da E a tale tipo di unione.
Una conversione dell'unione viene eseguita chiamando il membro di creazione dell'unione:
Pet pet = dog;
// becomes
Pet pet = new Pet(dog);
// and
Result<string> result = "Hello"
//becomes
Result<string> result = Result<string>.IUnionMembers.Create("Hello");
Si tratta di un errore se la risoluzione dell'overload non trova un singolo membro candidato migliore o se tale membro non è uno dei membri dell'unione del tipo di unione.
La conversione dell'unione è solo un altro "formato" di una conversione implicita definita dall'utente. Conversione dell'operatore di conversione "shadows" applicabile definita dall'utente.
La logica alla base di questa decisione:
Se un utente ha scritto un operatore definito dall'utente, dovrebbe ottenere la priorità. In altre parole, se l'utente ha effettivamente scritto il proprio operatore, vuole chiamarlo. I tipi esistenti con operatori di conversione trasformati in tipi di unione continuano a funzionare allo stesso modo rispetto al codice esistente che utilizza attualmente gli operatori.
Nell'esempio seguente una conversione implicita definita dall'utente ha la priorità su una conversione unione.
struct S1 : System.Runtime.CompilerServices.IUnion
{
public S1(int x) => ...
public S1(string x) => ...
object System.Runtime.CompilerServices.IUnion.Value => ...
public static implicit operator S1(int x) => ...
}
class Program
{
static S1 Test1() => 10; // implicit operator S1(int x) is used
static S1 Test2() => (S1)20; // implicit operator S1(int x) is used
}
Nell'esempio seguente, quando viene usato il cast esplicito nel codice, una conversione esplicita definita dall'utente ha la priorità su una conversione unione. Tuttavia, quando non è presente alcun cast esplicito nel codice, viene usata una conversione unione perché la conversione esplicita definita dall'utente non è applicabile.
struct S2 : System.Runtime.CompilerServices.IUnion
{
public S2(int x) => ...
public S2(string x) => ...
object System.Runtime.CompilerServices.IUnion.Value => ...
public static explicit operator S2(int x) => ...
}
class Program
{
static S2 Test3() => 10; // Union conversion S2.S2(int) is used
static S2 Test4() => (S2)20; // explicit operator S2(int x)
}
Corrispondenza unione
Quando il valore in ingresso di un criterio è di un tipo di unione o di un tipo di unione nullable, il valore nullable e il contenuto del valore di unione sottostante possono essere "unwrapped", a seconda del criterio.
Per i modelli e var non condizionali_, il modello viene applicato al valore in ingresso stesso. Per esempio:
if (GetPet() is var pet) { ... } // 'pet' is the union value returned from `GetPet`
Tuttavia, tutti gli altri modelli vengono applicati in modo implicito alla proprietà dell'unione Value sottostante:
if (GetPet() is Dog dog) { ... } // 'Dog dog' is applied to 'GetPet().Value'
if (GetPet() is null) { ... } // 'null' is applied to 'GetPet().Value'
if (GetPet() is { } value) { ... } // '{ } value' is applied to 'GetPet().Value'
Per i modelli logici, questa regola viene applicata singolarmente ai rami, tenendo presente che il ramo sinistro di un and criterio può influire sul tipo in ingresso del ramo destro:
GetPet() switch
{
var pet and not null => ... // 'var pet' applies to the incoming 'Pet' and 'not null' to its 'Value'
not null and var value => ... // 'not null' applies to the 'Value' as does 'var value' because of the
// left branch changing the incoming type to `object?`.
}
Nota: Questa regola significa che GetPet() is Pet pet probabilmente non avrà esito positivo, come Pet viene applicato al contenuto, non all'unione Pet stessa.
Nota: Il motivo del diverso trattamento del modello incondizionato var (e _, che è essenzialmente una abbreviata per var _) è un presupposto che il loro uso sia qualitativamente diverso da altri modelli.
var i modelli vengono usati semplicemente per denominare il valore confrontato, spesso in modelli annidati, ad esempio PetOwner{ Pet: var pet }. In questo caso, la semantica utile consiste pet nel mantenere il tipo Petdi unione , anziché la Value proprietà da dereferenziare a un tipo inutile object? .
Se il valore in ingresso è un tipo di classe, il null criterio avrà esito positivo indipendentemente dal fatto che il valore di unione stesso sia null o il relativo valore contenuto sia null:
if (result is null) { ... } // if (result == null || result.Value == null)
Altri criteri di corrispondenza unione avranno esito positivo solo quando il valore di unione stesso non nullè .
if (result is 1) { ... } // if (result != null && result.Value is 1)
Analogamente, se il valore in ingresso è un tipo di valori nullable (wrapping di un tipo di unione struct), il null criterio avrà esito positivo indipendentemente dal fatto che il valore in ingresso sia null o il relativo valore contenuto sia null:
if (result is null) { ... } // if (result.HasValue == false || result.GetValueOrDefault().Value == null)
Altri criteri di corrispondenza dell'unione avranno esito positivo solo quando il valore in ingresso stesso non nullè .
if (result is 1) { ... } // if (result.HasValue && result.GetValueOrDefault().Value is 1)
Il compilatore preferisce implementare il comportamento del modello tramite i membri prescritti dal modello di accesso non boxing. Sebbene sia possibile eseguire qualsiasi ottimizzazione entro i limiti delle regole di ben formato, è garantito che vengano applicate le impostazioni minime seguenti:
- Per un modello che implica il controllo di un tipo
Tspecifico , se è disponibile unTryGetValue(S value)metodo e esiste un'identità o una conversione implicita di riferimento/boxing daTaS, tale metodo viene usato per ottenere il valore. Il criterio viene quindi applicato a tale valore. Se è presente più di un metodo di questo tipo, qualsiasi posizione in cui la conversione daTaSnon è una conversione boxing è preferibile, se disponibile. Se è ancora presente più di un metodo, ne viene scelto uno in modo definito dall'implementazione. - In caso contrario, per un criterio che implica la verifica di
null, se è disponibile unaHasValueproprietà, tale proprietà viene usata per verificare se il valore di unione è Null. - In caso contrario, il modello viene applicato al risultato dell'accesso alla
IUnion.Valueproprietà nell'unione in ingresso.
L'operatore di tipo is applicato a un tipo di unione ha lo stesso significato di un criterio di tipo applicato al tipo di unione.
Esaustività dell'Unione
Si presuppone che un tipo di unione sia "esaurito" dai relativi tipi di maiuscole e minuscole. Ciò significa che un'espressione switch è esaustiva se gestisce tutti i tipi di maiuscole e minuscole di un'unione:
var name = pet switch
{
Dog dog => ...,
Cat cat => ...,
// No warning about non-exhaustive switch
};
Nullabilità
Lo stato Null della proprietà di Value un'unione viene rilevato come qualsiasi altra proprietà, con queste modifiche:
- Quando un membro di creazione dell'unione viene chiamato (in modo esplicito o tramite una conversione di
Valueunione), il nuovo unione ottiene lo stato Null del valore in ingresso. - Quando il modello di
HasValueaccesso non boxing oTryGetValue(...)viene usato per eseguire query sul contenuto di un tipo di unione (in modo esplicito o tramite criteri di ricerca), influisce sulloValuestato di nullità nello stesso modo in cui seValuefosse stato selezionato direttamente: lo stato Null diValuediventa "not null" neltrueramo.
Anche quando un'opzione di unione è altrimenti esaustiva, se lo stato Null della proprietà dell'unione Value in ingresso è "forse null", verrà visualizzato un avviso su null non gestito.
Pet pet = GetNullableDog(); // 'pet.Value' is "maybe null"
var value = pet switch
{
Dog dog => ...,
Cat cat => ...,
// Warning: 'null' not handled
}
Interfacce unioni
Le interfacce seguenti vengono usate dal linguaggio nell'implementazione delle funzionalità di unione.
Interfaccia di accesso unione
L'interfaccia IUnion contrassegna un tipo come tipo di unione in fase di compilazione e consente di accedere al contenuto dell'unione in fase di esecuzione.
public interface IUnion
{
// The value of the union or null
object? Value { get; }
}
Le unioni generate dal compilatore implementano questa interfaccia.
Esempio di utilizzo:
if (value is IUnion { Value: null }) { ... }
Dichiarazioni di unione
Le dichiarazioni di unione sono un modo conciso e opinioneato di dichiarare i tipi di unione in C#. Dichiarano uno struct che usa un singolo riferimento a un oggetto per l'archiviazione del relativo Value, ovvero:
- Boxing: qualsiasi tipo valore tra i relativi tipi di maiuscole e minuscole verrà sottoposto a boxing nella voce.
- Compattazione: i valori unione contengono solo un singolo campo.
L'intento è che le dichiarazioni di unione coprono la maggior parte dei casi d'uso abbastanza bene. Si prevede che i due motivi principali per la codifica manuale di tipi di unione specifici anziché usare le dichiarazioni di unione siano:
- Adattamento dei tipi esistenti ai modelli di unione per ottenere comportamenti di unione.
- Implementazione di una strategia di archiviazione diversa per motivi di efficienza o interoperabilità.
Sintassi
Una dichiarazione di unione ha un nome e un elenco di tipi di costruttori di unione .
union_declaration
: attributes? struct_modifier* 'partial'? 'union' identifier type_parameter_list?
'(' type (',' type)* ')' struct_interfaces? type_parameter_constraints_clause*
(`{` struct_member_declaration* `}` | ';')
;
Oltre alle restrizioni relative ai membri struct (§16.3), i seguenti si applicano ai membri dell'unione:
- Non sono consentiti campi di istanza, proprietà automatiche o eventi simili a campi.
- I costruttori pubblici dichiarati in modo esplicito con un singolo parametro non sono consentiti.
- I costruttori dichiarati in modo esplicito devono usare un
this(...)inizializzatore per (direttamente o indirettamente) delegato a uno dei costruttori generati.
I tipi di costruttori di unione possono essere qualsiasi tipo che converte in object, ad esempio interfacce, parametri di tipo, tipi nullable e altre unioni. È opportuno che i case risultanti si sovrappongano e che le unioni annidano o siano null.
Esempi:
// Union of existing types
public union Pet(Cat, Dog, Bird);
// Union with function member
public union OneOrMore<T>(T, IEnumerable<T>)
{
public IEnumerable<T> AsEnumerable() => Value switch
{
IEnumerable<T> list => list,
T value => [value],
}
}
// "Discriminated" union with freshly declared case types
public record class None();
public record class Some<T>(T value);
public union Option<T>(None, Some<T>);
#### Lowering
A union declaration is lowered to a struct declaration with
* the same attributes, modifiers, name, type parameters and constraints,
* implicit implementations of `IUnion`,
* a `public object? Value { get; }` auto-property,
* a public constructor for each *union constructor* type,
* any members in the union declaration's body.
It is an error for user-declared members to conflict with generated members.
Example:
``` c#
public union Pet(Cat, Dog){ ... }
È inferiore a:
[Union] public struct Pet : IUnion
{
public Pet(Cat value) => Value = value;
public Pet(Dog value) => Value = value;
public object? Value { get; }
... // original body
}
Domande aperte
[Risolto] La dichiarazione di unione è un record?
Una dichiarazione di unione viene ridotta a uno struct di record
Penso che questo comportamento predefinito non sia necessario e, dato che non è configurabile, andando a limitare in modo significativo gli scenari di utilizzo. I record generano un sacco di codice non usato o non soddisfano requisiti specifici. Ad esempio, i record sono praticamente vietati nella codebase del compilatore a causa di tale bloat di codice. Penso che sarebbe meglio modificare il valore predefinito:
- Per impostazione predefinita, una dichiarazione di unione dichiara uno struct regolare con solo membri specifici dell'unione.
- Un utente può dichiarare un'unione di record:
record union U(E1, ...) ...
Risoluzione: Una dichiarazione di unione è uno struct semplice, non uno struct di record. Non record union ... è supportato
[Risolto] Sintassi della dichiarazione di unione
Sembra che la sintassi proposta sia incompleta o inutilmente limitata. Ad esempio, sembra che la clausola base non sia consentita. Tuttavia, posso facilmente immaginare una necessità di implementare un'interfaccia, ad esempio.
A parte l'elemento-types-list, la sintassi deve corrispondere alla dichiarazione regolare struct/record struct in cui la parola chiave viene sostituita con union la struct parola chiave .
Risoluzione: La restrizione viene rimossa.
[Risolto] Membri della dichiarazione di Unione
Non sono consentiti campi di istanza, proprietà automatiche o eventi simili a campi.
Questo si sente arbitrario e assolutamente superfluo.
Risoluzione: La restrizione viene mantenuta.
[Risolto] Tipi valore nullable come tipi di maiuscole/minuscole union
I tipi di maiuscole e minuscole dell'unione vengono identificati come set di tipi di parametro da questi costruttori. I tipi di case dell'unione vengono identificati come set di tipi di parametro di questi metodi factory.
Allo stesso tempo:
Metodo
TryGetValueper ogni tipo di case. Il metodo restituisceboole accetta un singolo parametro out di un tipo che corrisponde al tipo di case specificato nel modo seguente:
- Se il tipo case è un tipo valore nullable, il tipo del parametro deve essere identity-convertibile nel tipo sottostante
- In caso contrario, il tipo deve essere identity-convertibile nel tipo case.
Esiste un vantaggio per avere un tipo valore nullable tra i tipi case, in particolare che un criterio di tipo non può usare un tipo valore nullable come tipo di destinazione? Si ritiene semplicemente che, se il tipo di parametro del costruttore/factory è un tipo valore nullable, il tipo case corrispondente è il tipo sottostante. Non sarebbe quindi necessaria tale clausola aggiuntiva per il TryGetValue metodo, tutti i parametri out sono tipi di maiuscole e minuscole.
Risoluzione: Il suggerimento è approvato
[Risolto] Stato nullable predefinito della Value proprietà
Per i tipi di unione in cui nessuno dei tipi case è nullable, lo stato predefinito per
Valueè "not null" anziché "forse null".
Con la nuova progettazione, dove Value la proprietà non è definita in un'interfaccia generale, ma è un'API che appartiene in modo specifico al tipo dichiarato, la regola riportata sopra si sente come over engineering. Inoltre, è probabile che la regola imponi ai consumer di usare tipi nullable in situazioni in cui i tipi altrimenti nullable non verranno usati.
Si consideri ad esempio la dichiarazione di unione seguente:
union U1(int, bool, DateTime);
In base alla regola tra virgolette, lo stato predefinito per Value è "not null". Ma questo non corrisponde al comportamento del tipo, default(U1).Value è null. Per riallineare il comportamento, il consumer deve rendere nullable almeno un tipo case. Qualcosa di simile al seguente:
union U1(int?, bool, DateTime);
Ma questo è probabilmente indesiderato, il consumer potrebbe non voler consentire la creazione esplicita con int? valore.
Proposta: rimuovere la regola tra virgolette, l'analisi nullable deve usare annotazioni dalla Value proprietà per dedurre il relativo valore Nullbility predefinito.
Risoluzione: La proposta è approvata
[Risolto] Corrispondenza dell'unione per Nullable di un tipo di valore unione
Quando il valore in ingresso di un criterio è di un tipo di unione, il contenuto del valore di unione può essere "decombattuto", a seconda del modello.
È necessario espandere questa regola agli scenari in cui il valore in ingresso di un modello è di ?Nullable<union type>
Si consideri lo scenario seguente:
static bool Test1(StructUnion? u)
{
return u is 1;
}
static bool Test2(ClassUnion? u)
{
return u is 1;
}
Il significato di u is 1 in Test1 e Test2 è molto diverso. In Test1 non corrisponde a un'unione, in Test2 è.
Forse "corrispondenza unione" dovrebbe "scavare" attraverso Nullable<T> come criteri di ricerca in genere fa in altre situazioni.
Se si procede con questo, il modello di corrispondenza null dell'unione rispetto Nullable<union type> a dovrebbe funzionare come rispetto alle classi.
Vale a dire che il modello è true quando (!nullableValue.HasValue || nullableValue.Value.Value is null).
Risoluzione: La proposta è approvata.
Cosa fare sulle API "non dannose"?
Cosa deve fare il compilatore sulle API di corrispondenza dell'unione che hanno un aspetto simile a una corrispondenza, ma altrimenti "non valido"? Ad esempio, il compilatore trova TryGetValue/HasValue con firma corrispondente, ma è "non valido" perché un modificatore personalizzato obbligatorio o richiede una funzionalità sconosciuta e così via. Il compilatore deve ignorare automaticamente l'API o segnalare un errore? Analogamente, l'API potrebbe essere contrassegnata come obsoleta/sperimentale. Il compilatore deve segnalare eventuali dati di diagnostica, usare automaticamente l'API o non usare automaticamente l'API?
Cosa accade se mancano i tipi per la dichiarazione di unione
Cosa accade se UnionAttribute, IUnion o IUnion<TUnion> se mancano? Errore? Sintetizzare? Qualcos'altro?
[Risolto] Progettazione dell'interfaccia IUnion generica
Sono stati effettuati argomenti che IUnion<TUnion> non devono ereditare o IUnion vincolare il parametro di tipo a IUnion<TUnion>. Dovremmo rivedere.
Risoluzione: L'interfaccia IUnion<TUnion> viene rimossa per il momento.
[Risolto] Tipi valore nullable come tipi di maiuscole e minuscole e l'interazione con TryGetValue
Le regole precedenti dichiarano che se un tipo case è un tipo valore nullable, il tipo di parametro usato in un metodo corrispondente TryGetValue deve essere il tipo sottostante .
Ciò è motivato dal fatto che un null valore non verrebbe mai restituito tramite questo metodo. Sul lato consumo, un tipo valore nullable non è consentito come modello di tipo, mentre una corrispondenza con il tipo sottostante deve essere in grado di eseguire il mapping a una chiamata di questo metodo.
Dovremmo confermare che siamo d'accordo con questo annullamento del wrapping.
Risoluzione: Concordato/confermato
Modello di accesso di unione non boxing
È necessario specificare regole precise per trovare le API e TryGetValue appropriateHasValue.
L'ereditarietà è coinvolta? La corrispondenza di lettura/scrittura HasValue è accettabile? And so on.
[Risolto] TryGetValue conversioni corrispondenti
La sezione Union Matching (Unione corrispondente) indica:
Per un modello che implica il controllo di un tipo
Tspecifico, se è disponibile unTryGetValue(S value)metodo e viene eseguita una conversione implicita daTaS, tale metodo viene usato per ottenere il valore.
Il set di conversioni implicite è limitato in alcun modo? Ad esempio, le conversioni definite dall'utente sono consentite? Che ne dici delle conversioni di tuple e di altre conversioni non così semplici? Alcune di queste sono anche conversioni standard.
Il set di TryGetValue metodi è limitato in altro modo? Ad esempio, la sezione Union Patterns implica che vengono considerati solo i metodi con un tipo di parametro corrispondente a un tipo di case:
metodo
public bool TryGetValue(out T value)per ogni tipo diTcase.
Sarebbe opportuno avere una risposta esplicita.
Risoluzione: Vengono considerate solo le conversioni implicite di identità, riferimento o riferimento o conversione boxing
TryGetValue e l'analisi nullable
Quando il modello di
HasValueaccesso non boxing oTryGetValue(...)viene usato per eseguire query sul contenuto di un tipo di unione (in modo esplicito o tramite criteri di ricerca), influisce sulloValuestato di nullità nello stesso modo in cui seValuefosse stato selezionato direttamente: lo stato Null diValuediventa "not null" neltrueramo.
Il set di TryGetValue metodi è limitato in qualche modo? Ad esempio, la sezione Union Patterns implica che vengono considerati solo i metodi con un tipo di parametro corrispondente a un tipo di case:
metodo
public bool TryGetValue(out T value)per ogni tipo diTcase.
Sarebbe opportuno avere una risposta esplicita.
Chiarire le regole relative ai default valori dei tipi di unione struct
Nota: la regola di nullità predefinita indicata di seguito è stata rimossa.
Nota: sono state rimosse le regole di correttezza "predefinite" indicate di seguito. Dobbiamo confermare che questo è ciò che vogliamo.
La sezione Nullability dice:
Per i tipi di unione in cui nessuno dei tipi case è nullable, lo stato predefinito per
Valueè "not null" anziché "forse null".
Dato che, per l'esempio seguente, l'implementazione corrente considera Values2 "not null":
S2 s2 = default;
struct S2 : System.Runtime.CompilerServices.IUnion
{
public S2(int x) => throw null!;
public S2(bool x) => throw null!;
object? System.Runtime.CompilerServices.IUnion.Value => throw null!;
}
Allo stesso tempo, la sezione Well-formedness dice:
- Valore predefinito: se un tipo di unione è un tipo valore, il valore predefinito è
null.Value- Costruttore predefinito: se un tipo di unione ha un costruttore nullry (no-argument), l'unione risultante ha
nullcome .Value
Un'implementazione di questo tipo sarà in contraddizione con il comportamento di analisi nullable per l'esempio precedente.
Le regole di correttezza devono essere regolate o devono Valuedefault essere "forse null"?
Se quest'ultimo, l'inizializzazione S2 s2 = default; dovrebbe generare un avviso di nullità?
Verificare che un parametro di tipo non sia mai un tipo di unione, anche se vincolato a uno.
class C1 : System.Runtime.CompilerServices.IUnion
{
private readonly object _value;
public C1(int x) { _value = x; }
public C1(string x) { _value = x; }
object System.Runtime.CompilerServices.IUnion.Value => _value;
}
class Program
{
static bool Test1<T>(T u) where T : C1
{
return u is int; // Not a union matching
}
static bool Test2<T>(T u) where T : C1
{
return u is string; // Not a union matching
}
}
Gli attributi post-condizione devono influire sull'impostazione predefinita dei valori Null di un'istanza di Union?
Nota: la regola di nullità predefinita indicata di seguito è stata rimossa. E non si deduce più il valore Nullbility predefinito della proprietà dai metodi di Value creazione dell'unione. Pertanto, la domanda è obsoleta/non più applicabile alla progettazione corrente.
Per i tipi di unione in cui nessuno dei tipi case è nullable, lo stato predefinito per
Valueè "not null" anziché "forse null".
Avviso previsto nello scenario seguente
#nullable enable
struct S1 : System.Runtime.CompilerServices.IUnion
{
public S1(int x) => throw null!;
public S1([System.Diagnostics.CodeAnalysis.NotNull] bool? x) => throw null!;
object? System.Runtime.CompilerServices.IUnion.Value => throw null!;
}
class Program
{
static void Test2(S1 s)
{
// warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive).
// For example, the pattern 'null' is not covered.
_ = s switch { int => 1, bool => 3 }; //
}
}
Conversioni di unioni
[Risolto] Dove appartengono tra le altre conversioni prioritarie?
Le conversioni di unione si sentono come un'altra forma di conversione definita dall'utente. Pertanto, l'implementazione corrente li classifica subito dopo un tentativo non riuscito di classificare una conversione implicita definita dall'utente e, in caso di esistenza, viene considerata come un'altra forma di conversione definita dall'utente. Ciò ha le conseguenze seguenti:
- Una conversione implicita definita dall'utente ha la priorità rispetto a una conversione unione
- Quando il cast esplicito viene usato nel codice, una conversione esplicita definita dall'utente assume la priorità rispetto a una conversione unione
- Quando non è presente alcun cast esplicito nel codice, una conversione dell'unione ha la priorità su una conversione esplicita definita dall'utente
struct S1 : System.Runtime.CompilerServices.IUnion
{
public S1(int x) => ...
public S1(string x) => ...
object System.Runtime.CompilerServices.IUnion.Value => ...
public static implicit operator S1(int x) => ...
}
struct S2 : System.Runtime.CompilerServices.IUnion
{
public S2(int x) => ...
public S2(string x) => ...
object System.Runtime.CompilerServices.IUnion.Value => ...
public static explicit operator S2(int x) => ...
}
class Program
{
static S1 Test1() => 10; // implicit operator S1(int x) is used
static S1 Test2() => (S1)20; // implicit operator S1(int x) is used
static S2 Test3() => 10; // Union conversion S2.S2(int) is used
static S2 Test4() => (S2)20; // explicit operator S2(int x)
}
È necessario verificare che questo sia il comportamento desiderato. In caso contrario, è necessario chiarire le regole di conversione.
Risoluzione:
Approvato dal gruppo di lavoro.
[Risolto] Ref-ness del parametro del costruttore
Attualmente il linguaggio consente solo per valore e in parametri per gli operatori di conversione definiti dall'utente.
Si ritiene che i motivi di questa restrizione siano applicabili anche ai costruttori adatti per le conversioni di unioni.
Proposta:
Modificare la definizione di un oggetto case type constructor nella Union types sezione precedente:
-For each public constructor with exactly one parameter, the type of that parameter is considered a *case type* of the union type.
+For each public constructor with exactly one **by-value or `in`** parameter, the type of that parameter is considered a *case type* of the union type.
Risoluzione:
Approvato dal gruppo di lavoro per il momento. Tuttavia, è possibile considerare la "suddivisione" del set di costruttori di tipi case e il set di costruttori adatti per le conversioni dei tipi di unione.
[Risolto] Conversioni nullable
La sezione Conversioni nullable elenca in modo esplicito le conversioni che possono essere usate come sottostanti. La specifica corrente non propone modifiche a tale elenco. Questo risultato è un errore per lo scenario seguente:
struct S1 : System.Runtime.CompilerServices.IUnion
{
public S1(int x) => throw null;
public S1(string x) => throw null;
object System.Runtime.CompilerServices.IUnion.Value => throw null;
}
class Program
{
static S1? Test1(int x)
{
return x; // error CS0029: Cannot implicitly convert type 'int' to 'S1?'
}
}
Proposta:
Modificare la specifica per supportare una conversione implicita nullable da S a T? supportata da una conversione unione.
In particolare, supponendo T che sia un tipo di unione esiste una conversione implicita in un tipo T? da un tipo o un'espressione E se è presente una conversione unione da E a un tipo C ed C è un tipo case di T.
Si noti che non esiste alcun requisito per il tipo di E un tipo di valore non nullable.
La conversione viene valutata come conversione unione sottostante da S a T seguita da un wrapping da T a T?
Risoluzione:
Approvato.
[Risolto] Conversioni lifted
Si vuole modificare la sezione Conversioni lifted per supportare le conversioni di unioni lifted? Attualmente non sono consentiti:
struct S1 : System.Runtime.CompilerServices.IUnion
{
public S1(int x) => throw null;
public S1(string x) => throw null;
object System.Runtime.CompilerServices.IUnion.Value => throw null;
}
class Program
{
static S1 Test1(int? x)
{
return x; // error CS0029: Cannot implicitly convert type 'int?' to 'S1'
}
static S1? Test2(int? y)
{
return y; // error CS0029: Cannot implicitly convert type 'int?' to 'S1?'
}
}
Risoluzione:
Nessuna conversione di unioni revocate per il momento. Alcune note della discussione:
L'analogia con le conversioni definite dall'utente si suddivide leggermente qui. In generale, le unioni sono in grado di contenere un valore Null incluso. Non è chiaro se il lifting deve creare un'istanza di un tipo di unione con
nullvalore archiviato in esso o se deve creare unnullvalore diNullable<Union>.
[Risolto] Bloccare la conversione dell'unione da un'istanza di un tipo di base?
Si potrebbe trovare il comportamento corrente confusione:
struct S1 : System.Runtime.CompilerServices.IUnion
{
public S1(System.ValueType x)
{
}
public S1(string x) => throw null;
object System.Runtime.CompilerServices.IUnion.Value => throw null;
}
class Program
{
static S1 Test1(System.ValueType x)
{
return x; // Union conversion
}
static S1 Test2(System.ValueType y)
{
return (S1)y; // Unboxing conversion
}
}
Si noti che il linguaggio non consente in modo esplicito di dichiarare conversioni definite dall'utente da un tipo di base. Pertanto, potrebbe fare ence per non consentire conversioni di unioni come questa.
Risoluzione:
Non fare niente di speciale per il momento. Gli scenari generici non possono comunque essere completamente protetti.
[Risolto] Blocca la conversione dell'unione da un'istanza di un tipo di interfaccia?
Si potrebbe trovare il comportamento corrente confusione:
struct S1 : I1, System.Runtime.CompilerServices.IUnion
{
public S1(I1 x) => throw null;
public S1(string x) => throw null;
object System.Runtime.CompilerServices.IUnion.Value => throw null;
}
interface I1 { }
struct S2 : System.Runtime.CompilerServices.IUnion
{
public S2(I1 x) => throw null;
public S2(string x) => throw null;
object System.Runtime.CompilerServices.IUnion.Value => throw null;
}
class C3 : System.Runtime.CompilerServices.IUnion
{
public C3(I1 x) => throw null;
public C3(string x) => throw null;
object System.Runtime.CompilerServices.IUnion.Value => throw null;
}
class Program
{
static S1 Test1(I1 x)
{
return x; // Union conversion
}
static S1 Test2(I1 x)
{
return (S1)x; // Unboxing
}
static S2 Test3(I1 x)
{
return x; // Union conversion
}
static S2 Test4(I1 x)
{
return (S2)x; // Union conversion
}
static C3 Test3(I1 x)
{
return x; // Union conversion
}
static C3 Test4(I1 x)
{
return (C3)x; // Reference conversion
}
}
Si noti che il linguaggio non consente in modo esplicito di dichiarare conversioni definite dall'utente da un tipo di base. Pertanto, potrebbe fare ence per non consentire conversioni di unioni come questa.
Risoluzione:
Non fare niente di speciale per il momento. Gli scenari generici non possono comunque essere completamente protetti.
Spazio dei nomi dell'interfaccia IUnion
Lo spazio dei nomi contenitore per IUnion l'interfaccia rimane non specificato. Se la finalità consiste nel conservarla in uno global spazio dei nomi, in questo caso viene specificato in modo esplicito.
Proposta: se si tratta di qualcosa semplicemente trascurato, è possibile usare lo System.Runtime.CompilerServices spazio dei nomi.
Classi come Union tipi
[Risolto] Controllo dell'istanza stessa per null
Se un tipo di unione è un tipo di classe, il valore potrebbe essere Null. Che ne dici dei controlli Null?
Il null modello è stato scelto esplicitamente per controllare la Value proprietà, quindi come si verifica che l'unione stessa non sia null?
Per esempio:
- Quando
Sè unoUnionstruct,s is nullper un valore diS?ètruesolo quandosènull. QuandoCè unaUnionclasse ,c is nullper un valore diC?èfalsenullquandocè , ma ètruequandocnonnullè ec.Valueènull.
Un altro esempio:
class C1 : IUnion
{
private readonly object? _value;
public C1(){}
public C1(int x) { _value = x; }
public C1(string x) { _value = x; }
object? IUnion.Value => _value;
}
class Program
{
static int Test1(C1? u)
{
// warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive).
// For example, the pattern 'null' is not covered.
// This is very confusing, the switch expression is indeed not exhaustive (u itself is not
// checked for null), but there is a case 'null => 3' in the switch expression.
// It looks like the only way to shut off the warning is to use 'case _'. Adding it removes
// all benefits of exhaustiveness checking, any union case could be missing and there would
// be no diagnostic about that.
return u switch { int => 1, string => 2, null => 3 };
}
}
Questa parte della progettazione è chiaramente ottimizzata in base all'aspettativa che un tipo di unione sia uno struct. Alcune opzioni:
- Peccato. Usare
==per il controllo Null invece di una corrispondenza del criterio. - Lasciare che il criterio (e il
nullcontrollo null implicito in altri modelli) si applichino sia al valore di unione che alla relativaValueproprietà:u is null ==> u == null || u.Value == null. - Non consentire alle classi di essere tipi di unione.
[Risolto] Derivazione da una Union classe
Quando una classe usa una Unionclasse come classe base, in base alla specifica corrente, diventa una Unionclasse stessa. Ciò si verifica perché "eredita" automaticamente l'implementazione dell'interfaccia IUnion , non è necessario implementarla nuovamente. Allo stesso tempo, i costruttori del tipo derivato definiscono il set di tipi in questo nuovo Unionoggetto . È molto facile passare a un comportamento del linguaggio molto strano intorno alle due classi:
class C1 : IUnion
{
private readonly object _value;
public C1(long x) { _value = x; }
public C1(string x) { _value = x; }
object IUnion.Value => _value;
}
class C2(int x) : C1(x);
class Program
{
static int Test1(C1 u)
{
// Good
return u switch { long => 1, string => 2, null => 3 };
}
static int Test2(C2 u)
{
// error CS8121: An expression of type 'C2' cannot be handled by a pattern of type 'long'.
// error CS8121: An expression of type 'C2' cannot be handled by a pattern of type 'string'.
return u switch { long => 1, string => 2, null => 3 };
}
}
Alcune opzioni:
Modificare quando un tipo di classe è un
Uniontipo. Ad esempio, una classe è unUniontipo quando tutto true:- Il motivo è
sealedche i tipi derivati non verranno considerati comeUniontipi, consentendo di generare confusione. - Nessuna delle sue basi implementa
IUnion
Questo non è ancora perfetto. Le regole sono troppo sottili. È facile commettere un errore. Non esiste alcuna diagnostica nella dichiarazione, ma
Unionla corrispondenza non funziona.- Il motivo è
Non consentire alle classi di essere tipi di unione.
[Risolto] Operatore di tipo is
L'operatore is-type viene specificato come controllo del tipo di runtime. Sintatticamente sembra molto simile a un modello di tipo, ma non lo è. Di conseguenza, non verrà usata la corrispondenza speciale Union, che potrebbe causare confusione dell'utente.
struct S1 : IUnion
{
private readonly object _value;
public S1(int x) { _value = x; }
public S1(string x) { _value = x; }
object IUnion.Value => _value;
}
class Program
{
static bool Test1(S1 u)
{
return u is int; // warning CS0184: The given expression is never of the provided ('int') type
}
static bool Test2(S1 u)
{
return u is string and ['1', .., '2']; // Good
}
}
In caso di unione ricorsiva, il modello di tipo potrebbe non generare alcun avviso, ma non eseguirà comunque le operazioni che l'utente potrebbe pensare che farebbe.
Risoluzione: Deve funzionare come modello di tipo.
Schema elenco
Il criterio di elenco ha sempre esito negativo con Union corrispondenza:
struct S1 : IUnion
{
private readonly object _value;
public S1(int[] x) { _value = x; }
public S1(string[] x) { _value = x; }
object IUnion.Value => _value;
}
class Program
{
static bool Test1(S1 u)
{
// error CS8985: List patterns may not be used for a value of type 'object'. No suitable 'Length' or 'Count' property was found.
// error CS0021: Cannot apply indexing with [] to an expression of type 'object'
return u is [10];
}
}
static class Extensions
{
extension(object o)
{
public int Length => 0;
}
}
Altre domande
- Sia l'uso dei costruttori nelle conversioni di unione che l'uso di
TryGetValue(...)nella corrispondenza dei criteri di unione vengono specificati per essere minori quando si applicano più costruttori: ne sceglieranno solo uno. Questo non dovrebbe importare in base alle regole di ben formato, ma siamo a nostro agio con esso? - La specifica si basa in modo secondario sull'implementazione della
IUnion.Valueproprietà anziché su qualsiasiValueproprietà trovata nel tipo di unione stessa. Questo è progettato per offrire maggiore flessibilità per i tipi esistenti (che possono avere la propriaValueproprietà per altri usi) per implementare il modello. Ma è imbarazzante e incoerente con il modo in cui altri membri vengono trovati e usati direttamente sul tipo di unione. Dobbiamo apportare una modifica? Altre opzioni:- Richiedere tipi di unione per esporre una proprietà pubblica
Value. - Preferisce una proprietà pubblica
Value, se esistente, ma eseguire il fallback all'implementazioneIUnion.Value, se non (simile alleGetEnumeratorregole).
- Richiedere tipi di unione per esporre una proprietà pubblica
- La sintassi proposta di dichiarazione di unione non è universalmente amata, in particolare quando si tratta di esprimere i tipi di maiuscole e minuscole. Le alternative finora si incontrano anche con le critiche, ma è possibile che si finirà per apportare un cambiamento. Alcune preoccupazioni principali hanno parlato dell'attuale:
- Le virgole come separatori tra i tipi di maiuscole possono sembrare implicare che l'ordine è importante.
- Gli elenchi racchiusi tra parentesi sono troppo simili ai costruttori primari (nonostante non abbiano nomi di parametri).
- Troppo diverso dalle enumerazioni, che hanno i loro "case" tra parentesi graffe.
- Mentre le dichiarazioni di unione generano struct con un singolo campo di riferimento, sono comunque soggetti a comportamenti imprevisti quando vengono usati in un contesto simultaneo. Ad esempio, se un membro di funzione definito dall'utente dereferenzia
thispiù volte, la variabile contenitore potrebbe essere stata riassegnata nel suo complesso da un altro thread tra i due accessi. Il compilatore potrebbe generare codice da copiarethisin un ambiente locale, se necessario. Dovrebbe farlo? In generale, quale grado di resilienza della concorrenza è auspicabile e ragionevolmente raggiungibile?
C# feature specifications