Sammlungsausdrücke
Anmerkung
Dieser Artikel ist eine Featurespezifikation. Die Spezifikation dient als Designdokument für das Feature. Es enthält vorgeschlagene Spezifikationsänderungen sowie Informationen, die während des Entwurfs und der Entwicklung des Features erforderlich sind. Diese Artikel werden veröffentlicht, bis die vorgeschlagenen Spezifikationsänderungen abgeschlossen und in die aktuelle ECMA-Spezifikation aufgenommen werden.
Es kann einige Abweichungen zwischen der Featurespezifikation und der abgeschlossenen Implementierung geben. Diese Unterschiede werden in den entsprechenden Hinweisen zum Language Design Meeting (LDM) erfasst.
Weitere Informationen zum Einführen von Featurespezifikationen in den C#-Sprachstandard finden Sie im Artikel zu den Spezifikationen.
Champion Issue: https://github.com/dotnet/csharplang/issues/8652
Zusammenfassung
Sammlungsausdrücke führen eine neue, knappe Syntax ein, [e1, e2, e3, etc]
, um gemeinsame Sammlungswerte zu erstellen. Das Eingliedern anderer Auflistungen in diese Werte ist mit einem Spread-Element möglich..e
so: [e1, ..c2, e2, ..c2]
.
Mehrere sammlungsähnliche Typen können erstellt werden, ohne dass externe BCL-Unterstützung erforderlich ist. Diese Typen sind:
- Array-Typen, wie z. B.
int[]
. -
Span<T>
undReadOnlySpan<T>
. - Typen, die Sammlungsinitialisiererunterstützen, wie
List<T>
.
Eine weitere Unterstützung ist für sammlungsähnliche Typen vorhanden, die nicht unter das oben genannte durch ein neues Attribut- und API-Muster fallen, das direkt auf den Typ selbst übernommen werden kann.
Motivation
Sammlungsähnliche Werte sind in Programmierung, Algorithmen und insbesondere im C#/.NET-Ökosystem sehr präsent. Fast alle Programme verwenden diese Werte, um Daten zu speichern und Daten von anderen Komponenten zu senden oder zu empfangen. Derzeit müssen fast alle C#-Programme viele verschiedene und unglücklicherweise umfangreiche Ansätze verwenden, um Instanzen solcher Werte zu erzeugen. Einige Ansätze haben auch Leistungseinbußen. Nachfolgend finden Sie einige gängige Beispiele:
- Arrays, die entweder
new Type[]
odernew[]
vor den{ ... }
Werten erfordern. - Spans, die
stackalloc
und andere umständliche Konstrukte verwenden können. - Sammlungsinitialisierer, die eine Syntax wie
new List<T>
(ohne Inferenz eines möglicherweise ausführlichenT
) vor ihren Werten erfordern und die mehrfache Neuzuweisungen von Speicher verursachen können, weil sie N.Add
-Aufrufe verwenden, ohne eine Anfangskapazität zu liefern. - Unveränderliche Auflistungen, die Syntax wie
ImmutableArray.Create(...)
zum Initialisieren der Werte erfordern und zu zwischengeschalteten Zuordnungen und Datenkopien führen können. Effizientere Bauformen (wieImmutableArray.CreateBuilder
) sind umständlich und erzeugen immer noch unvermeidbaren Abfall.
- Arrays, die entweder
Wenn wir das umgebende Ökosystem betrachten, finden wir überall Beispiele dafür, dass die Erstellung von Listen bequemer und angenehmer ist. TypeScript, Dart, Swift, Elm, Python und andere bevorzugen eine prägnante Syntax zu diesem Zweck, mit weit verbreiteter Nutzung und großer Wirkung. Oberflächliche Untersuchungen haben keine wesentlichen Probleme in diesen Ökosystemen ergeben, die durch den Einbau dieser Literale entstehen.
C# hat in C# 11 auch Listenmuster hinzugefügt. Dieses Muster ermöglicht das Abgleichen und Deconieren von listenähnlichen Werten mithilfe einer sauberen und intuitiven Syntax. Im Gegensatz zu fast allen anderen Musterkonstrukten fehlt diese Zuordnungs-/Dekonstruktionssyntax jedoch an der entsprechenden Konstruktionssyntax.
Das Erzielen der besten Leistung beim Erstellen der einzelnen Sammlungstypen kann schwierig sein. Einfache Lösungen verschwenden häufig sowohl CPU als auch Arbeitsspeicher. Eine wörtliche Form ermöglicht maximale Flexibilität von der Compiler-Implementierung, um das Literal zu optimieren, um ein mindestens so gutes Ergebnis zu erzielen, wie es ein Benutzer bereitstellen könnte, aber mit einfachem Code. Sehr oft wird der Compiler in der Lage sein, besser zu tun, und die Spezifikation zielt darauf ab, die Implementierung große Mengen an Spielraum in Bezug auf die Implementierungsstrategie zu ermöglichen, um dies zu gewährleisten.
Für C# ist eine inklusive Lösung erforderlich. Es sollte die überwiegende Mehrheit der Kunden in Bezug auf die kollektionsähnlichen Arten und Werte erfüllen, die sie bereits haben. Es sollte sich auch in der Sprache natürlich anfühlen und die Leistung beim Musterabgleich widerspiegeln.
Dies führt zu einer natürlichen Schlussfolgerung, dass die Syntax wie [e1, e2, e3, e-etc]
oder [e1, ..c2, e2]
sein sollte, die den Musteräquivalenten von [p1, p2, p3, p-etc]
und [p1, ..p2, p3]
entsprechen.
Detailentwurf
Die folgenden Grammatik-Produktionen werden hinzugefügt:
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
;
Sammlungsliterale sind zieltypisiert.
Spezifikationsabklärungen
Der Kürze halber wird
collection_expression
in den folgenden Abschnitten als "wörtlich" bezeichnet.expression_element
Instanzen werden üblicherweise alse1
,e_n
, usw. bezeichnet.spread_element
Instanzen werden üblicherweise als..s1
,..s_n
usw. bezeichnet.span type bedeutet entweder
Span<T>
oderReadOnlySpan<T>
.Literale werden häufig als
[e1, ..s1, e2, ..s2, etc]
angezeigt, um eine Anzahl beliebiger Elemente in beliebiger Reihenfolge darzustellen. Wichtig ist, dass dieses Formular verwendet wird, um alle Fälle darzustellen, z. B.:- Leere Literale
[]
- Literale, die kein
expression_element
enthalten. - Literale, die kein
spread_element
enthalten. - Literale mit beliebiger Reihenfolge eines beliebigen Elementtyps.
- Leere Literale
Der Iterationstyp von
..s_n
ist der Typ der Iterationsvariablen , der so bestimmt wird, als obs_n
als der Ausdruck verwendet würde, über den in einemforeach_statement
iteriert wird.Variablen, die mit
__name
beginnen, werden verwendet, um die Ergebnisse der Auswertung vonname
darzustellen, die an einem Speicherort gespeichert sind, sodass sie nur einmal ausgewertet wird. Beispielsweise ist__e1
die Auswertung vone1
.List<T>
,IEnumerable<T>
usw. beziehen sich auf die jeweiligen Typen imSystem.Collections.Generic
Namespace.Die Spezifikation definiert eine Übersetzung des Literals in bestehende C#-Konstrukte. Ähnlich wie bei der Übersetzung von Abfrageausdrückenist das Literal selbst nur dann legal, wenn die Übersetzung zu legalem Code führen würde. Zweck dieser Regel besteht darin, zu vermeiden, dass andere Regeln der Sprache wiederholt werden müssen, die impliziert werden (z. B. zur Konvertierungsfähigkeit von Ausdrücken, wenn sie Speicherorten zugewiesen sind).
Eine Implementierung ist nicht erforderlich, um Literale genau wie unten angegeben zu übersetzen. Jede Übersetzung ist legal, wenn dasselbe Ergebnis erzeugt wird und es keine feststellbaren Unterschiede in der Produktion des Ergebnisses gibt.
- Beispielsweise könnte eine Implementierung Literale wie
[1, 2, 3]
direkt in einennew int[] { 1, 2, 3 }
Ausdruck übersetzen, der die Rohdaten direkt in die Assembly einbettet und damit die Notwendigkeit für__index
oder eine Reihe von Anweisungen zum Zuweisen jedes Werts überflüssig macht. Wichtig ist dabei, dass wenn ein beliebiger Schritt der Übersetzung zur Laufzeit eine Ausnahme verursachen kann, der Programmstatus trotzdem im durch die Übersetzung angegebenen Zustand bleibt.
- Beispielsweise könnte eine Implementierung Literale wie
Verweise auf "Stack-Zuweisung" beziehen sich auf jede Strategie zur Zuweisung auf dem Stack und nicht auf dem Heap. Wichtig ist, dass es nicht impliziert oder erfordert, dass diese Strategie durch den tatsächlichen
stackalloc
Mechanismus erfolgt. Zum Beispiel ist die Verwendung von Inline-Arrays ebenfalls ein zulässiger und wünschenswerter Ansatz, um die Stack-Allokation zu erreichen, sofern verfügbar. Beachten Sie, dass in C# 12 Inlinearrays nicht mit einem Auflistungsausdruck initialisiert werden können. Das bleibt ein offener Vorschlag.Bei Sammlungen wird davon ausgegangen, dass sie sich gut verhalten. Zum Beispiel:
- Es wird davon ausgegangen, dass der Wert von
Count
für eine Auflistung denselben Wert wie die Anzahl der Elemente erzeugt, wenn sie aufgezählt werden. - Die in dieser Spezifikation verwendeten Typen, die im
System.Collections.Generic
-Namespace definiert sind, werden als nebeneffektfrei angenommen. Daher kann der Compiler Szenarien optimieren, in denen solche Typen als Zwischenwerte verwendet werden, andernfalls aber nicht verfügbar gemacht werden. - Es wird davon ausgegangen, dass ein Aufruf eines
.AddRange(x)
-Mitglieds einer Sammlung zu demselben Endwert führt, als wenn Sie überx
iterieren und alle seine Aufzählungswerte einzeln mit.Add
zur Sammlung hinzufügen. - Das Verhalten von Sammlungsliteralen mit Sammlungen, die sich nicht gut benehmen, ist undefiniert.
- Es wird davon ausgegangen, dass der Wert von
Umwandlungen
Eine Sammlungsausdruck-Konvertierung ermöglicht die Konvertierung eines Sammlungsausdrucks in einen Typ.
Es gibt eine implizite Sammlungsausdruck-Konvertierung von einem Sammlungsausdruck zu den folgenden Typen:
- Ein eindimensionaler Arraytyp
T[]
, wobei der ElementtypT
ist. - Ein Span-Typ:
System.Span<T>
System.ReadOnlySpan<T>
In diesem Fall ist der ElementtypT
- Ein -Typ mit einer geeigneten --Create-Methode. In diesem Fall ist der -Elementtyp der --Iterationstyp, der aus einer
GetEnumerator
-Instanzmethode oder einer Enumerable-Schnittstelle bestimmt wird, nicht aus einer Erweiterungsmethode. - Eine Struktur oder ein Klassentyp, der
System.Collections.IEnumerable
implementiert, wobei:Der Typ hat einen anwendbaren Konstruktor, der ohne Argumente aufgerufen werden kann, und der Konstruktor ist an der Stelle des Sammlungsausdrucks zugänglich.
Wenn der Auflistungsausdruck Elemente enthält, hat der Typ eine Instanz- oder Erweiterungsmethode
Add
, wobei:- Die Methode kann mit einem einzelnen Wertargument aufgerufen werden.
- Wenn die Methode generisch ist, können die Typargumente aus der Auflistung und dem Argument abgeleitet werden.
- Die Methode ist am Speicherort des Sammlungsausdrucks zugänglich.
In diesem Fall ist der -Elementtyp der Iterationstyp vom Typ .
- Ein Schnittstellentyp:
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>
In diesem Fall ist der ElementtypT
Die implizite Konvertierung existiert, wenn der Typ einen ElementtypT
hat, wobei für jedes ElementEᵢ
im Sammlungsausdruck:
- Wenn
Eᵢ
ein Ausdruckselementist, gibt es eine implizite Konvertierung vonEᵢ
inT
. - Wenn
Eᵢ
ein Spread-Element..Sᵢ
ist, gibt es eine implizite Konvertierung vom Iterationstyp vonSᵢ
inT
.
Es gibt keine Sammlungsausdruck-Konvertierung von einem Sammlungsausdruck zu einem mehrdimensionalen Array-Typ.
Typen, für die es eine implizite Konvertierung eines Sammlungsausdrucks aus einem Sammlungsausdruck gibt, sind die gültigen Zieltypen für diesen Sammlungsausdruck.
Es gibt die folgenden zusätzlichen impliziten Konvertierungen aus einem Sammlungsausdruck:
Zu einem nullbaren Werttyp
T?
, bei dem es eine Sammlungsausdruck-Konvertierung vom Sammlungsausdruck zu einem WerttypT
gibt. Die Konvertierung ist eine Sammlungsausdruck-Konvertierung nachT
gefolgt von einer impliziten nullbaren Konvertierung vonT
nachT?
.Zu einem Referenztyp
T
, bei dem es eine Erstellungsmethode gibt, die mitT
assoziiert ist und einen TypU
und eine implizite Referenzkonvertierung vonU
zuT
zurückgibt. Die Konvertierung ist eine Sammlungsausdruck-Konvertierung nachU
gefolgt von einer impliziten Referenzkonvertierung vonU
nachT
.Zu einem Schnittstellentyp
I
, bei dem es eine Erstellungsmethode gibt, die mitI
assoziiert ist und einen TypV
und eine implizite Boxing-Konvertierung vonV
nachI
zurückgibt. Die Konvertierung ist eine Sammlungsausdruck-Konvertierung nachV
gefolgt von einer impliziten Boxing-Konvertierung vonV
nachI
.
Erstellen von Methoden
Eine Erstellungsmethode wird mit einem [CollectionBuilder(...)]
-Attribut auf dem Sammlungstypangezeigt.
Das Attribut gibt den Generatortyp und Methodennamen einer Methode an, die aufgerufen werden soll, um eine Instanz des Auflistungstyps zu erstellen.
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; }
}
}
Das Attribut kann auf eine class
, struct
, ref struct
oder interface
angewendet werden.
Das Attribut wird nicht geerbt, obwohl das Attribut auf eine Basis-class
oder eine abstract class
angewendet werden kann.
Der Buildertyp muss ein nicht-generischer class
oder struct
sein.
Zunächst wird die Menge der anwendbaren ErstellungsmethodenCM
bestimmt.
Sie besteht aus Methoden, die die folgenden Anforderungen erfüllen:
- Die Methode muss den im attribut
[CollectionBuilder(...)]
angegebenen Namen haben. - Die Methode muss für den Generatortyp direkt definiert werden.
- Die Methode muss
static
sein. - Die Methode muss dort zugänglich sein, wo der Sammlungsausdruck verwendet wird.
- Die Sorte der Methode muss mit der Sorte des Sammlungstyps übereinstimmen.
- Die Methode muss über einen einzelnen Parameter vom Typ
System.ReadOnlySpan<E>
verfügen, der nach Wert übergeben wird. - Es gibt eine Identitätskonvertierung, implizite Referenzkonvertierungoder Boxing-Konvertierung vom Rückgabetyp der Methode zum Sammlungstyp.
Methoden, die auf Basistypen oder Schnittstellen deklariert sind, werden ignoriert und sind nicht Teil der CM
Menge.
Wenn die CM
Menge leer ist, dann hat der Sammlungstyp keinen Elementtyp und hat keine Erstellungsmethode. Es gelten keine der folgenden Schritte.
Wenn nur eine Methode unter denen in der CM
Menge eine Identitätskonvertierung von E
zum Elementtyp des Sammlungstypshat, das ist die Erstellungsmethode für den Sammlungstyp. Andernfalls hat der Sammlungstyp keine Erstellungsmethode.
Wenn das attribut "[CollectionBuilder]
" nicht auf eine aufrufende Methode mit der erwarteten Signatur verweist, wird ein Fehler gemeldet.
Für einen Auflistungsausdruck mit einem Zieltyp C<S0, S1, …>
, in dem die TypdeklarationC<T0, T1, …>
eine zugeordnete GeneratormethodeB.M<U0, U1, …>()
hat, werden die generischen Typargumente aus dem Zieltyp in der Reihenfolge angewendet – vom äußersten enthaltenen Typ bis zum innersten – auf die Generatormethode.
Der span-Parameter für die Methode create kann explizit als scoped
oder [UnscopedRef]
markiert werden. Wenn der Parameter implizit oder explizit scoped
ist, kann der Compiler den Speicher für die Spanne auf dem Stack und nicht auf dem Heap zuweisen.
Ein Beispiel: Eine mögliche -Erstellungsmethode für ImmutableArray<T>
:
[CollectionBuilder(typeof(ImmutableArray), "Create")]
public struct ImmutableArray<T> { ... }
public static class ImmutableArray
{
public static ImmutableArray<T> Create<T>(ReadOnlySpan<T> items) { ... }
}
Mit der obigen Methode create könnte ImmutableArray<int> ia = [1, 2, 3];
als emittiert werden:
[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);
Konstruktion
Die Elemente eines Sammlungsausdrucks werden in der Reihenfolge von links nach rechts ausgewertet . Jedes Element wird genau einmal ausgewertet, und alle weiteren Verweise auf die Elemente beziehen sich auf die Ergebnisse dieser Erstauswertung.
Ein Spreizungselement kann iteriert werden, bevor oder nachdem die nachfolgenden Elemente im Sammlungsausdruck ausgewertetwerden.
Eine unbehandelte Ausnahme, die von einer der während der Konstruktion verwendeten Methoden ausgelöst wird, wird nicht erfasst und verhindert weitere Schritte in der Konstruktion.
Length
, Count
und GetEnumerator
werden davon ausgegangen, dass keine Nebenwirkungen auftreten.
Wenn der Zieltyp eine Struktur oder ein Klassentyp ist, der System.Collections.IEnumerable
implementiert, und der Zieltyp nicht über eine create-Methodeverfügt, wird die Sammlungsinstanz wie folgt konstruiert:
Die Elemente werden in der Reihenfolge ausgewertet. Einige oder alle Elemente können während der folgenden Schritte ausgewertet werden und nicht vorher.
Der Compiler kann die bekannte Länge des Sammlungsausdrucks bestimmen, indem er abzählbare Eigenschaften- oder äquivalente Eigenschaften von bekannten Schnittstellen oder Typen- für jeden Sammlungselement-Ausdruckaufruft.
Der Konstruktor, der ohne Argumente anwendbar ist, wird aufgerufen.
Für jedes Element in der Reihenfolge:
- Wenn das Element ein Ausdruckselementist, wird die entsprechende
Add
Instanz oder Erweiterungsmethode mit dem Element Ausdruck als Argument aufgerufen. (Im Gegensatz zum klassischen Sammlungsinitialisierungsverhaltensind Elementauswertung undAdd
-Aufrufe nicht notwendigerweise verschachtelt). - Wenn das Element ein Spread-Element ist, wird eine der folgenden Optionen verwendet:
- Eine anwendbare
GetEnumerator
-Instanz oder Erweiterungsmethode wird auf dem Spread-Element-Ausdruck ausgeführt, und für jedes Element vom Enumerator wird die anwendbareAdd
-Instanz oder Erweiterungsmethode auf der Sammlung-Instanz mit dem Element als Argument ausgeführt. Wenn der EnumeratorIDisposable
implementiert, wirdDispose
unabhängig von Ausnahmen nach enumeration aufgerufen. - Eine anwendbare
AddRange
Instanz oder Erweiterungsmethode wird auf der Sammlungsinstanz mit dem Spread-Element Ausdruck als Argument aufgerufen. - Eine anwendbare
CopyTo
Instanz oder Erweiterungsmethode wird auf dem Spread-Element-Ausdruck mit der Sammlungsinstanz und demint
Index als Argumente aufgerufen.
- Eine anwendbare
- Wenn das Element ein Ausdruckselementist, wird die entsprechende
Während der obigen
EnsureCapacity
Konstruktionsschritte kann eine anwendbare Instanz oder Erweiterungsmethode ein oder mehrere Male auf der Sammlungsinstanz mit einemint
Kapazitätsargument aufgerufen werden.
Wenn der Zieltyp ein Array, ein span, ein Typ mit einer Erstellungsmethodeoder ein Interfaceist, erfolgt die Konstruktion der Sammlungsinstanz wie folgt:
Die Elemente werden in der Reihenfolge ausgewertet. Einige oder alle Elemente können während der folgenden Schritte ausgewertet werden und nicht vorher.
Der Compiler kann die bekannte Länge des Sammlungsausdrucks bestimmen, indem er abzählbare Eigenschaften- oder äquivalente Eigenschaften von bekannten Schnittstellen oder Typen- für jeden Sammlungselement-Ausdruckaufruft.
Es wird eine Initialisierungsinstanz wie folgt erstellt:
- Wenn der Zieltyp ein Array ist und der Sammlungsausdruck eine bekannte Längehat, wird ein Array mit der erwarteten Länge zugewiesen.
- Wenn der Zieltyp ein Span oder ein Typ mit einer Erstellungsmethodeist und die Sammlung eine bekannte Längehat, wird ein Span mit der erwarteten Länge erstellt, der sich auf zusammenhängenden Speicher bezieht.
- Andernfalls wird ein Zwischenspeicher zugewiesen.
Für jedes Element in der Reihenfolge:
- Wenn das Element ein Ausdruckselementist, wird die Initialisierungsinstanz Indexer aufgerufen, um den ausgewerteten Ausdruck im aktuellen Index hinzuzufügen.
- Wenn das Element ein Spread-Element ist, wird eine der folgenden Optionen verwendet:
- Ein Element einer bekannten Schnittstelle oder eines bekannten Typs wird aufgerufen, um Elemente aus dem Spread-Elementausdruck in die Initialisierungsinstanz zu kopieren.
- Eine anwendbare
GetEnumerator
Instanz oder Erweiterungsmethode wird auf dem Ausdruck des Elements spread aufgerufen und für jedes Element aus dem Enumerator wird die Initialisierungsinstanz Indexer aufgerufen, um das Element am aktuellen Index hinzuzufügen. Wenn der EnumeratorIDisposable
implementiert, wirdDispose
unabhängig von Ausnahmen nach enumeration aufgerufen. - Eine anwendbare
CopyTo
Instanz oder Erweiterungsmethode wird auf dem Spread-Element-Ausdruck mit der Initialisierungsinstanz undint
Index als Argumente aufgerufen.
Wenn der Sammlung Zwischenspeicher zugewiesen wurde, wird eine Sammlungsinstanz mit der tatsächlichen Sammlungslänge zugewiesen, und die Werte aus der Initialisierungsinstanz werden in die Sammlungsinstanz kopiert, oder wenn eine Spanne erforderlich ist, kann der Compiler eine Spanne der tatsächlichen Sammlungslänge aus dem Zwischenspeicher verwenden. Andernfalls ist die Initialisierungsinstanz die Sammlungsinstanz.
Wenn der Zieltyp über eine Create-Methodeverfügt, wird die Create-Methode mit der Span-Instanz aufgerufen.
Hinweis: Der Compiler kann verzögern, indem er der Sammlung Elemente hinzufügt – oder verzögern, indem er durch verteilte Elemente iteriert- bis nach der Auswertung der nachfolgenden Elemente. (Wenn nachfolgende Spread-Elemente zählbare Eigenschaften aufweisen, die die Berechnung der erwarteten Länge der Auflistung vor der Zuordnung der Auflistung ermöglichen würden.) Im Gegensatz dazu kann der Compiler vorschnell Elemente zur Sammlung hinzufügen — und vorschnell durch Spread-Elemente iterieren — wenn es keinen Vorteil gibt, sich zu verzögern.
Betrachten Sie den folgenden Sammlungsausdruck:
int[] x = [a, ..b, ..c, d];
Wenn die Spread-Elemente
b
undc
zählbar sind, könnte der Compiler das Hinzufügen von Elementen ausa
undb
hinauszögern, bisc
ausgewertet wurde, um die Zuordnung des resultierenden Arrays in der erwarteten Länge zu ermöglichen. Danach könnte der Compiler eifrig Elemente ausc
hinzufügen, bevor erd
auswertet.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;
Leeres Sammlungsliteral
Das leere Literal
[]
hat keinen Typ. Ähnlich wie das Null-Literalkann dieses Literal jedoch implizit in einen beliebigen konstruierbaren Sammlungstyp umgewandelt werden.Das folgende Beispiel ist nicht zulässig, da es keinen Zieltyp gibt und keine anderen Konvertierungen beteiligt sind:
var v = []; // illegal
Das Verteilen eines leeren Buchstabens ist erlaubt. Zum Beispiel:
bool b = ... List<int> l = [x, y, .. b ? [1, 2, 3] : []];
Wenn
b
falsch ist, ist es nicht erforderlich, dass tatsächlich ein Wert für den leeren Auflistungsausdruck erstellt wird, da er sofort in Nullwerte im endgültigen Literal verteilt wird.Der leere Sammlungsausdruck darf ein Singleton sein, wenn er zum Erstellen eines endgültigen Sammlungswerts verwendet wird, von dem bekannt ist, dass er nicht veränderlich ist. Zum Beispiel:
// 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 = [];
Referenzsicherheit
Siehe safe-context constraint für Definitionen der safe-context Werte: Deklarationsblock, Funktionselementund Aufrufer-Kontext.
Der safe-context eines Sammlungsausdrucks ist:
Der sichere Kontext eines leeren Sammlungsausdrucks
[]
ist der Aufrufkontext.Wenn der Zieltyp ein span-Typ
System.ReadOnlySpan<T>
ist, undT
einer der primitiven Typenbool
,sbyte
,byte
,short
,ushort
,char
,int
,uint
,long
,ulong
,float
oderdouble
, und der Sammlungsausdruck enthält nur konstante Werte, ist der sichere Kontext des Sammlungsausdrucks der Aufrufer-Kontext.Wenn der Zieltyp ein span-Typ
System.Span<T>
oderSystem.ReadOnlySpan<T>
ist, ist der sichere Kontext des Sammlungsausdrucks der Deklarationsblock.Wenn der Zieltyp ein ref struct-Typ mit einer create-Methodeist, ist der safe-context des collection-Ausdrucks der safe-context eines Aufrufs der create-Methode, bei dem der collection-Ausdruck das span-Argument der Methode ist.
Andernfalls ist der sichere Kontext des Sammlungsausdrucks der Aufrufer-Kontext.
Ein Ausdruck für eine Sammlung mit einem sicheren Kontext von Deklarationsblock kann dem umschließenden Bereich nicht entkommen, und der Compiler kann die Sammlung auf dem Stack statt auf dem Heap speichern.
Damit ein Auflistungsausdruck für einen ref struct-Typ den Deklarationsblockumgehen kann, kann es notwendig sein, den Ausdruck auf einen anderen Typ zu übertragen.
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
}
Typinferenz
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;
Die Typ-Inferenz-Regeln werden wie folgt aktualisiert.
Die bestehenden Regeln für die Erste Phase werden in einen neuen Abschnitt Eingabetyp-Inferenz extrahiert, und eine Regel wird zu Eingabetyp-Inferenz und Ausgabetyp-Inferenz für Sammlungsausdrücke hinzugefügt.
11.6.3.2 Die erste Phase
Für jedes der Methodenargumente
Eᵢ
:
- Eine Eingabetyp-Inferenz wird von
Eᵢ
zu dem entsprechenden ParametertypTᵢ
gemacht.Eine Eingabetyp-Inferenz wird von einem Ausdruck
E
zu einem TypT
auf folgende Weise vorgenommen:
- Wenn
E
ein Sammlungsausdruck mit ElementenEᵢ
ist, undT
ist ein Typ mit einem ElementtypTₑ
oderT
ist ein nullbarer WerttypT0?
undT0
hat einen ElementtypTₑ
, dann gilt für jedesEᵢ
:
- Wenn
Eᵢ
ein Ausdruckselementist, dann wird eine Eingabetyp-Inferenz gemacht vonEᵢ
nachTₑ
.- Wenn
Eᵢ
ein Spreizungselement mit einem IterationstypSᵢ
ist, dann wird eine Untergrenze-Inferenz von nachSᵢ
bisTₑ
gemacht.- [bestehende Regeln aus der ersten Phase] ...
11.6.3.7 Ausgabetyp-Inferenzen
Eine Ausgangstyp-Inferenz wird von einem Ausdruck
E
zu einem TypT
auf folgende Weise vorgenommen:
- Wenn
E
ein Sammlungsausdruck mit ElementenEᵢ
ist, undT
ist ein Typ mit einem ElementtypTₑ
oderT
ist ein nullbarer WerttypT0?
undT0
hat einen ElementtypTₑ
, dann gilt für jedesEᵢ
:
- Wenn
Eᵢ
ein Ausdruckselementist, dann wird eine Ausgangstyp-Inferenz gemacht vonEᵢ
nachTₑ
.- Wenn
Eᵢ
ein Spread-Elementist, wird keine Ableitung ausEᵢ
hergestellt.- [vorhandene Regeln aus Ausgabetypinferenz] ...
Erweiterungsmethoden
Keine Änderungen an den Regeln für Aufrufe von Erweiterungsmethoden .
12.8.10.3 Erweiterungsmethodeaufrufe
Eine Erweiterungsmethode
Cᵢ.Mₑ
ist wählbar , wenn:
- ...
- Es gibt eine implizite Identitäts-, Referenz- oder Boxing-Konvertierung von expr zum Typ des ersten Parameters von
Mₑ
.
Ein Auflistungsausdruck hat keinen natürlichen Typ, sodass die vorhandenen Konvertierungen vom Typ auf Typ nicht anwendbar sind. Daher kann ein Sammlungsausdruck nicht direkt als erster Parameter für einen Aufruf einer Erweiterungsmethode verwendet werden.
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
Überladungsauflösung
Bessere Konvertierung von Ausdruck wurde aktualisiert, um bestimmte Zieltypen bei der Konvertierung von Sammelausdrücken zu bevorzugen.
In den aktualisierten Regeln:
- Ein span_type ist eines von:
System.Span<T>
-
System.ReadOnlySpan<T>
.
- Ein array_oder_array_interface ist eines von:
- ein Array Typ
- einer der folgenden Schnittstellentypen , die von einem Array-Typimplementiert werden:
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>
Aufgrund einer impliziten Konvertierung
C₁
, die von einem AusdruckE
in einen TypT₁
konvertiert wird, und einer impliziten KonvertierungC₂
, die von einem AusdruckE
in einen TypT₂
konvertiert wird, istC₁
eine bessere Konvertierung alsC₂
, wenn einer der folgenden Haltebereiche gilt:
E
ist ein Sammlungsausdruck und eine der folgenden Bedingungen ist erfüllt:
T₁
istSystem.ReadOnlySpan<E₁>
, undT₂
istSystem.Span<E₂>
, und eine implizite Konvertierung ist vonE₁
inE₂
vorhanden.T₁
istSystem.ReadOnlySpan<E₁>
oderSystem.Span<E₁>
, undT₂
ist ein array_oder_array_interface mit ElementtypE₂
, und es gibt eine implizite Umwandlung vonE₁
nachE₂
T₁
ist kein span_type, undT₂
ist kein span_type, und eine implizite Umwandlung vonT₁
zuT₂
existiert.E
ist kein Sammlungs-Ausdruck und eine der folgenden Bedingungen gilt:
E
stimmt genau mitT₁
überein, undE
stimmt nicht genau mitT₂
E
stimmt genau entweder mit beiden vonT₁
undT₂
überein oder mit keinem von beiden, undT₁
ist ein besseres Konvertierungsziel alsT₂
E
ist eine Methodengruppe, ...
Beispiele für Unterschiede bei der Überladungsauflösung zwischen Arrayinitialisierern und Auflistungsausdrücken:
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
Span-Typen
Die span-Typen ReadOnlySpan<T>
und Span<T>
sind beide konstruierbare Sammlungstypen. Die Unterstützung für sie folgt dem Design für params Span<T>
. Genauer gesagt, führt das Konstruieren einer dieser Spans zu einem Array T[], das auf dem Stapel erstellt wird, wenn das Array params innerhalb der vom Compiler festgelegten Grenzen liegt (falls vorhanden). Andernfalls wird das Array auf dem Heap zugewiesen.
Wenn der Compiler sich dafür entscheidet, auf dem Stack zu allozieren, ist es nicht erforderlich, ein Literal direkt in ein stackalloc
an diesem bestimmten Punkt zu übersetzen. Angenommen, dies liegt vor:
foreach (var x in y)
{
Span<int> span = [a, b, c];
// do things with span
}
Der Compiler darf das mit stackalloc
übersetzen, solange die Bedeutung von Span
gleich bleibt und span-safety beibehalten wird. Zum Beispiel kann es den obigen Text folgendermaßen übersetzen:
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
}
Der Compiler kann auch Inline-Arraysverwenden, falls verfügbar, wenn er sich für eine Allokation auf dem Stack entscheidet. Beachten Sie, dass in C# 12 Inlinearrays nicht mit einem Auflistungsausdruck initialisiert werden können. Dieses Feature ist ein offener Vorschlag.
Wenn der Compiler sich für eine Allokation auf dem Heap entscheidet, ist die Übersetzung für Span<T>
einfach:
T[] __array = [...]; // using existing rules
Span<T> __result = __array;
Sammlung Literalübersetzung
Ein Sammlungsausdruck hat eine bekannte Länge , wenn der Kompilierzeittyp jedes Sammelelements im Sammlungsausdruck abzählbarist.
Schnittstellenübersetzung
Nicht veränderbare Schnittstellenübersetzung
Bei einem Zieltyp, der keine verändernden Mitglieder enthält, nämlich IEnumerable<T>
, IReadOnlyCollection<T>
und IReadOnlyList<T>
, ist eine konforme Implementierung erforderlich, um einen Wert zu erzeugen, der diese Schnittstelle implementiert. Wenn ein Typ synthetisiert wird, wird empfohlen, dass der synthetisierte Typ alle diese Schnittstellen sowie ICollection<T>
und IList<T>
implementiert, unabhängig davon, welcher Schnittstellentyp gezielt war. Dies gewährleistet eine maximale Kompatibilität mit vorhandenen Bibliotheken, einschließlich solcher, die die von einem Wert implementierten Schnittstellen betrachten, um Performance-Optimierungen zu beleuchten.
Darüber hinaus muss der Wert die nichtgenerischen ICollection
und IList
Schnittstellen implementieren. Auf diese Weise können Sammlungsausdrücke die dynamische Introspektion in Szenarien wie datenbindung unterstützen.
Eine kompatible Implementierung ist kostenlos für:
- Verwenden Sie einen vorhandenen Typ, der die erforderlichen Schnittstellen implementiert.
- Synthetisieren Sie einen Typ, der die erforderlichen Schnittstellen implementiert.
In beiden Fällen darf der verwendete Typ einen größeren Satz von Schnittstellen implementieren, als unbedingt erforderlich ist.
Synthetisierte Typen können jede Strategie verwenden, mit der sie die erforderlichen Schnittstellen ordnungsgemäß implementieren möchten. Beispielsweise kann ein synthetisierter Typ die Elemente direkt in sich selbst inlineieren und die Notwendigkeit zusätzlicher interner Sammlungszuweisungen vermeiden. Ein synthetisierter Typ konnte auch keinen Speicher verwenden, indem er die Werte direkt berechnet. Zum Beispiel die Rückgabe von index + 1
für [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
.
- Der Wert muss
true
zurückgeben, wenn er fürICollection<T>.IsReadOnly
(falls implementiert) und nicht-generischeIList.IsReadOnly
undIList.IsFixedSize
abgefragt wird. Dadurch wird sichergestellt, dass Verbraucher erkennen können, dass es sich um eine nicht veränderbare Sammlung handelt, obwohl veränderbare Ansichten implementiert wurden. - Der Wert muss bei jedem Aufruf einer Mutationsmethode (wie
IList<T>.Add
) ausgelöst werden. Dies sorgt für Sicherheit und verhindert, dass eine nicht veränderbare Sammlung versehentlich verändert wird.
Änderbare Schnittstellenübersetzung
Gegebener Zieltyp, der veränderliche Mitglieder enthält, nämlich ICollection<T>
oder IList<T>
:
- Der Wert muss eine Instanz von
List<T>
sein.
Übersetzung der bekannten Länge
Eine bekannte Länge ermöglicht eine effiziente Konstruktion eines Ergebnisses ohne das Kopieren von Daten und ohne unnötigen Schlupfspeicher in einem Ergebnis.
Das Fehlen einer bekannten Länge verhindert nicht, dass ein Ergebnis erstellt wird. Dies kann jedoch zu zusätzlichen CPU- und Arbeitsspeicherkosten führen, wenn die Daten erzeugt und dann zum endgültigen Ziel verschoben werden.
Für ein Bekannte Länge-Literal
[e1, ..s1, etc]
beginnt die Übersetzung zunächst mit folgendem:int __len = count_of_expression_elements + __s1.Count; ... __s_n.Count;
Angesichts eines Zieltyps
T
für dieses Literal:Wenn
T
einT1[]
ist, dann wird das Wörtchen mit übersetzt:T1[] __result = new T1[__len]; int __index = 0; __result[__index++] = __e1; foreach (T1 __t in __s1) __result[__index++] = __t; // further assignments of the remaining elements
Die Implementierung darf andere Mittel verwenden, um das Array aufzufüllen. Verwenden Sie beispielsweise effiziente Massenkopiemethoden wie
.CopyTo()
.Wenn
T
einSpan<T1>
ist, wird das Literal wie oben beschrieben übersetzt, mit der Ausnahme, dass die__result
Initialisierung wie folgt übersetzt wird:Span<T1> __result = new T1[__len]; // same assignments as the array translation
Die Übersetzung kann
stackalloc T1[]
oder ein Inline-Array anstelle vonnew T1[]
verwenden, wenn span-safety beibehalten wird.Wenn
T
einReadOnlySpan<T1>
ist, dann wird das Literal genauso übersetzt wie im FallSpan<T1>
, außer dass das Endergebnis ist, dassSpan<T1>
implizit in einReadOnlySpan<T1>
umgewandelt wurde.Ein
ReadOnlySpan<T1>
, bei demT1
ein primitiver Typ ist und alle Elemente der Sammlung konstant sind, braucht seine Daten nicht auf dem Heap oder dem Stack zu haben. Beispielsweise könnte eine Implementierung diese Spanne direkt als Verweis auf einen Teil des Datensegments des Programms erstellen.Die obigen Formulare (für Arrays und Spannweiten) sind die Basisdarstellungen des Sammlungsausdrucks und werden für die folgenden Übersetzungsregeln verwendet:
Wenn
T
einC<S0, S1, …>
ist, das eine entsprechende ErstellungsmethodeB.M<U0, U1, …>()
hat, dann wird das Literal wie folgt übersetzt:// Collection literal is passed as is as the single B.M<...>(...) argument C<S0, S1, …> __result = B.M<S0, S1, …>([...])
Da die create-Methode einen Argumenttyp eines instanziierten
ReadOnlySpan<T>
haben muss, gilt die Übersetzungsregel für Spans, wenn der Ausdruck für die Sammlung an die create-Methode übergeben wird.Wenn
T
Sammlungsinitialisiererunterstützt, dann:wenn der Typ
T
einen barrierefreien Konstruktor mit einem einzelnen Parameterint capacity
enthält, wird das Literal übersetzt als:T __result = new T(capacity: __len); __result.Add(__e1); foreach (var __t in __s1) __result.Add(__t); // further additions of the remaining elements
Hinweis: Der Name des Parameters muss
capacity
werden.Dieses Formular ermöglicht es, einen Literalwert zu verwenden, um den neu erstellten Typ in Bezug auf die Anzahl der Elemente zu spezifizieren, um eine effiziente Zuweisung des internen Speichers zu ermöglichen. Dies vermeidet verschwenderische Neuzuweisungen, wenn die Elemente hinzugefügt werden.
andernfalls wird das Literal wie folgt übersetzt:
T __result = new T(); __result.Add(__e1); foreach (var __t in __s1) __result.Add(__t); // further additions of the remaining elements
Dies ermöglicht das Erstellen des Zieltyps, wenn auch ohne Kapazitätsoptimierung, um die interne Verschiebung des Speichers zu verhindern.
Übersetzung unbekannter Länge
Gegeben ein Zieltyp
T
für ein unbekannte Länge Literal:Wenn
T
Sammlungsinitialisiererunterstützt, dann wird das Literal als übersetzt:T __result = new T(); __result.Add(__e1); foreach (var __t in __s1) __result.Add(__t); // further additions of the remaining elements
Dies ermöglicht die Ausbreitung jeglicher iterierbarer Typen, auch wenn dies mit dem geringsten Maß an Optimierung erfolgt.
Wenn
T
einigeT1[]
ist, hat das Literal die gleiche Semantik wie:List<T1> __list = [...]; /* initialized using predefined rules */ T1[] __result = __list.ToArray();
Die obige Methode ist jedoch ineffizient; sie erstellt die Zwischenliste und erstellt daraus eine Kopie des endgültigen Arrays. Implementierungen haben die Freiheit, dies wegzuoptimieren, zum Beispiel Code zu erzeugen wie folgt:
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);
Dies ermöglicht minimale Verschwendung und Kopieren, ohne dass zusätzlichen Aufwand für Bibliothekssammlungen entstehen kann.
Die an
CreateArray
übergebenen Zählungen werden verwendet, um einen Hinweis auf die Anfangsgröße bereitzustellen, um unnötige Größenänderungen zu vermeiden.Wenn
T
ein span-Typist, kann eine Implementierung der obigenT[]
-Strategie oder einer anderen Strategie mit der gleichen Semantik, aber besserer Leistung folgen. Anstatt z. B. das Array als Kopie der Listenelemente zuzuweisen, könnteCollectionsMarshal.AsSpan(__list)
verwendet werden, um einen Spanwert direkt abzurufen.
Nicht unterstützte Szenarien
Während Sammlungsliterale für viele Szenarien verwendet werden können, gibt es einige, die sie nicht ersetzen können. Dazu gehören:
- Mehrdimensionale Arrays (z. B.
new int[5, 10] { ... }
). Es gibt keine Möglichkeit, die Dimensionen einzubeziehen, und alle Sammlungsliterale sind entweder nur lineare oder nur Kartenstrukturen. - Auflistungen, die spezielle Werte an ihre Konstruktoren übergeben. Es gibt keine Möglichkeit, auf den verwendeten Konstruktor zuzugreifen.
- Verschachtelte Sammlungsinitialisierer, z.B.
new Widget { Children = { w1, w2, w3 } }
. Dieses Formular muss bleiben, da es eine sehr unterschiedliche Semantik vonChildren = [w1, w2, w3]
hat. Ersteres ruft.Add
wiederholt auf.Children
auf, während letzteres eine neue Sammlung über.Children
zuweisen würde. Wir könnten in Erwägung ziehen, dass die letztere Form auf das Hinzufügen zu einer bestehenden Sammlung zurückgreift, wenn.Children
nicht zugewiesen werden kann, aber das scheint extrem verwirrend zu sein.
Syntaxunklarheiten
Es gibt zwei echte syntaktische Mehrdeutigkeiten, bei denen es mehrere legale syntaktische Interpretationen des Codes gibt, der eine
collection_literal_expression
verwendet.Das
spread_element
ist zweideutig mit einemrange_expression
. Man könnte technisch folgendes haben:Range[] ranges = [range1, ..e, range2];
Um dies zu beheben, können wir eine der folgenden Aktionen ausführen:
- Erfordern Sie, dass Benutzer
(..e)
in Klammern setzen oder einen Startindex0..e
einschließen, wenn sie einen Bereich festlegen wollen. - Wählen Sie eine andere Syntax (z. B.
...
) für die Verbreitung aus. Dies wäre bedauerlich für die mangelnde Konsistenz mit Schnittmustern.
- Erfordern Sie, dass Benutzer
Es gibt zwei Fälle, in denen es keine echte Mehrdeutigkeit gibt, aber in denen die Syntax die Analysekomplexität erheblich erhöht. Obwohl es kein Problem hinsichtlich der Ingenieurzeit darstellt, erhöht dies dennoch den kognitiven Aufwand für Benutzer beim Betrachten des Codes.
Zweideutigkeit zwischen
collection_literal_expression
undattributes
bei Anweisungen oder lokalen Funktionen. Erwägen:[X(), Y, Z()]
Dies kann eine der folgenden Sein:
// A list literal inside some expression statement [X(), Y, Z()].ForEach(() => ...); // The attributes for a statement or local function [X(), Y, Z()] void LocalFunc() { }
Ohne ein komplexes Voraussehen wäre es unmöglich zu sagen, ohne die Gesamtheit des Wörtlichen zu verbrauchen.
Optionen zur Behebung dieses Problems umfassen folgende:
- Lassen Sie dies zu, indem Sie die Analysearbeiten ausführen, um zu bestimmen, welche dieser Fälle dies ist.
- Lassen Sie dies nicht zu und verlangen Sie, dass der Benutzer das Literal in Klammern wie
([X(), Y, Z()]).ForEach(...)
einschließt. - Zweideutigkeit zwischen einem
collection_literal_expression
in einemconditional_expression
und einemnull_conditional_operations
. Erwägen:
M(x ? [a, b, c]
Dies kann eine der folgenden Sein:
// 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]);
Ohne ein komplexes Voraussehen wäre es unmöglich zu sagen, ohne die Gesamtheit des Wörtlichen zu verbrauchen.
Hinweis: Dies ist auch ohne einen natürlichen Typ ein Problem, da die Zieltypisierung durch
conditional_expressions
erfolgt.Wie bei den anderen könnten wir Klammern erfordern, um Unklarheiten zu vermeiden. Mit anderen Worten, nehmen Sie die
null_conditional_operation
-Auslegung an, es sei denn, es ist wie folgt geschrieben:x ? ([1, 2, 3]) :
. Das scheint jedoch ziemlich unglücklich zu sein. Diese Art von Code scheint nicht unvernünftig zu schreiben und wird wahrscheinlich Leute stolpern lassen.
Nachteile
- Damit wird eine weitere Form für Sammlungsausdrücke eingeführt, zusätzlich zu den unzähligen Möglichkeiten, die wir bereits haben. Dies ist eine zusätzliche Komplexität für die Sprache. Dies ermöglicht jedoch auch die Vereinheitlichung einer
-Ring--Syntax, die alle regiert, was bedeutet, dass bestehende Codebasen vereinfacht und überall in ein einheitliches Erscheinungsbild gebracht werden können. - Die Verwendung von
[
...]
anstelle von{
...}
weicht von der Syntax ab, die wir in der Regel bereits für Arrays und Sammelinitialisierer verwendet haben. Es verwendet insbesondere[
...]
anstelle von{
...}
. Dies wurde jedoch bereits vom Sprachteam vereinbart, als wir Listenmuster festlegten. Wir haben versucht,{
...}
mit Listenmustern zum Laufen zu bringen, aber dabei traten unüberwindbare Probleme auf. Aus diesem Grund zogen wir zu[
...]
, die sich, während sie für C# neu ist, in vielen Programmiersprachen natürlich fühlt und uns erlaubte, ohne Mehrdeutigkeit neu zu beginnen. Die Verwendung von[
...]
als entsprechende Literalform ergänzt unsere neuesten Entscheidungen und bietet uns eine saubere Grundlage, um ohne Probleme zu arbeiten.
Dies bringt Fehler in die Sprache. Zum Beispiel sind die folgenden Beispiele sowohl legal als auch (glücklicherweise) bedeutungsgleich.
int[] x = { 1, 2, 3 };
int[] x = [ 1, 2, 3 ];
Angesichts der Breite und Konsistenz, die durch die neue Literalsyntax gebracht wird, sollten wir jedoch in Betracht ziehen, dass Benutzer zur neuen Form wechseln. IDE-Vorschläge und Korrekturen könnten in diesem Zusammenhang hilfreich sein.
Alternativen
- Welche anderen Designs wurden berücksichtigt? Welche Auswirkungen hat es, dies nicht zu tun?
Gelöste Fragen
Soll der Compiler
stackalloc
für die Stapelzuweisung verwenden, wenn Inline-Arrays nicht verfügbar sind und der Iterationstyp ein primitiver Typ ist?Entschluss: Nein. Das Verwalten eines
stackalloc
Puffers erfordert mehr Aufwand im Vergleich zu einem Inlinearray, um sicherzustellen, dass der Puffer nicht wiederholt zugewiesen wird, wenn sich der Sammlungsausdruck in einer Schleife befindet. Die zusätzliche Komplexität im Compiler und im generierten Code überwiegt den Vorteil der Stapelzuweisung auf älteren Plattformen.In welcher Reihenfolge sollten literale Elemente im Vergleich zur Auswertung der Length/Count-Eigenschaft ausgewertet werden? Sollten wir zuerst alle Elemente auswerten, dann alle Längen? Oder sollten wir ein Element auswerten, dann seine Länge, dann das nächste Element usw.?
Lösung: Zunächst werden alle Elemente ausgewertet, dann folgt alles andere.
Kann ein unbekannte Länge-Literal einen Sammlungstyp erstellen, der eine bekannte Längebenötigt, wie eine Array-, Span- oder Construct(array/span)-Sammlung? Dies wäre schwieriger, effizient zu tun, aber es könnte durch den geschickten Einsatz von gepoolten Arrays und/oder Buildern möglich sein.
Lösung: Ja, wir erlauben die Erstellung einer Sammlung mit fester Länge aus einem Literal unbekannter Länge . Der Compiler darf dies so effizient wie möglich implementieren.
Der folgende Text ist vorhanden, um die ursprüngliche Diskussion dieses Themas aufzuzeichnen.
Benutzer können ein unbekannte Länge-Literal jederzeit in ein bekannte Länge-Literal umwandeln, und zwar mit Code wie diesem:
ImmutableArray<int> x = [a, ..unknownLength.ToArray(), b];
Dies ist jedoch aufgrund der Notwendigkeit, die Zuteilung temporärer Speicher zu erzwingen, bedauerlich. Wir könnten potenziell effizienter sein, wenn wir kontrollierten, wie dies ausgegeben wurde.
Kann eine
collection_expression
auf eineIEnumerable<T>
oder andere Sammlungsschnittstellen ausgerichtet werden?Zum Beispiel:
void DoWork(IEnumerable<long> values) { ... } // Needs to produce `longs` not `ints` for this to work. DoWork([1, 2, 3]);
Lösung: Ja, ein Literal kann auf jeden Interfacetyp
I<T>
, denList<T>
implementiert, als Ziel gesetzt werden. Beispiel:IEnumerable<long>
. Dies ist dasselbe wie eine Zieltypisierung aufList<long>
und die anschließende Zuweisung dieses Ergebnisses an den angegebenen Schnittstellentyp. Der folgende Text ist vorhanden, um die ursprüngliche Diskussion dieses Themas aufzuzeichnen.Die offene Frage hier ist, zu bestimmen, welcher zugrunde liegende Typ tatsächlich erstellt werden soll. Eine Möglichkeit ist, sich den Vorschlag für
params IEnumerable<T>
anzusehen. Dort würden wir ein Array generieren, das die Werte weitergibt, ähnlich wie beiparams T[]
.Kann/sollte der Compiler
Array.Empty<T>()
für[]
ausgeben? Sollte dies vorgeschrieben werden, um Zuweisungen nach Möglichkeit zu vermeiden?Ja. Der Compiler sollte
Array.Empty<T>()
für jeden Fall ausgeben, in dem dies legal ist und das Endergebnis nicht änderbar ist. Zum Beispiel, wenn manT[]
,IEnumerable<T>
,IReadOnlyCollection<T>
oderIReadOnlyList<T>
anvisiert. Es sollte nichtArray.Empty<T>
verwendet werden, wenn das Ziel änderbar ist (ICollection<T>
oderIList<T>
).Sollten wir die Sammlungsinitialisierer erweitern, um nach der sehr häufigen Methode
AddRange
zu suchen? Es könnte von dem zugrunde liegenden konstruierten Typ verwendet werden, um das Hinzufügen von Spread-Elementen potenziell effizienter durchzuführen. Wir sollten vielleicht auch nach Dingen wie.CopyTo
suchen. Hier kann es zu Nachteilen kommen, da diese Methoden am Ende zu übermäßigen Zuweisungen/Versendungen im Vergleich zur direkten Aufzählung im übersetzten Code führen können.Ja. Eine Implementierung darf andere Methoden verwenden, um einen Sammlungswert zu initialisieren, unter der Annahme, dass diese Methoden über eine klar definierte Semantik verfügen und dass Sammlungstypen "gut verhalten" sein sollten. In der Praxis sollte eine Implementierung jedoch vorsichtig sein, da Vorteile in einer Hinsicht (Massenkopieren) auch negative Folgen haben können (z. B. das Boxen einer Struktursammlung).
Eine Implementierung sollte die Vorteile nutzen, wo es keine Nachteile gibt. Beispielsweise mit einer
.AddRange(ReadOnlySpan<T>)
-Methode.
Ungelöste Fragen
- Sollten wir das Ableiten des Elementtyps des zulassen, wenn der Iterationstyp des "mehrdeutig" ist (nach irgendeiner Definition)? Zum Beispiel:
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;
}
Sollte es legal sein, ein Sammlungsliteral zu erstellen und sofort zu indizieren? Hinweis: Dies erfordert eine Antwort auf die unten stehende ungelöste Frage, ob Sammlungsliterale einen natürlichen Typhaben.
Stapelzuweisungen für riesige Sammlungen könnten den Stapel sprengen. Sollte der Compiler eine Heuristik zum Platzieren dieser Daten auf dem Heap haben? Sollte die Sprache nicht angegeben werden, um diese Flexibilität zu ermöglichen? Wir sollten den Spezifikationen für
params Span<T>
folgen.Müssen wir
spread_element
als Zieltyp festlegen? Betrachten Sie z. B.:Span<int> span = [a, ..b ? [c] : [d, e], f];
Hinweis: Dies kann häufig in der folgenden Form auftreten, um die bedingte Einbeziehung einiger Elemente oder nichts zuzulassen, wenn die Bedingung falsch ist:
Span<int> span = [a, ..b ? [c, d, e] : [], f];
Um dieses vollständige Literal auszuwerten, müssen wir die darin enthaltenen Elementausdrücke auswerten. Das bedeutet, dass man
b ? [c] : [d, e]
auswerten kann. Ohne einen Zieltyp, der diesen Ausdruck im Kontext auswertet, und ohne irgendeine Art von natürlichem Typwären wir jedoch nicht in der Lage festzustellen, was wir mit[c]
oder[d, e]
hier tun sollen.Um dies zu beheben, könnten wir sagen, dass beim Auswerten des
spread_element
Ausdrucks eines Literals ein impliziter Zieltyp vorhanden war, der dem Zieltyp des Literals selbst entspricht. Das würde also im obigen Sinne wie folgt umgeschrieben: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;
Die Spezifikation eines konstruierbaren Sammlungstyps unter Verwendung einer Erstellungsmethode ist empfindlich gegenüber dem Kontext, in dem die Konvertierung klassifiziert wird
Die Existenz der Umwandlung hängt in diesem Fall von der Vorstellung eines Iterationstyps des Sammlungstypsab. Wenn eine Erstellungs-Methode vorhanden ist, die ein ReadOnlySpan<T>
akzeptiert, wobei T
der Iterationstypist, existiert die Umwandlung. Andernfalls nicht.
Ein Iterationstyp ist jedoch gegenüber dem Kontext empfindlich, in dem foreach
erfolgt. Für denselben Sammlungstyp kann es je nach den zur Verfügung stehenden Erweiterungsmethoden unterschiedlich sein, und es kann auch undefiniert sein.
Das ist für den Zweck von foreach
in Ordnung, wenn der Typ nicht so konzipiert ist, dass er sich selbst vorhersehen lässt. Wenn dies der Fall ist, können Erweiterungsmethoden nicht ändern, wie der Typ vorhergesagt wird, unabhängig vom Kontext.
Das fühlt sich jedoch etwas seltsam an, wenn eine Konvertierung kontextempfindlich ist. Tatsächlich ist die Konvertierung "instabil". Ein Sammlungstyp , der ausdrücklich konstruierbar sein soll, darf die Definition eines sehr wichtigen Details auslassen- seinen Iterationstyp. Lassen Sie den Typ "nicht konvertierbar" auf sich selbst.
Hier ist ein Beispiel:
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.
}
}
}
Wenn der Typ Iterationstyp selbst nicht definiert, kann der Compiler aufgrund des aktuellen Entwurfs keine Anwendung eines CollectionBuilder
-Attributs zuverlässig überprüfen. Wenn wir den Iterationstypnicht kennen, wissen wir nicht, welche Signatur der Create-Methode sein sollte. Wenn der Iterationstyp aus dem Kontext stammt, besteht keine Garantie dafür, dass der Typ immer in einem ähnlichen Kontext verwendet wird.
Params Collections Funktion ist ebenfalls davon betroffen. Es fühlt sich seltsam an, dass der Elementtyp eines params
-Parameters am Deklarationspunkt nicht zuverlässig vorhergesagt werden kann. Der aktuelle Vorschlag muss außerdem sicherstellen, dass die Erstellungsmethode mindestens so barrierefrei ist wie der params
Sammlungstyp. Es ist unmöglich, diese Überprüfung zuverlässig durchzuführen, es sei denn, der Sammlungstyp definiert seinen Iterationstyp selbst.
Beachten Sie, dass wir auch https://github.com/dotnet/roslyn/issues/69676 für Compiler geöffnet haben, was im Grunde dasselbe Problem beobachtet, aber aus der Perspektive der Optimierung darüber spricht.
Vorschlag
Erfordert einen Typ, der CollectionBuilder
Attribut verwendet, um seinen Iterationstyp für sich selbst zu definieren.
Anders ausgedrückt bedeutet dies, dass der Typ entweder IEnumarable
/IEnumerable<T>
implementieren soll, oder dass er über eine öffentliche GetEnumerator
Methode mit der richtigen Signatur verfügen sollte (dies schließt alle Erweiterungsmethoden aus).
Außerdem muss die Methode Erstellen "dort zugänglich sein, wo der Sammlungsausdruck verwendet wird". Dies ist ein weiterer Punkt der Kontextabhängigkeit basierend auf der Barrierefreiheit. Der Zweck dieser Methode ist dem Zweck einer benutzerdefinierten Konvertierungsmethode sehr ähnlich und muss öffentlich sein. Daher sollten wir erwägen, dass auch die Erstellen-Methode öffentlich sein muss.
Schlussfolgerung
Genehmigt mit Änderungen LDM-2024-01-08
Der Begriff Iterationstyp wird nicht konsistent auf Umwandlungenangewendet.
- Zu einem Konstrukt oder Klassentyp , der
System.Collections.Generic.IEnumerable<T>
implementiert, wobei:
- Für jedes -Element
Ei
gibt es eine implizite Konvertierung inT
.
Es sieht so aus, als würde angenommen, dass T
der Iterationstyp des Konstrukts oder Klassentyps in diesem Fall notwendig ist.
Diese Annahme ist jedoch falsch. Das kann zu einem sehr seltsamen Verhalten führen. Zum Beispiel:
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" };
}
}
- Zu einem Konstrukt oder Klassentyp , der
System.Collections.IEnumerable
implementiert und nichtSystem.Collections.Generic.IEnumerable<T>
implementiert.
Es sieht so aus, als ob bei der Implementierung davon ausgegangen wird, dass der Iterationstypobject
ist, aber die Spezifikation lässt diese Tatsache unbestimmt und erfordert nicht, dass jedes Element in etwas konvertiert werden muss. Im Allgemeinen ist der Iterationstyp jedoch nicht unbedingt der object
-Typ. Dies kann im folgenden Beispiel beobachtet werden:
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
}
}
}
Das Konzept des Iterationstyps ist grundlegend für Params Collections Feature. Und dieses Problem führt zu einer seltsamen Diskrepanz zwischen den beiden Features. Zum Beispiel:
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)
{
}
}
Es wird wahrscheinlich gut sein, sich in die eine oder andere Richtung zu orientieren.
Vorschlag
Geben Sie die Konvertierung von Struktur oder Klassentyp an, die System.Collections.Generic.IEnumerable<T>
oder System.Collections.IEnumerable
im Hinblick auf Iterationstyp implementiert und eine implizite Konvertierung für jedes ElementEi
in den Iterationstyperfordert.
Schlussfolgerung
Approved LDM-2024-01-08
Sollte die Konvertierung von Sammlungsausdrücken die Verfügbarkeit eines minimalen Satzes von APIs für die Konstruktion erfordern?
Ein konstruierbarer Sammlungstyp gemäß Konvertierungen kann in Wirklichkeit nicht konstruierbar sein, was zu einem unerwarteten Verhalten bei der Auflösung von Überlasten führen kann. Zum Beispiel:
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[])'
}
}
Das 'C1.M1(string)' ist kein Kandidat, der verwendet werden kann, weil:
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?)
Hier ist ein weiteres Beispiel mit einem benutzerdefinierten Typ und einem stärkeren Fehler, der nicht einmal einen gültigen Kandidaten erwähnt:
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;
}
Es sieht so aus, als ob die Situation sehr ähnlich ist wie bei der Methodengruppe zum Delegieren von Conversions. D.h. es gab Szenarien, in denen die Konvertierung existierte, aber falsch war. Wir haben beschlossen, dies zu verbessern, indem wir sicherstellen, dass, wenn die Konvertierung fehlerhaft ist, sie nicht existiert.
Beachten Sie, dass mit dem Feature "Params Collections" ein ähnliches Problem auftreten wird. Es kann sinnvoll sein, die Verwendung des params
Modifikators bei nicht konstruierbaren Sammlungen zu verbieten. Im aktuellen Vorschlag basiert diese Prüfung jedoch auf dem Abschnitt Konvertierungen . Hier ist ein Beispiel:
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;
}
Es sieht so aus, als ob das Problem zuvor etwas diskutiert wurde, siehe https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-10-02.md#collection-expressions. Zu diesem Zeitpunkt wurde ein Argument gemacht, dass die Regeln, wie sie gerade angegeben sind, konsistent damit sind, wie interpolierte Zeichenfolgenhandler spezifiziert sind. Hier ist ein Zitat:
Insbesondere wurden interpolierte Zeichenfolgenhandler ursprünglich auf diese Weise angegeben, aber wir haben die Spezifikation überarbeitet, nachdem dieses Problem berücksichtigt wurde.
Obwohl es eine Ähnlichkeit gibt, gibt es auch einen wichtigen Unterschied, der berücksichtigt werden sollte. Hier ist ein Zitat aus https://github.com/dotnet/csharplang/blob/main/proposals/csharp-10.0/improved-interpolated-strings.md#interpolated-string-handler-conversion:
Der Typ
T
wird als applicable_interpolated_string_handler_type bezeichnet, wenn er mitSystem.Runtime.CompilerServices.InterpolatedStringHandlerAttribute
versehen ist. Es gibt eine implizite interpolated_string_handler_conversion (Handlerkonvertierung einer interpolierten Zeichenfolge) zuT
, die von einem interpolated_string_expression (interpolierten Zeichenfolgenausdruck) herrührt oder aus einem additive_expression (additiven Ausdruck), der vollständig aus „_interpolated_string_expression_s“ besteht und nur+
-Operatoren verwendet.
Der zu bearbeitende Typ muss über ein spezielles Attribut verfügen, das ein starker Indikator für die Absicht des Autors ist, dass der Typ ein interpolierter Zeichenfolgenhandler sein soll. Es ist fair anzunehmen, dass das Vorhandensein des Attributs kein Zufall ist.
Im Gegensatz dazu bedeutet die Tatsache, dass ein Typ "enumerable" ist, nicht notwendig, dass der Autor beabsichtigt hat, dass der Typ konstruierbar ist. Das Vorhandensein einer Erstellungsmethode, die mit einem [CollectionBuilder(...)]
Attribut auf dem Sammlungstypangezeigt wird, ist ein deutlicher Hinweis auf die Absicht des Autors, dass der Typ konstruierbar sein soll.
Vorschlag
Für ein Konstrukt oder einen Klassentyp , der System.Collections.IEnumerable
implementiert und der keine ErstellungsmethodeKonvertierungen hat, sollten mindestens die folgenden APIs vorhanden sein:
- Ein barrierefreier Konstruktor, der ohne Argumente anwendbar ist.
- Eine zugängliche
Add
Instanz oder Erweiterungsmethode, die mit einem Wert vom Iterationstyp als Argument aufgerufen werden kann.
Für das Params Collections Feature sind solche Typen gültige params
Typen, wenn diese APIs als öffentlich deklariert werden und Instanzmethoden (statt Erweiterung) sind.
Schlussfolgerung
Genehmigt mit Änderungen LDM-2024-01-10
Designbesprechungen
https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-11-01.md#collection-literals https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-09.md#ambiguity-of--in-collection-expressions https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-09-28.md#collection-literals https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-01-08.md https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-01-10.md
Arbeitsgruppenbesprechungen
https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2022-10-06.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2022-10-14.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2022-10-21.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-04-05.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-04-28.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-05-26.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-06-12.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-06-26.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-08-03.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-08-10.md
Anstehende Tagesordnungspunkte
Stapelzuweisungen für riesige Sammlungen könnten den Stapel sprengen. Sollte der Compiler eine Heuristik zum Platzieren dieser Daten auf dem Heap haben? Sollte die Sprache nicht angegeben werden, um diese Flexibilität zu ermöglichen? Wir sollten dem folgen, was die spec/impl für
params Span<T>
tut. Optionen sind:- Immer Stackalloc. Lehren Sie die Menschen, vorsichtig mit Span umzugehen. Dies ermöglicht es, dass Dinge wie
Span<T> span = [1, 2, ..s]
funktionieren und gut laufen, solanges
klein ist. Wenn dies den Stapel sprengen könnte, könnten Benutzer stattdessen immer ein Array erstellen und dann eine Spanne darum herum erhalten. Dies scheint am ehesten dem zu entsprechen, was Menschen möchten, jedoch mit extremer Gefahr. - Nur stackalloc, wenn das Literal eine feste Anzahl von Elementen hat (d.h. keine verteilten Elemente). Dies macht die Dinge dann wahrscheinlich immer sicher, mit festem Stapelverbrauch, und der Compiler kann (hoffentlich) diesen festen Puffer wiederverwenden. Es bedeutet jedoch, dass dinge wie
[1, 2, ..s]
niemals möglich wären, auch wenn der Benutzer weiß, dass es zur Laufzeit völlig sicher ist.
- Immer Stackalloc. Lehren Sie die Menschen, vorsichtig mit Span umzugehen. Dies ermöglicht es, dass Dinge wie
Wie funktioniert die Überladungsauflösung? Wenn eine API folgendes hat:
public void M(T[] values); public void M(List<T> values);
Was geschieht mit
M([1, 2, 3])
? Wir müssen wahrscheinlich "Besserheit" für diese Konvertierungen definieren.Sollten wir die Sammlungsinitialisierer erweitern, um nach der sehr häufigen Methode
AddRange
zu suchen? Es könnte von dem zugrunde liegenden konstruierten Typ verwendet werden, um das Hinzufügen von Spread-Elementen potenziell effizienter durchzuführen. Vielleicht sollten wir auch nach Dingen wie.CopyTo
suchen. Es kann hier zu Nachteilen kommen, da diese Methoden dazu führen können, dass übermäßige Zuordnungen und Verteilungen im Vergleich zur direkten Aufzählung im übersetzten Code entstehen.Generische Typinferenz sollte aktualisiert werden, um Typinformationen zu/von Sammlungsliteralen fließen zu lassen. Zum Beispiel:
void M<T>(T[] values); M([1, 2, 3]);
Es scheint natürlich, dass dies etwas sein sollte, das der Ableitungsalgorithmus bewusst gemacht werden kann. Sobald dies für die 'Basis' konstruierbaren Sammlungstypen (
T[]
,I<T>
,Span<T>
new T()
) unterstützt wird, sollte es auch aus dem FallCollect(constructible_type)
herausfallen. Zum Beispiel:void M<T>(ImmutableArray<T> values); M([1, 2, 3]);
Hier ist
Immutable<T>
über eineinit void Construct(T[] values)
Methode konstruierbar. Daher würde derT[] values
Typ mit Rückschlüssen auf[1, 2, 3]
verwendet werden, was zu einer Ableitung vonint
fürT
führt.Cast/Index Mehrdeutigkeit.
Heute ist der folgende Ausdruck indiziert in
var v = (Expr)[1, 2, 3];
Aber es wäre schön, Dinge wie machen zu können:
var v = (ImmutableArray<int>)[1, 2, 3];
Können/sollten wir hier eine Pause machen?
Syntaktische Zweideutigkeiten mit
?[
.Es kann sinnvoll sein, die Regeln für
nullable index access
zu ändern, um anzugeben, dass kein Platz zwischen?
und[
auftreten kann. Dies wäre eine einschneidende Änderung (aber wahrscheinlich geringfügig, da Visual Studio (VS) dies bereits zusammenführt, wenn man versucht, sie mit einem Leerzeichen einzugeben). Wenn wir dies tun, können wirx?[y]
anders alsx ? [y]
interpretieren.Etwas Ähnliches tritt auf, wenn wir mit https://github.com/dotnet/csharplang/issues/2926fortfahren möchten. In dieser Welt ist
x?.y
zweideutig mitx ? .y
. Wenn wir verlangen, dass das?.
aneinanderstößt, können wir die beiden Fälle syntaktisch trivial unterscheiden.
C# feature specifications