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/8652
Riassunto
Le espressioni di raccolta introducono una nuova sintassi terse, [e1, e2, e3, etc], per creare valori di raccolta comuni. L'inlining di altre raccolte in questi valori è possibile usando un elemento ..e spread come in questo modo: [e1, ..c2, e2, ..c2].
È possibile creare diversi tipi simili a raccolte senza richiedere supporto BCL esterno. Questi tipi sono:
-
Tipi di matrice, ad esempio
int[]. -
Span<T>eReadOnlySpan<T>. - Tipi che supportano gli inizializzatori di raccolta, ad esempio
List<T>.
È disponibile ulteriore supporto per i tipi simili a raccolte non descritti in precedenza tramite un nuovo attributo e un modello API che possono essere adottati direttamente nel tipo stesso.
Motivazione
I valori simili alla raccolta sono enormemente presenti nella programmazione, negli algoritmi e soprattutto nell'ecosistema C#/.NET. Quasi tutti i programmi utilizzeranno questi valori per archiviare i dati e inviare o ricevere dati da altri componenti. Attualmente, quasi tutti i programmi C# devono usare molti approcci diversi e purtroppo dettagliato per creare istanze di tali valori. Alcuni approcci presentano anche svantaggi delle prestazioni. Ecco alcuni esempi comuni:
- Matrici, che richiedono
new Type[]onew[]prima dei{ ... }valori. - Span, che possono usare
stackalloce altri costrutti complessi. - Inizializzatori di raccolta, che richiedono una sintassi simile
new List<T>(senza inferenza di un possibile dettagliatoT) prima dei valori e che possono causare più riallocazioni della memoria perché usano.AddN chiamate senza fornire una capacità iniziale. - Raccolte non modificabili, che richiedono sintassi come
ImmutableArray.Create(...)inizializzare i valori e che possono causare allocazioni intermedie e copia dei dati. Le forme di costruzione più efficienti (ad esempioImmutableArray.CreateBuilder) non sono complesse e producono ancora una spazzatura inevitabile.
- Matrici, che richiedono
Esaminando l'ecosistema circostante, troviamo anche esempi ovunque la creazione di elenchi sia più conveniente e piacevole da usare. TypeScript, Dart, Swift, Elm, Python e altro optano per una sintassi concisa a questo scopo, con un utilizzo diffuso e per un grande effetto. Le indagini cursori hanno rivelato problemi sostanziali che si verificano in tali ecosistemi con l'inserimento di questi valori letterali.
C# ha anche aggiunto modelli di elenco in C# 11. Questo modello consente la corrispondenza e la decostruzione di valori simili a un elenco usando una sintassi pulita e intuitiva. Tuttavia, a differenza di quasi tutti gli altri costrutti di pattern, questa sintassi di corrispondenza/decostruzione non dispone della sintassi di costruzione corrispondente.
Ottenere prestazioni ottimali per costruire ogni tipo di raccolta può essere difficile. Soluzioni semplici spesso sprecano cpu e memoria. La presenza di un formato letterale consente la massima flessibilità dell'implementazione del compilatore per ottimizzare il valore letterale per produrre almeno un risultato ottimale come un utente può fornire, ma con codice semplice. Molto spesso il compilatore sarà in grado di fare meglio e la specifica mira a consentire l'implementazione di grandi quantità di spazio in termini di strategia di implementazione per garantire questo risultato.
È necessaria una soluzione inclusiva per C#. Dovrebbe soddisfare la maggior parte delle casse per i clienti in termini di tipi e valori simili alla raccolta già presenti. Dovrebbe anche essere naturale nel linguaggio e rispecchiare il lavoro svolto in criteri di ricerca.
Ciò porta a una conclusione naturale che la sintassi deve essere simile [e1, e2, e3, e-etc] a o [e1, ..c2, e2], che corrispondono agli equivalenti del criterio di [p1, p2, p3, p-etc] e [p1, ..p2, p3].
Progettazione dettagliata
Vengono aggiunte le produzioni grammaticali seguenti:
primary_no_array_creation_expression
...
+ | collection_expression
;
+ collection_expression
: '[' ']'
| '[' collection_element ( ',' collection_element )* ']'
;
+ collection_element
: expression_element
| spread_element
;
+ expression_element
: expression
;
+ spread_element
: '..' expression
;
I valori letterali della raccolta sono tipizzato come destinazione.
Chiarimenti sulle specifiche
Per brevità,
collection_expressionverrà definito "letterale" nelle sezioni seguenti.expression_elementLe istanze vengono comunemente definitee1, ee_ncosì via.spread_elementLe istanze vengono comunemente definite..s1, e..s_ncosì via.il tipo span indica
Span<T>oReadOnlySpan<T>.I valori letterali vengono comunemente visualizzati per
[e1, ..s1, e2, ..s2, etc]indicare qualsiasi numero di elementi in qualsiasi ordine. Importante, questo modulo verrà usato per rappresentare tutti i casi, ad esempio:- Valori letterali vuoti
[] - Valori letterali senza alcun
expression_elementvalore in essi contenuti. - Valori letterali senza alcun
spread_elementvalore in essi contenuti. - Valori letterali con ordinamento arbitrario di qualsiasi tipo di elemento.
- Valori letterali vuoti
Il tipo di iterazione di
..s_nè il tipo della variabile di iterazione determinata come ses_nfosse stata usata come espressione sottoposta a iterazione in un oggettoforeach_statement.Le variabili che iniziano con
__namevengono usate per rappresentare i risultati della valutazione diname, archiviati in una posizione in modo che vengano valutate una sola volta. Ad esempio__e1, è la valutazione die1.List<T>,IEnumerable<T>e così via fanno riferimento ai rispettivi tipi nello spazio deiSystem.Collections.Genericnomi .La specifica definisce una conversione del valore letterale in costrutti C# esistenti. Analogamente alla traduzione dell'espressione di query, il valore letterale è legale solo se la traduzione genera codice legale. Lo scopo di questa regola è evitare di dover ripetere altre regole del linguaggio implicite( ad esempio, sulla convertibilità delle espressioni quando vengono assegnate alle posizioni di archiviazione).
Un'implementazione non è necessaria per tradurre i valori letterali esattamente come specificato di seguito. Qualsiasi traduzione è legale se viene prodotto lo stesso risultato e non vi sono differenze osservabili nella produzione del risultato.
- Ad esempio, un'implementazione potrebbe convertire i valori letterali direttamente
[1, 2, 3]in un'espressionenew int[] { 1, 2, 3 }che esegue il bake dei dati non elaborati nell'assembly, slidando la necessità__indexdi o una sequenza di istruzioni per assegnare ogni valore. Importante, ciò significa che se un passaggio della traduzione potrebbe causare un'eccezione in fase di esecuzione che lo stato del programma è ancora lasciato nello stato indicato dalla traduzione.
- Ad esempio, un'implementazione potrebbe convertire i valori letterali direttamente
I riferimenti all'allocazione dello stack fanno riferimento a qualsiasi strategia da allocare nello stack e non all'heap. Importante, non implica o richiede che tale strategia sia attraverso il meccanismo effettivo
stackalloc. Ad esempio, l'uso di matrici inline è anche un approccio consentito e auspicabile per eseguire l'allocazione dello stack, se disponibile. Si noti che in C# 12 non è possibile inizializzare matrici inline con un'espressione di raccolta. Ciò rimane una proposta aperta.Si presuppone che le raccolte siano ben comportate. Per esempio:
- Si presuppone che il valore di
Countin una raccolta producano lo stesso valore del numero di elementi durante l'enumerazione. - I tipi usati in questa specifica definita nello spazio dei
System.Collections.Genericnomi sono considerati privi di effetti collaterali. Di conseguenza, il compilatore può ottimizzare gli scenari in cui tali tipi possono essere usati come valori intermedi, ma altrimenti non vengono esposti. - Si presuppone che una chiamata a un membro applicabile
.AddRange(x)in una raccolta comporti lo stesso valore finale dell'iterazionexe l'aggiunta di tutti i relativi valori enumerati singolarmente alla raccolta con.Add. - Il comportamento dei valori letterali della raccolta con raccolte non ben comportate è indefinito.
- Si presuppone che il valore di
Conversions
Una conversione di espressioni di raccolta consente la conversione di un'espressione di raccolta in un tipo.
Esiste una conversione implicita dell'espressione di raccolta da un'espressione di raccolta ai tipi seguenti:
-
Tipo di
T[]matrice unidimensionale, nel qual caso il tipo di elemento èT -
Tipo span:
System.Span<T>System.ReadOnlySpan<T>
Nel qual caso il tipo di elemento èT
-
Tipo con un metodo di creazione appropriato, nel qual caso il tipo di elemento è il tipo di iterazione determinato da un
GetEnumeratormetodo di istanza o un'interfaccia enumerabile, non da un metodo di estensione -
Uno struct o un tipo di classe che implementa dove
System.Collections.IEnumerable:Il tipo ha un costruttore applicabile che può essere richiamato senza argomenti e il costruttore è accessibile nella posizione dell'espressione di raccolta.
Se l'espressione di raccolta contiene elementi, il tipo dispone di un'istanza o di un metodo
Adddi estensione in cui:- Il metodo può essere richiamato con un singolo argomento di valore.
- Se il metodo è generico, gli argomenti di tipo possono essere dedotti dalla raccolta e dall'argomento .
- Il metodo è accessibile nella posizione dell'espressione di raccolta.
Nel qual caso il tipo di elemento è il tipo di iterazione del tipo .
- Tipo di interfaccia:
System.Collections.Generic.IEnumerable<T>System.Collections.Generic.IReadOnlyCollection<T>System.Collections.Generic.IReadOnlyList<T>System.Collections.Generic.ICollection<T>System.Collections.Generic.IList<T>
Nel qual caso il tipo di elemento èT
La conversione implicita esiste se il tipo ha un tipo diT elemento in cui per ogni elementoEᵢ nell'espressione di raccolta:
- Se
Eᵢè un elemento di espressione, è presente una conversione implicita daEᵢaT. - Se
Eᵢè un elemento..Sᵢspread, è presente una conversione implicita dal tipo di iterazione diSᵢaT.
Non esiste alcuna conversione di espressioni di raccolta da un'espressione di raccolta a un tipo di matrice multidimensionale.
I tipi per i quali è presente una conversione implicita di espressioni di raccolta da un'espressione di raccolta sono i tipi di destinazione validi per tale espressione di raccolta.
Esistono le conversioni implicite aggiuntive seguenti da un'espressione di raccolta:
In un tipo valore
T?nullable in cui è presente una conversione dell'espressione di raccolta dall'espressione di raccolta a un tipo valoreT. La conversione è una conversione di espressioni di raccolta inTseguita da una conversione implicita nullable daTaT?.Per un tipo di
Triferimento in cui è presente un metodo create associatoTa che restituisce un tipoUe una conversione implicita di riferimento daUa .TLa conversione è una conversione di espressioni di raccolta inUseguita da una conversione di riferimento implicita daUaT.Per un tipo di
Iinterfaccia in cui è presente un metodo create associato aIche restituisce un tipoVe una conversione boxing implicita daVaI. La conversione è una conversione di espressioni di raccolta inVseguita da una conversione boxing implicita daVaI.
Creare metodi
Un metodo create è indicato con un [CollectionBuilder(...)] attributo sul tipo di raccolta.
L'attributo specifica il tipo di generatore e il nome del metodo di un metodo da richiamare per costruire un'istanza del tipo di raccolta.
namespace System.Runtime.CompilerServices
{
[AttributeUsage(
AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface,
Inherited = false,
AllowMultiple = false)]
public sealed class CollectionBuilderAttribute : System.Attribute
{
public CollectionBuilderAttribute(Type builderType, string methodName);
public Type BuilderType { get; }
public string MethodName { get; }
}
}
L'attributo può essere applicato a un classoggetto , struct, ref structo interface.
L'attributo non viene ereditato anche se l'attributo può essere applicato a una base class o a un oggetto abstract class.
Il tipo di generatore deve essere un tipo non generico class o struct.
In primo luogo, viene determinato il set di metodiCM di creazione applicabili.
È costituito da metodi che soddisfano i requisiti seguenti:
- Il metodo deve avere il nome specificato nell'attributo
[CollectionBuilder(...)]. - Il metodo deve essere definito direttamente nel tipo di generatore .
- Il metodo deve essere
static. - Il metodo deve essere accessibile in cui viene utilizzata l'espressione di raccolta.
- L'arità del metodo deve corrispondere all'arità del tipo di raccolta.
- Il metodo deve avere un singolo parametro di tipo
System.ReadOnlySpan<E>, passato per valore. - Esiste una conversione di identità, una conversione di riferimento implicita o una conversione boxing dal tipo restituito dal metodo al tipo di raccolta.
I metodi dichiarati su tipi o interfacce di base vengono ignorati e non fanno parte del CM set.
Se il CM set è vuoto, il tipo di raccolta non ha un tipo di elemento e non ha un metodo di creazione. Nessuno dei passaggi seguenti si applica.
Se un solo metodo tra quelli nel CM set ha una conversione identity da E al tipo di elemento del tipo di raccolta, ovvero il metodo create per il tipo di raccolta. In caso contrario, il tipo di raccolta non dispone di un metodo create.
Se l'attributo [CollectionBuilder] non fa riferimento a un metodo richiamabile con la firma prevista, viene segnalato un errore.
Per un'espressione di raccolta con un tipo di destinazione in cui la C<S0, S1, …> di C<T0, T1, …> tipo ha un metodoB.M<U0, U1, …>() builder associato, gli argomenti di tipo generico del tipo di destinazione vengono applicati in ordine , e dal tipo contenitore più esterno all'interno, al metodo builder.
Il parametro span per il metodo create può essere contrassegnato scoped in modo esplicito o [UnscopedRef]. Se il parametro è implicito o esplicito scoped, il compilatore può allocare lo spazio di archiviazione per l'intervallo nello stack anziché l'heap.
Ad esempio, un possibile metodo di creazione per ImmutableArray<T>:
[CollectionBuilder(typeof(ImmutableArray), "Create")]
public struct ImmutableArray<T> { ... }
public static class ImmutableArray
{
public static ImmutableArray<T> Create<T>(ReadOnlySpan<T> items) { ... }
}
Con il metodo create precedente, ImmutableArray<int> ia = [1, 2, 3]; può essere generato come:
[InlineArray(3)] struct __InlineArray3<T> { private T _element0; }
Span<int> __tmp = new __InlineArray3<int>();
__tmp[0] = 1;
__tmp[1] = 2;
__tmp[2] = 3;
ImmutableArray<int> ia =
ImmutableArray.Create((ReadOnlySpan<int>)__tmp);
Costruzione
Gli elementi di un'espressione di raccolta vengono valutati in ordine, da sinistra a destra. Ogni elemento viene valutato esattamente una volta e tutti gli altri riferimenti agli elementi fanno riferimento ai risultati di questa valutazione iniziale.
Un elemento spread può essere iterato prima o dopo la valutazione degli elementi successivi nell'espressione di raccolta.
Un'eccezione non gestita generata da uno dei metodi utilizzati durante la costruzione non verrà rilevata e impedirà ulteriori passaggi nella costruzione.
Length, Counte GetEnumerator si presuppone che non abbiano effetti collaterali.
Se il tipo di destinazione è uno struct o un tipo di classe che implementa System.Collections.IEnumerablee il tipo di destinazione non dispone di un metodo create, la costruzione dell'istanza della raccolta è la seguente:
Gli elementi vengono valutati in ordine. Alcuni o tutti gli elementi possono essere valutati durante i passaggi seguenti anziché prima.
Il compilatore può determinare la lunghezza nota dell'espressione di raccolta richiamando proprietà conteggiabili , o proprietà equivalenti da interfacce o tipi noti, in ogni espressione di elemento spread.
Viene richiamato il costruttore applicabile senza argomenti.
Per ogni elemento nell'ordine:
- Se l'elemento è un elemento di espressione, l'istanza o il metodo di estensione applicabile
Addviene richiamato con l'espressione dell'elemento come argomento. A differenza del comportamento dell'inizializzatore di raccolte classiche, la valutazione degli elementi eAddle chiamate non sono necessariamente interleaved. - Se l'elemento è un elemento spread , viene usato uno dei seguenti elementi:
- Un metodo di estensione o istanza applicabile
GetEnumeratorviene richiamato sull'espressione dell'elemento spread e per ogni elemento dell'enumeratore viene richiamato l'istanza o il metodo di estensione applicabileAddsull'istanza della raccolta con l'elemento come argomento. Se l'enumeratore implementaIDisposable,Disposeverrà chiamato dopo l'enumerazione, indipendentemente dalle eccezioni. - Un'istanza o un metodo di estensione applicabile
AddRangeviene richiamato nell'istanza della raccolta con l'espressione dell'elemento spread come argomento. - Un'istanza o un metodo di estensione applicabile
CopyToviene richiamato sull'espressione dell'elemento spread con l'istanza della raccolta eintl'indice come argomenti.
- Un metodo di estensione o istanza applicabile
- Se l'elemento è un elemento di espressione, l'istanza o il metodo di estensione applicabile
Durante i passaggi di costruzione precedenti, è
EnsureCapacityrichiamare un'istanza o un metodo di estensione applicabile una o più volte nell'istanza di raccolta con unintargomento di capacità.
Se il tipo di destinazione è una matrice, un intervallo, un tipo con un metodo create o un'interfaccia, la costruzione dell'istanza della raccolta è la seguente:
Gli elementi vengono valutati in ordine. Alcuni o tutti gli elementi possono essere valutati durante i passaggi seguenti anziché prima.
Il compilatore può determinare la lunghezza nota dell'espressione di raccolta richiamando proprietà conteggiabili , o proprietà equivalenti da interfacce o tipi noti, in ogni espressione di elemento spread.
Viene creata un'istanza di inizializzazione come segue:
- Se il tipo di destinazione è una matrice e l'espressione di raccolta ha una lunghezza nota, viene allocata una matrice con la lunghezza prevista.
- Se il tipo di destinazione è un intervallo o un tipo con un metodo create e la raccolta ha una lunghezza nota, viene creato un intervallo con la lunghezza prevista che fa riferimento all'archiviazione contigua.
- In caso contrario, viene allocata l'archiviazione intermedia.
Per ogni elemento nell'ordine:
- Se l'elemento è un elemento di espressione, viene richiamato l'indicizzatore dell'istanza di inizializzazione per aggiungere l'espressione valutata in corrispondenza dell'indice corrente.
- Se l'elemento è un elemento spread , viene usato uno dei seguenti elementi:
- Un membro di un'interfaccia o di un tipo noto viene richiamato per copiare elementi dall'espressione dell'elemento spread all'istanza di inizializzazione.
- Un'istanza o un metodo di estensione applicabile
GetEnumeratorviene richiamato sull'espressione dell'elemento spread e per ogni elemento dell'enumeratore viene richiamato l'indicizzatore dell'istanza di inizializzazione per aggiungere l'elemento in corrispondenza dell'indice corrente. Se l'enumeratore implementaIDisposable,Disposeverrà chiamato dopo l'enumerazione, indipendentemente dalle eccezioni. - Un'istanza o un metodo di estensione applicabile
CopyToviene richiamato sull'espressione dell'elemento spread con l'istanza di inizializzazione eintl'indice come argomenti.
Se l'archiviazione intermedia è stata allocata per la raccolta, un'istanza di raccolta viene allocata con la lunghezza effettiva della raccolta e i valori dell'istanza di inizializzazione vengono copiati nell'istanza della raccolta oppure se è necessario un intervallo, il compilatore può usare un intervallo della lunghezza effettiva della raccolta dall'archiviazione intermedia. In caso contrario, l'istanza di inizializzazione è l'istanza della raccolta.
Se il tipo di destinazione ha un metodo create, il metodo create viene richiamato con l'istanza span.
Nota: Il compilatore può ritardare l'aggiunta di elementi alla raccolta o ritardare l'iterazione degli elementi distribuiti fino a quando non vengono valutati gli elementi successivi. Quando gli elementi di dispersione successivi hanno proprietà conteggiabili che consentono di calcolare la lunghezza prevista della raccolta prima di allocare la raccolta. Al contrario, il compilatore può aggiungere in modo desideroso elementi alla raccolta e scorrere con entusiasmo gli elementi distribuiti, quando non vi è alcun vantaggio di ritardare.
Si consideri l'espressione di raccolta seguente:
int[] x = [a, ..b, ..c, d];Se gli elementi
bdistribuiti ecsono conteggiabili, il compilatore potrebbe ritardare l'aggiunta di elementi daaebfino a quando non viene valutato,cper consentire l'allocazione della matrice risultante alla lunghezza prevista. Successivamente, il compilatore potrebbe aggiungere gli elementi dac, prima di valutared.var __tmp1 = a; var __tmp2 = b; var __tmp3 = c; var __result = new int[2 + __tmp2.Length + __tmp3.Length]; int __index = 0; __result[__index++] = __tmp1; foreach (var __i in __tmp2) __result[__index++] = __i; foreach (var __i in __tmp3) __result[__index++] = __i; __result[__index++] = d; x = __result;
Valore letterale raccolta vuota
Il valore letterale
[]vuoto non ha alcun tipo. Tuttavia, analogamente al valore letterale null, questo valore letterale può essere convertito in modo implicito in qualsiasi tipo di raccolta costruiscibile .Ad esempio, il codice seguente non è valido perché non esiste alcun tipo di destinazione e non sono coinvolte altre conversioni:
var v = []; // illegalLa distribuzione di un valore letterale vuoto può essere eliminata. Per esempio:
bool b = ... List<int> l = [x, y, .. b ? [1, 2, 3] : []];In questo caso, se
bè false, non è necessario che alcun valore venga effettivamente costruito per l'espressione di raccolta vuota perché verrebbe immediatamente distribuito in valori zero nel valore letterale finale.L'espressione di raccolta vuota può essere un singleton se usato per costruire un valore di raccolta finale che non è modificabile. Per esempio:
// Can be a singleton, like Array.Empty<int>() int[] x = []; // Can be a singleton. Allowed to use Array.Empty<int>(), Enumerable.Empty<int>(), // or any other implementation that can not be mutated. IEnumerable<int> y = []; // Must not be a singleton. Value must be allowed to mutate, and should not mutate // other references elsewhere. List<int> z = [];
Sicurezza di riferimento
Vedere Vincolo di contesto sicuro per le definizioni dei valori del contesto sicuro : declaration-block, function-member e caller-context.
Il contesto sicuro di un'espressione di raccolta è:
Il contesto sicuro di un'espressione
[]di raccolta vuota è il contesto del chiamante.Se il tipo di destinazione è un tipo
System.ReadOnlySpan<T>span edTè uno dei tipiboolprimitivi,sbyte,byteshortushortcharintuintlongulongfloato ,doublee l'espressione di raccolta contiene solo valori costanti, il contesto sicuro dell'espressione di raccolta è il contesto chiamante.Se il tipo di destinazione è un tipo
System.Span<T>span oSystem.ReadOnlySpan<T>, il contesto sicuro dell'espressione di raccolta è il blocco di dichiarazione.Se il tipo di destinazione è un tipo di struct ref con un metodo create, il contesto sicuro dell'espressione di raccolta è il contesto sicuro di una chiamata del metodo create in cui l'espressione di raccolta è l'argomento span del metodo .
In caso contrario, il contesto sicuro dell'espressione di raccolta è il contesto del chiamante.
Un'espressione di raccolta con un contesto sicuro di declaration-block non può eseguire l'escape dell'ambito di inclusione e il compilatore può archiviare la raccolta nello stack anziché nell'heap.
Per consentire a un'espressione di raccolta per un tipo di struct ref di eseguire l'escape del blocco di dichiarazione, potrebbe essere necessario eseguire il cast dell'espressione in un altro tipo.
static ReadOnlySpan<int> AsSpanConstants()
{
return [1, 2, 3]; // ok: span refers to assembly data section
}
static ReadOnlySpan<T> AsSpan2<T>(T x, T y)
{
return [x, y]; // error: span may refer to stack data
}
static ReadOnlySpan<T> AsSpan3<T>(T x, T y, T z)
{
return (T[])[x, y, z]; // ok: span refers to T[] on heap
}
Tipo di inferenza
var a = AsArray([1, 2, 3]); // AsArray<int>(int[])
var b = AsListOfArray([[4, 5], []]); // AsListOfArray<int>(List<int[]>)
static T[] AsArray<T>(T[] arg) => arg;
static List<T[]> AsListOfArray<T>(List<T[]> arg) => arg;
Le regole di inferenza del tipo vengono aggiornate nel modo seguente.
Le regole esistenti per la prima fase vengono estratte in una nuova sezione di inferenza del tipo di input e viene aggiunta una regola all'inferenza del tipo di input e all'inferenza del tipo di output per le espressioni di espressione di raccolta.
11.6.3.2 La prima fase
Per ognuno degli argomenti
Eᵢdel metodo :
- Un'inferenza del tipo di input viene eseguita da
Eᵢaltipo diTᵢparametro corrispondente.Un'inferenza del tipo di input viene eseguita da un'espressione
Ea un tipoTnel modo seguente:
- Se
Eè un'espressione di raccolta con elementiEᵢeTè un tipo con un tipo diTₑelemento oTè un tipo valoreT0?nullable eT0ha un tipo diTₑelemento, quindi per ogniEᵢ:
- Se
Eᵢè un elemento di espressione, viene eseguita un'inferenza del tipo di inputdaEᵢaTₑ.- Se
Eᵢè un elemento spread con un tipo diSᵢiterazione, viene eseguita un'inferenza con limite inferioredaSᵢaTₑ.- [regole esistenti dalla prima fase] ...
Inferenze del tipo di output 11.6.3.7
Un'inferenza del tipo di output viene eseguita da un'espressione
Ea un tipoTnel modo seguente:
- Se
Eè un'espressione di raccolta con elementiEᵢeTè un tipo con un tipo diTₑelemento oTè un tipo valoreT0?nullable eT0ha un tipo diTₑelemento, quindi per ogniEᵢ:
- Se
Eᵢè un elemento di espressione, viene eseguita un'inferenza del tipo di outputdaEᵢaTₑ.- Se
Eᵢè un elemento spread, non viene eseguita alcuna inferenza daEᵢ.- [regole esistenti dalle inferenze del tipo di output] ...
Metodi di estensione
Nessuna modifica alle regole di chiamata al metodo di estensione .
12.8.10.3 Invocazioni di metodi di estensione
Un metodo
Cᵢ.Mₑdi estensione è idoneo se:
- ...
- Esiste un'identità implicita, un riferimento o una conversione boxing da expr al tipo del primo parametro di
Mₑ.
Un'espressione di raccolta non dispone di un tipo naturale, pertanto le conversioni esistenti dal tipo non sono applicabili. Di conseguenza, un'espressione di raccolta non può essere usata direttamente come primo parametro per una chiamata al metodo di estensione.
static class Extensions
{
public static ImmutableArray<T> AsImmutableArray<T>(this ImmutableArray<T> arg) => arg;
}
var x = [1].AsImmutableArray(); // error: collection expression has no target type
var y = [2].AsImmutableArray<int>(); // error: ...
var z = Extensions.AsImmutableArray([3]); // ok
Risoluzione dell'overload
Una migliore conversione dall'espressione viene aggiornata per preferire determinati tipi di destinazione nelle conversioni di espressioni di raccolta.
Nelle regole aggiornate:
- Un span_type è uno dei seguenti:
System.Span<T>-
System.ReadOnlySpan<T>.
- Un array_or_array_interface è uno dei seguenti:
- un tipo di matrice
- uno dei tipi di interfaccia seguenti implementati da un tipo di matrice:
System.Collections.Generic.IEnumerable<T>System.Collections.Generic.IReadOnlyCollection<T>System.Collections.Generic.IReadOnlyList<T>System.Collections.Generic.ICollection<T>System.Collections.Generic.IList<T>
Data una conversione implicita
C₁che esegue la conversione da un'espressioneEa un tipoT₁e una conversione implicitaC₂che converte da un'espressioneEa un tipoT₂,C₁è una conversione migliore rispetto aC₂se si verifica una delle seguenti condizioni:
Eè un'espressione di raccolta e uno dei blocchi seguenti:
T₁è eSystem.ReadOnlySpan<E₁>èT₂System.Span<E₂>e esiste una conversione implicita daE₁aE₂T₁èSystem.ReadOnlySpan<E₁>oSystem.Span<E₁>eT₂è un array_or_array_interface con tipo diE₂elemento e esiste una conversione implicita daE₁aE₂T₁non è un span_type eT₂non è un span_type e esiste una conversione implicita daT₁aT₂Enon è un'espressione di raccolta e uno dei blocchi seguenti:
EcorrispondeT₁esattamente eEnon corrisponde esattamenteT₂Ecorrisponde esattamente a entrambi o nessuno diT₁eT₂e èT₁una destinazione di conversione migliore rispetto aT₂Eè un gruppo di metodi, ...
Esempi di differenze con la risoluzione dell'overload tra gli inizializzatori di matrice e le espressioni di raccolta:
static void Generic<T>(Span<T> value) { }
static void Generic<T>(T[] value) { }
static void SpanDerived(Span<string> value) { }
static void SpanDerived(object[] value) { }
static void ArrayDerived(Span<object> value) { }
static void ArrayDerived(string[] value) { }
// Array initializers
Generic(new[] { "" }); // string[]
SpanDerived(new[] { "" }); // ambiguous
ArrayDerived(new[] { "" }); // string[]
// Collection expressions
Generic([""]); // Span<string>
SpanDerived([""]); // Span<string>
ArrayDerived([""]); // ambiguous
Tipi span
I tipi ReadOnlySpan<T> span e Span<T> sono entrambi tipi di raccolta costruiscibili. Il supporto per tali elementi segue la progettazione per params Span<T>. In particolare, la costruzione di uno di questi intervalli comporterà la creazione di una matrice T[] nello stack se la matrice params è entro limiti (se presenti) impostati dal compilatore. In caso contrario, la matrice verrà allocata nell'heap.
Se il compilatore sceglie di allocare nello stack, non è necessario convertire un valore letterale direttamente in un stackalloc oggetto in quel punto specifico. Ad esempio, dato:
foreach (var x in y)
{
Span<int> span = [a, b, c];
// do things with span
}
Il compilatore può tradurlo usando stackalloc purché il Span significato rimanga invariato e venga mantenuto lo stesso intervallo di sicurezza . Ad esempio, può convertire l'oggetto precedente in:
Span<int> __buffer = stackalloc int[3];
foreach (var x in y)
{
__buffer[0] = a
__buffer[1] = b
__buffer[2] = c;
Span<int> span = __buffer;
// do things with span
}
Il compilatore può anche usare matrici inline, se disponibili, quando si sceglie di allocare nello stack. Si noti che in C# 12 non è possibile inizializzare matrici inline con un'espressione di raccolta. Questa caratteristica è una proposta aperta.
Se il compilatore decide di allocare nell'heap, la traduzione per Span<T> è semplicemente:
T[] __array = [...]; // using existing rules
Span<T> __result = __array;
Conversione letterale raccolta
Un'espressione di raccolta ha una lunghezza nota se il tipo in fase di compilazione di ogni elemento spread nell'espressione di raccolta è conteggiabile.
Conversione dell'interfaccia
Conversione dell'interfaccia non modificabile
Dato un tipo di destinazione che non contiene membri modificanti, vale a dire IEnumerable<T>, IReadOnlyCollection<T>e IReadOnlyList<T>, è necessaria un'implementazione conforme per produrre un valore che implementa tale interfaccia. Se un tipo viene sintetizzato, è consigliabile che il tipo sintetizzato implementi tutte queste interfacce, nonché ICollection<T> e IList<T>, indipendentemente dal tipo di interfaccia di destinazione. Ciò garantisce la massima compatibilità con le librerie esistenti, incluse quelle che introspettino le interfacce implementate da un valore per ottimizzare le prestazioni.
Inoltre, il valore deve implementare le interfacce e ICollection non genericheIList. Ciò consente alle espressioni di raccolta di supportare l'introspezione dinamica in scenari come il data binding.
Un'implementazione conforme è gratuita per:
- Usare un tipo esistente che implementa le interfacce necessarie.
- Eseguire la sintesi di un tipo che implementa le interfacce necessarie.
In entrambi i casi, il tipo usato può implementare un set di interfacce di dimensioni maggiori rispetto a quelle strettamente necessarie.
I tipi sintetizzati sono liberi di adottare qualsiasi strategia che vogliono implementare correttamente le interfacce necessarie. Ad esempio, un tipo sintetizzato potrebbe inline gli elementi direttamente all'interno di se stesso, evitando la necessità di allocazioni di raccolte interne aggiuntive. Un tipo sintetizzato non può anche usare alcuna risorsa di archiviazione, optando per calcolare direttamente i valori. Ad esempio, restituendo index + 1 per [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].
- Il valore deve restituire
truequando viene eseguita una query perICollection<T>.IsReadOnly(se implementato) e non genericoIList.IsReadOnlyeIList.IsFixedSize. In questo modo i consumer possono indicare in modo appropriato che la raccolta non sia modificabile, nonostante l'implementazione delle visualizzazioni modificabili. - Il valore deve generare su qualsiasi chiamata a un metodo di mutazione (ad esempio
IList<T>.Add). Ciò garantisce la sicurezza, impedendo la modifica accidentale di una raccolta non modificabile.
Conversione dell'interfaccia modificabile
Tipo di destinazione specificato che contiene membri modificanti, vale a dire ICollection<T> o IList<T>:
- Il valore deve essere un'istanza di
List<T>.
Traduzione della lunghezza nota
Avere una lunghezza nota consente una costruzione efficiente di un risultato con il potenziale di nessuna copia dei dati e senza spazio di margine di flessibilità non necessario in un risultato.
Non avere una lunghezza nota non impedisce la creazione di alcun risultato. Tuttavia, può comportare costi aggiuntivi di CPU e memoria che producono i dati, quindi passare alla destinazione finale.
Per un valore letterale
[e1, ..s1, etc], la traduzione inizia prima con quanto segue:int __len = count_of_expression_elements + __s1.Count; ... __s_n.Count;Dato un tipo di destinazione per tale valore
Tletterale:Se
Tè di tipoT1[], il valore letterale viene convertito come segue:T1[] __result = new T1[__len]; int __index = 0; __result[__index++] = __e1; foreach (T1 __t in __s1) __result[__index++] = __t; // further assignments of the remaining elementsL'implementazione può usare altri mezzi per popolare la matrice. Ad esempio, l'uso di metodi efficienti di copia bulk, ad
.CopyTo()esempio .Se
Tè di tipoSpan<T1>, il valore letterale viene tradotto come sopra, ad eccezione del fatto che l'inizializzazione__resultviene convertita come segue:Span<T1> __result = new T1[__len]; // same assignments as the array translationLa traduzione può usare
stackalloc T1[]o una matrice inline anzichénew T1[]se viene mantenuta la protezione dell'intervallo .Se
Tè un valore ,ReadOnlySpan<T1>il valore letterale viene convertito come per ilSpan<T1>caso, ad eccezione del fatto che il risultato finale sarà convertitoSpan<T1>in modo implicito in un oggettoReadOnlySpan<T1>.Oggetto
ReadOnlySpan<T1>in cuiT1è un tipo primitivo e tutti gli elementi della raccolta sono costanti non richiede che i relativi dati si trovino nell'heap o nello stack. Ad esempio, un'implementazione potrebbe costruire questo intervallo direttamente come riferimento a una parte del segmento di dati del programma.I moduli precedenti (per matrici e intervalli) sono le rappresentazioni di base dell'espressione di raccolta e vengono usate per le regole di conversione seguenti:
Se
Tè un oggetto cheC<S0, S1, …>ha un metodo create-methodB.M<U0, U1, …>()corrispondente, il valore letterale viene tradotto come:// Collection literal is passed as is as the single B.M<...>(...) argument C<S0, S1, …> __result = B.M<S0, S1, …>([...])Poiché il metodo create deve avere un tipo di argomento di un'istanza
ReadOnlySpan<T>di , la regola di conversione per gli intervalli si applica quando si passa l'espressione di raccolta al metodo create.Se
Tsupporta gli inizializzatori di raccolta, procedere come illustrato di seguito:se il tipo
Tcontiene un costruttore accessibile con un singolo parametroint capacity, il valore letterale viene convertito come:T __result = new T(capacity: __len); __result.Add(__e1); foreach (var __t in __s1) __result.Add(__t); // further additions of the remaining elementsNota: il nome del parametro deve essere
capacity.Questo formato consente a un valore letterale di informare il tipo appena costruito del numero di elementi per consentire un'allocazione efficiente dello spazio di archiviazione interno. In questo modo si evitano riassegnazioni sprecate man mano che vengono aggiunti gli elementi.
in caso contrario, il valore letterale viene tradotto come:
T __result = new T(); __result.Add(__e1); foreach (var __t in __s1) __result.Add(__t); // further additions of the remaining elementsCiò consente di creare il tipo di destinazione, anche se senza ottimizzazione della capacità per impedire la riallocazione interna dell'archiviazione.
Traduzione di lunghezza sconosciuta
Dato un tipo di
Tdestinazione per un valore letterale di lunghezza sconosciuto :Se
Tsupporta gli inizializzatori di raccolta, il valore letterale viene convertito come segue:T __result = new T(); __result.Add(__e1); foreach (var __t in __s1) __result.Add(__t); // further additions of the remaining elementsCiò consente la distribuzione di qualsiasi tipo iterabile, anche se con la minima quantità di ottimizzazione possibile.
Se
Tè di tipoT1[], il valore letterale ha la stessa semantica di:List<T1> __list = [...]; /* initialized using predefined rules */ T1[] __result = __list.ToArray();L'esempio precedente è tuttavia inefficiente; crea l'elenco intermedio e quindi crea una copia della matrice finale. Le implementazioni sono gratuite per ottimizzare questa funzionalità, ad esempio producendo codice simile al seguente:
T1[] __result = <private_details>.CreateArray<T1>( count_of_expression_elements); int __index = 0; <private_details>.Add(ref __result, __index++, __e1); foreach (var __t in __s1) <private_details>.Add(ref __result, __index++, __t); // further additions of the remaining elements <private_details>.Resize(ref __result, __index);Ciò consente di sprecare e copiare minimi, senza sovraccarichi aggiuntivi che potrebbero comportare raccolte di librerie.
I conteggi passati a
CreateArrayvengono usati per fornire un suggerimento iniziale per le dimensioni per evitare il ridimensionamento sprecato.Se
Tè un tipo di intervallo, un'implementazione può seguire la strategia precedenteT[]o qualsiasi altra strategia con la stessa semantica, ma prestazioni migliori. Ad esempio, anziché allocare la matrice come copia degli elementi dell'elenco,CollectionsMarshal.AsSpan(__list)è possibile usare per ottenere direttamente un valore span.
Scenari non supportati
Anche se i valori letterali di raccolta possono essere usati per molti scenari, esistono alcuni elementi che non sono in grado di sostituire. Questi includono:
- Matrici multidimensionali ,ad esempio
new int[5, 10] { ... }. Non esiste alcuna funzionalità per includere le dimensioni e tutti i valori letterali raccolta sono solo strutture lineari o mappa. - Raccolte che passano valori speciali ai relativi costruttori. Non esiste alcuna funzionalità per accedere al costruttore in uso.
- Inizializzatori di raccolta annidati, ad esempio
new Widget { Children = { w1, w2, w3 } }. Questo modulo deve rimanere perché ha una semantica molto diversa daChildren = [w1, w2, w3]. Il primo chiama.Addripetutamente su.Childrenmentre quest'ultimo assegnerebbe una nuova raccolta su.Children. È possibile prendere in considerazione il fallback del secondo modulo per aggiungere a una raccolta esistente se.Childrennon è possibile assegnare, ma sembra che possa essere estremamente confuso.
Ambiguità della sintassi
Esistono due ambiguità sintattiche "vere" in cui sono presenti più interpretazioni sintattiche legali del codice che usano un oggetto
collection_literal_expression.l'oggetto
spread_elementè ambiguo con un oggettorange_expression. Si potrebbe tecnicamente avere:Range[] ranges = [range1, ..e, range2];Per risolvere questo problema, è possibile:
- Richiedere agli utenti di parentesi
(..e)o includere un indice0..einiziale se vogliono un intervallo. - Scegliere una sintassi diversa (ad esempio
...) per la distribuzione. Questo sarebbe sfortunato per la mancanza di coerenza con i modelli di sezione.
- Richiedere agli utenti di parentesi
Ci sono due casi in cui non c'è una vera ambiguità, ma dove la sintassi aumenta notevolmente la complessità di analisi. Anche se non è un problema dato il tempo di progettazione, questo aumenta comunque il sovraccarico cognitivo per gli utenti durante l'analisi del codice.
Ambiguità tra
collection_literal_expressioneattributessu istruzioni o funzioni locali. Considerare:[X(), Y, Z()]Potrebbe trattarsi di una delle seguenti:
// A list literal inside some expression statement [X(), Y, Z()].ForEach(() => ...); // The attributes for a statement or local function [X(), Y, Z()] void LocalFunc() { }Senza lookahead complesso, sarebbe impossibile dire senza consumare l'intero valore letterale.
Le opzioni per risolvere questo problema includono:
- Consentire questa operazione, eseguendo il lavoro di analisi per determinare quale di questi casi si tratta.
- Non consentire questa operazione e richiedere all'utente di eseguire il wrapping del valore letterale tra parentesi come
([X(), Y, Z()]).ForEach(...). - Ambiguità tra un
collection_literal_expressionoggetto in econditional_expressionun oggettonull_conditional_operations. Considerare:
M(x ? [a, b, c]Potrebbe trattarsi di una delle seguenti:
// A ternary conditional picking between two collections M(x ? [a, b, c] : [d, e, f]); // A null conditional safely indexing into 'x': M(x ? [a, b, c]);Senza lookahead complesso, sarebbe impossibile dire senza consumare l'intero valore letterale.
Nota: si tratta di un problema anche senza un tipo naturale perché la digitazione di destinazione si applica tramite
conditional_expressions.Come per gli altri, potremmo richiedere parentesi per disambiguare. In altre parole, presupporre l'interpretazione
null_conditional_operationa meno che non venga scritta così:x ? ([1, 2, 3]) :. Tuttavia, sembra piuttosto sfortunato. Questo tipo di codice non sembra irragionevole scrivere e probabilmente inseprirà le persone.
Svantaggi
- Questo introduce ancora un altro modulo per le espressioni di raccolta oltre alle innumerevoli modalità già disponibili. Questa è una complessità aggiuntiva per il linguaggio. Detto questo, ciò consente anche di unificare su una sintassi
circolareper regolarli tutti, il che significa che le codebase esistenti possono essere semplificate e spostate in un aspetto uniforme ovunque. - L'uso
[di ...]invece di{...}si allontana dalla sintassi usata in genere per matrici e inizializzatori di raccolta già. In particolare, che usa[...]invece di{...}. Tuttavia, questo è già stato stabilito dal team linguistico quando abbiamo fatto modelli di elenco. Si è tentato di fare{...}lavorare con i modelli di elenco e si sono verificati problemi insormontabili. Per questo motivo, ci siamo spostati in[...]che, mentre è nuovo per C#, si sente naturale in molti linguaggi di programmazione e ci ha permesso di iniziare fresco senza ambiguità. L'uso[di ...]come forma letterale corrispondente è complementare alle nostre ultime decisioni, e ci dà un posto pulito per lavorare senza problemi.
In questo modo si introducono le warts nella lingua. Ad esempio, i seguenti sono sia legali che (fortunatamente) indicano esattamente la stessa cosa:
int[] x = { 1, 2, 3 };
int[] x = [ 1, 2, 3 ];
Tuttavia, data l'ampiezza e la coerenza apportata dalla nuova sintassi letterale, è consigliabile consigliare agli utenti di passare al nuovo modulo. I suggerimenti e le correzioni dell'IDE potrebbero essere utili in tal senso.
Alternativi
- Quali altri disegni sono stati considerati? Qual è l'impatto della mancata operazione?
Domande risolte
Il compilatore deve usare
stackallocper l'allocazione dello stack quando le matrici inline non sono disponibili e il tipo di iterazione è un tipo primitivo?Risoluzione: No. La gestione di un
stackallocbuffer richiede ulteriore sforzo su una matrice inline per assicurarsi che il buffer non venga allocato ripetutamente quando l'espressione di raccolta si trova all'interno di un ciclo. La complessità aggiuntiva nel compilatore e nel codice generato supera il vantaggio dell'allocazione dello stack su piattaforme meno recenti.In quale ordine è necessario valutare gli elementi letterali rispetto alla valutazione della proprietà Length/Count? È necessario valutare prima tutti gli elementi, quindi tutte le lunghezze? O dovremmo valutare un elemento, quindi la sua lunghezza, l'elemento successivo e così via?
Soluzione: vengono valutati prima tutti gli elementi, quindi tutto il resto lo segue.
Un valore letterale lunghezza sconosciuta può creare un tipo di raccolta che richiede una lunghezza nota, ad esempio una matrice, un intervallo o una raccolta Construct(array/span) ? Questa operazione sarebbe più difficile da eseguire in modo efficiente, ma potrebbe essere possibile tramite un uso intelligente di matrici in pool e/o generatori.
Soluzione: Sì, è possibile creare una raccolta a lunghezza fissa da un valore letterale di lunghezza sconosciuto . Il compilatore può implementare questa operazione nel modo più efficiente possibile.
Il testo seguente esiste per registrare la discussione originale di questo argomento.
Gli utenti possono sempre trasformare un valore letterale di lunghezza sconosciuta in una lunghezza nota con codice simile al seguente:
ImmutableArray<int> x = [a, ..unknownLength.ToArray(), b];Tuttavia, ciò è sfortunato a causa della necessità di forzare le allocazioni di archiviazione temporanea. Potremmo essere potenzialmente più efficienti se abbiamo controllato come questo è stato generato.
Un oggetto
collection_expressionpuò essere tipizzato come destinazione in un oggettoIEnumerable<T>o in altre interfacce di raccolta?Per esempio:
void DoWork(IEnumerable<long> values) { ... } // Needs to produce `longs` not `ints` for this to work. DoWork([1, 2, 3]);Soluzione: Sì, un valore letterale può essere tipizzato come destinazione a qualsiasi tipo di
I<T>interfaccia implementatoList<T>. Ad esempio:IEnumerable<long>. Equivale alla digitazione di destinazione aList<long>e quindi all'assegnazione del risultato al tipo di interfaccia specificato. Il testo seguente esiste per registrare la discussione originale di questo argomento.La domanda aperta qui consiste nel determinare il tipo sottostante da creare effettivamente. Un'opzione consiste nell'esaminare la proposta per
params IEnumerable<T>. Verrà generata una matrice per passare i valori, in modo analogo a quanto accade conparams T[].Può/deve generare
Array.Empty<T>()il compilatore per[]? Dobbiamo imporre che lo faccia, per evitare allocazioni quando possibile?Sì. Il compilatore deve generare
Array.Empty<T>()per qualsiasi caso in cui sia legale e il risultato finale non sia modificabile. Ad esempio, come destinazioneT[],IEnumerable<T>IReadOnlyCollection<T>oIReadOnlyList<T>. Non deve essere usatoArray.Empty<T>quando la destinazione è modificabile (ICollection<T>oIList<T>).È consigliabile espandere gli inizializzatori di raccolta per cercare il metodo molto comune
AddRange? Può essere usato dal tipo costruito sottostante per eseguire l'aggiunta di elementi distribuiti potenzialmente più efficiente. Potremmo anche voler cercare cose come.CopyTo. In questo caso possono verificarsi svantaggi perché questi metodi potrebbero causare allocazioni/dispatch in eccesso rispetto all'enumerazione diretta nel codice tradotto.Sì. Un'implementazione può utilizzare altri metodi per inizializzare un valore di raccolta, presupponendo che questi metodi abbiano una semantica ben definita e che i tipi di raccolta debbano essere "ben comportati". In pratica, tuttavia, un'implementazione deve essere prudente perché i vantaggi in un modo (copia bulk) possono avere conseguenze negative anche (ad esempio, boxing di una raccolta di struct).
Un'implementazione deve sfruttare i vantaggi nei casi in cui non ci sono svantaggi. Ad esempio, con un
.AddRange(ReadOnlySpan<T>)metodo .
Domande non risolte
- È consigliabile consentire l'inferenza del tipo di elemento quando il tipo di iterazione è "ambiguo" (da una definizione)? Per esempio:
Collection x = [1L, 2L];
// error CS1640: foreach statement cannot operate on variables of type 'Collection' because it implements multiple instantiations of 'IEnumerable<T>'; try casting to a specific interface instantiation
foreach (var x in new Collection) { }
static class Builder
{
public Collection Create(ReadOnlySpan<long> items) => throw null;
}
[CollectionBuilder(...)]
class Collection : IEnumerable<int>, IEnumerable<string>
{
IEnumerator<int> IEnumerable<int>.GetEnumerator() => throw null;
IEnumerator<string> IEnumerable<string>.GetEnumerator() => throw null;
IEnumerator IEnumerable.GetEnumerator() => throw null;
}
È consigliabile creare e indicizzare immediatamente in un valore letterale della raccolta? Nota: questo richiede una risposta alla domanda non risolta riportata di seguito per stabilire se i valori letterali della raccolta hanno un tipo naturale.
Le allocazioni di stack per raccolte di grandi dimensioni potrebbero far saltare lo stack. Il compilatore deve avere un'euristica per inserire questi dati nell'heap? Il linguaggio non deve essere specificato per consentire questa flessibilità? È consigliabile seguire la specifica per
params Span<T>.È necessario specificare il tipo di
spread_elementdestinazione? Si consideri, ad esempio:Span<int> span = [a, ..b ? [c] : [d, e], f];Nota: questo può in genere venire nel formato seguente per consentire l'inclusione condizionale di alcuni set di elementi o nulla se la condizione è false:
Span<int> span = [a, ..b ? [c, d, e] : [], f];Per valutare questo valore letterale completo, è necessario valutare le espressioni di elemento all'interno. Ciò significa essere in grado di valutare
b ? [c] : [d, e]. Tuttavia, assente un tipo di destinazione per valutare questa espressione nel contesto di e assente qualsiasi tipo di tipo naturale, non sarebbe possibile determinare cosa fare con[c]o[d, e]qui.Per risolvere questo problema, è possibile dire che durante la valutazione dell'espressione di
spread_elementun valore letterale è presente un tipo di destinazione implicito equivalente al tipo di destinazione del valore letterale stesso. Quindi, nell'esempio precedente, questo verrebbe riscritto come:int __e1 = a; Span<int> __s1 = b ? [c] : [d, e]; int __e2 = f; Span<int> __result = stackalloc int[2 + __s1.Length]; int __index = 0; __result[__index++] = a; foreach (int __t in __s1) __result[index++] = __t; __result[__index++] = f; Span<int> span = __result;
La specifica di un tipo di raccolta costruttibile che utilizza un metodo create è sensibile al contesto in cui viene classificata la conversione
Un'esistenza della conversione in questo caso dipende dal concetto di un tipo di iterazione del tipo di raccolta. Se è presente un metodo create che accetta dove ReadOnlySpan<T>T è il tipo di iterazione, la conversione esiste. In caso contrario, non lo fa.
Tuttavia, un tipo di iterazione è sensibile al contesto in corrispondenza del quale foreach viene eseguita. Per lo stesso tipo di raccolta può essere diverso in base ai metodi di estensione inclusi nell'ambito e può anche essere indefinito.
Ciò si sente bene allo scopo di foreach quando il tipo non è progettato per essere foreach-able su se stesso. In caso affermativo, i metodi di estensione non possono modificare il modo in cui il tipo è foreach-ed, indipendentemente dal contesto.
Tuttavia, sembra piuttosto strano che una conversione sia sensibile al contesto in questo modo. In effetti la conversione è "instabile". Un tipo di raccolta progettato in modo esplicito per essere costruiscibile può escludere una definizione di dettaglio molto importante, ovvero il tipo di iterazione. Lasciando il tipo "inconvertibile" su se stesso.
Di seguito è riportato un esempio:
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
[CollectionBuilder(typeof(MyCollectionBuilder), nameof(MyCollectionBuilder.Create))]
class MyCollection
{
}
class MyCollectionBuilder
{
public static MyCollection Create(ReadOnlySpan<long> items) => throw null;
public static MyCollection Create(ReadOnlySpan<string> items) => throw null;
}
namespace Ns1
{
static class Ext
{
public static IEnumerator<long> GetEnumerator(this MyCollection x) => throw null;
}
class Program
{
static void Main()
{
foreach (var l in new MyCollection())
{
long s = l;
}
MyCollection x1 = ["a", // error CS0029: Cannot implicitly convert type 'string' to 'long'
2];
}
}
}
namespace Ns2
{
static class Ext
{
public static IEnumerator<string> GetEnumerator(this MyCollection x) => throw null;
}
class Program
{
static void Main()
{
foreach (var l in new MyCollection())
{
string s = l;
}
MyCollection x1 = ["a",
2]; // error CS0029: Cannot implicitly convert type 'int' to 'string'
}
}
}
namespace Ns3
{
class Program
{
static void Main()
{
// error CS1579: foreach statement cannot operate on variables of type 'MyCollection' because 'MyCollection' does not contain a public instance or extension definition for 'GetEnumerator'
foreach (var l in new MyCollection())
{
}
MyCollection x1 = ["a", 2]; // error CS9188: 'MyCollection' has a CollectionBuilderAttribute but no element type.
}
}
}
Data la progettazione corrente, se il tipo non definisce il tipo di iterazione stesso, il compilatore non è in grado di convalidare in modo affidabile un'applicazione di un CollectionBuilder attributo. Se non si conosce il tipo di iterazione, non si conosce la firma del metodo create . Se il tipo di iterazione proviene dal contesto, non esiste alcuna garanzia che il tipo venga sempre usato in un contesto simile.
Questa funzionalità è interessata anche dalla funzionalità Raccolte params. Sembra strano non essere in grado di stimare in modo affidabile il tipo di elemento di un params parametro nel punto di dichiarazione. La proposta corrente richiede inoltre di assicurarsi che il metodo create sia almeno accessibile come il paramstipo di raccolta. È impossibile eseguire questo controllo in modo affidabile, a meno che il tipo di raccolta non definisca il tipo di iterazione stesso.
Si noti che è stato aperto anche https://github.com/dotnet/roslyn/issues/69676 per il compilatore, che fondamentalmente osserva lo stesso problema, ma ne parla dal punto di vista dell'ottimizzazione.
Proposta
Richiedere un tipo che usa l'attributo CollectionBuilder per definire il tipo di iterazione su se stesso.
In altre parole, questo significa che il tipo deve implementare IEnumarable/IEnumerable<T>o che deve avere un metodo pubblico GetEnumerator con la firma corretta (questo esclude tutti i metodi di estensione).
Inoltre, al momento è necessario creare il metodo per "essere accessibile dove viene usata l'espressione di raccolta". Si tratta di un altro punto di dipendenza del contesto basato sull'accessibilità. Lo scopo di questo metodo è molto simile allo scopo di un metodo di conversione definito dall'utente e che uno deve essere pubblico. Pertanto, è consigliabile considerare la necessità di rendere pubblico anche il metodo di creazione .
Conclusione
Approvato con modifiche LDM-2024-01-08
Il concetto di tipo di iterazione non viene applicato in modo coerente in tutte le conversioni
- Per uno struct o un tipo di classe che implementa
System.Collections.Generic.IEnumerable<T>dove:
- Per ogni elemento
Eiè presente una conversione implicita inT.
Sembra che venga fatto un presupposto che T sia necessario il tipo di iterazione dello struct o del tipo di classe in questo caso.
Tuttavia, tale presupposto non è corretto. Che può portare a un comportamento molto strano. Per esempio:
using System.Collections;
using System.Collections.Generic;
class MyCollection : IEnumerable<long>
{
IEnumerator<long> IEnumerable<long>.GetEnumerator() => throw null;
IEnumerator IEnumerable.GetEnumerator() => throw null;
public void Add(string l) => throw null;
public IEnumerator<string> GetEnumerator() => throw null;
}
class Program
{
static void Main()
{
foreach (var l in new MyCollection())
{
string s = l; // Iteration type is string
}
MyCollection x1 = ["a", // error CS0029: Cannot implicitly convert type 'string' to 'long'
2];
MyCollection x2 = new MyCollection() { "b" };
}
}
- Per uno struct o un tipo di classe che implementa
System.Collections.IEnumerablee non implementaSystem.Collections.Generic.IEnumerable<T>.
Sembra che l'implementazione presuppor che il tipo di iterazione sia object, ma la specifica lascia questo fatto non specificato e semplicemente non richiede che ogni elemento venga convertito in nulla. In generale, tuttavia, il tipo di iterazione non è necessario per il object tipo . Che può essere osservato nell'esempio seguente:
using System.Collections;
using System.Collections.Generic;
class MyCollection : IEnumerable
{
public IEnumerator<string> GetEnumerator() => throw null;
IEnumerator IEnumerable.GetEnumerator() => throw null;
}
class Program
{
static void Main()
{
foreach (var l in new MyCollection())
{
string s = l; // Iteration type is string
}
}
}
Il concetto di tipo di iterazione è fondamentale per la funzionalità Raccolte params . E questo problema porta a una strana discrepanza tra le due funzionalità. Per esempio:
using System.Collections;
using System.Collections.Generic;
class MyCollection : IEnumerable<long>
{
IEnumerator<long> IEnumerable<long>.GetEnumerator() => throw null;
IEnumerator IEnumerable.GetEnumerator() => throw null;
public IEnumerator<string> GetEnumerator() => throw null;
public void Add(long l) => throw null;
public void Add(string l) => throw null;
}
class Program
{
static void Main()
{
Test("2"); // error CS0029: Cannot implicitly convert type 'string' to 'long'
Test(["2"]); // error CS1503: Argument 1: cannot convert from 'collection expressions' to 'string'
Test(3); // error CS1503: Argument 1: cannot convert from 'int' to 'string'
Test([3]); // Ok
MyCollection x1 = ["2"]; // error CS0029: Cannot implicitly convert type 'string' to 'long'
MyCollection x2 = [3];
}
static void Test(params MyCollection a)
{
}
}
using System.Collections;
using System.Collections.Generic;
class MyCollection : IEnumerable
{
IEnumerator IEnumerable.GetEnumerator() => throw null;
public IEnumerator<string> GetEnumerator() => throw null;
public void Add(object l) => throw null;
}
class Program
{
static void Main()
{
Test("2", 3); // error CS1503: Argument 2: cannot convert from 'int' to 'string'
Test(["2", 3]); // Ok
}
static void Test(params MyCollection a)
{
}
}
Probabilmente sarà buono allineare un modo o l'altro.
Proposta
Specificare la convertibilità dello struct o del tipo di classe che implementa System.Collections.Generic.IEnumerable<T> o System.Collections.IEnumerable in termini di tipo iterazione e richiede una conversione implicita per ogni elementoEi nel tipo di iterazione.
Conclusione
Approvato LDM-2024-01-08
La conversione delle espressioni di raccolta deve richiedere la disponibilità di un set minimo di API per la costruzione?
Un tipo di raccolta costruttibile in base alle conversioni può effettivamente non essere costruttibile, che probabilmente comporta un comportamento imprevisto di risoluzione dell'overload. Per esempio:
class C1
{
public static void M1(string x)
{
}
public static void M1(char[] x)
{
}
void Test()
{
M1(['a', 'b']); // error CS0121: The call is ambiguous between the following methods or properties: 'C1.M1(string)' and 'C1.M1(char[])'
}
}
Tuttavia, il 'C1. M1(string)' non è un candidato che può essere usato perché:
error CS1729: 'string' does not contain a constructor that takes 0 arguments
error CS1061: 'string' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'string' could be found (are you missing a using directive or an assembly reference?)
Ecco un altro esempio con un tipo definito dall'utente e un errore più forte che non menziona nemmeno un candidato valido:
using System.Collections;
using System.Collections.Generic;
class C1 : IEnumerable<char>
{
public static void M1(C1 x)
{
}
public static void M1(char[] x)
{
}
void Test()
{
M1(['a', 'b']); // error CS1061: 'C1' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'C1' could be found (are you missing a using directive or an assembly reference?)
}
public static implicit operator char[](C1 x) => throw null;
IEnumerator<char> IEnumerable<char>.GetEnumerator() => throw null;
IEnumerator IEnumerable.GetEnumerator() => throw null;
}
Sembra che la situazione sia molto simile a quella usata con il gruppo di metodi per delegare le conversioni. Ci sono stati scenari in cui esisteva la conversione, ma era errato. Abbiamo deciso di migliorarlo assicurando che, se la conversione è errata, allora non esiste.
Si noti che con la funzionalità "Raccolte params" si verifica un problema simile. Potrebbe essere consigliabile non consentire l'uso del params modificatore per le raccolte non costruiscibili. Tuttavia, nella proposta corrente che il controllo si basa sulla sezione conversioni . Di seguito è riportato un esempio:
using System.Collections;
using System.Collections.Generic;
class C1 : IEnumerable<char>
{
public static void M1(params C1 x) // It is probably better to report an error about an invalid `params` modifier
{
}
public static void M1(params ushort[] x)
{
}
void Test()
{
M1('a', 'b'); // error CS1061: 'C1' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'C1' could be found (are you missing a using directive or an assembly reference?)
M2('a', 'b'); // Ok
}
public static void M2(params ushort[] x)
{
}
IEnumerator<char> IEnumerable<char>.GetEnumerator() => throw null;
IEnumerator IEnumerable.GetEnumerator() => throw null;
}
Sembra che il problema sia stato illustrato in precedenza, vedere https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-10-02.md#collection-expressions. A quel punto è stato fatto un argomento che le regole, come specificato in questo momento, sono coerenti con il modo in cui vengono specificati i gestori di stringhe interpolati. Ecco una citazione:
In particolare, i gestori di stringhe interpolati sono stati originariamente specificati in questo modo, ma è stata modificata la specifica dopo aver considerato questo problema.
Anche se c'è una certa somiglianza, c'è anche una distinzione importante da considerare. Ecco una citazione da https://github.com/dotnet/csharplang/blob/main/proposals/csharp-10.0/improved-interpolated-strings.md#interpolated-string-handler-conversion:
Il tipo
Tè detto essere un applicable_interpolated_string_handler_type se è attribuito conSystem.Runtime.CompilerServices.InterpolatedStringHandlerAttribute. Esiste un interpolated_string_handler_conversion implicito daTun interpolated_string_expression o un additive_expression composto interamente da _interpolated_string_expression_s e utilizzando solo+operatori.
Il tipo di destinazione deve avere un attributo speciale che rappresenta un indicatore sicuro della finalità dell'autore affinché il tipo sia un gestore di stringhe interpolato. È giusto presupporre che la presenza dell'attributo non sia una coincidenza.
Al contrario, il fatto che un tipo sia "enumerabile", non è necessario significa che c'era la finalità dell'autore per il tipo da costruire. Una presenza di un metodo di creazione, tuttavia, che è indicata con un [CollectionBuilder(...)] attributo sul tipo di raccolta, sembra un indicatore forte della finalità dell'autore per il tipo da costruire.
Proposta
Per uno struct o un tipo di classe che implementa System.Collections.IEnumerable e che non dispone di una sezione di conversioni di metodi di creazione devono richiedere almeno la presenza delle API seguenti:
- Costruttore accessibile applicabile senza argomenti.
- Un'istanza accessibile
Addo un metodo di estensione che può essere richiamato con il valore del tipo di iterazione come argomento.
Ai fini della funzionalità Collectons params , tali tipi sono tipi validi params quando queste API sono dichiarate pubbliche e sono metodi di istanza (vs. estensione).
Conclusione
Approvato con modifiche LDM-2024-01-10
C# feature specifications