Freigeben über


params Collections

Hinweis

Dieser Artikel ist eine Feature-Spezifikation. 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 Feature-Spezifikation und der abgeschlossenen Implementierung geben. Diese Unterschiede werden in den entsprechenden Hinweisen zum Language Design Meeting (LDM) erfasst.

Weitere Informationen zum Prozess für die Aufnahme von Funktions-Speclets in den C#-Sprachstandard finden Sie im Artikel zu den Spezifikationen.

Champion Issue: https://github.com/dotnet/csharplang/issues/7700

Zusammenfassung

In C# 12 wurde Unterstützung für das Erstellen von Instanzen auch von anderen Auflistungstypen als Arrays hinzugefügt. Siehe Auflistungsausdrücke. Dieser Vorschlag erweitert params die Unterstützung auf alle derartigenAuflistungstypen.

Motivation

Ein params Array-Parameter bietet eine bequeme Möglichkeit, eine Methode aufzurufen, die eine beliebig lange Liste von Argumenten benötigt. Heute muss der Parameter params ein Arraytyp sein. Für einen Entwickler kann es jedoch von Vorteil sein, wenn er beim Aufruf von APIs, die andere Collection-Typen verwenden, über den gleichen Komfort verfügen kann. Beispiele sind ImmutableArray<T>, ReadOnlySpan<T> oder einfache IEnumerable sein. Dies gilt insbesondere in Fällen, in dem der Compiler eine implizite Arrayzuordnung zur Erstellung der Auflistung vermeiden kann (ImmutableArray<T>, ReadOnlySpan<T> usw.).

In Situationen, in denen eine API einen Auflistungstyp verwendet, fügen Entwickler in der Regel eine params-Überladung hinzu, die ein Array akzeptiert, die Zielauflistung erstellt und die ursprüngliche Überladung mit dieser Auflistung aufruft. Daher müssen API-Consumer eine zusätzliche Arrayzuordnung verwenden.

Ein weiterer Vorteil ist die Möglichkeit, eine params-Bereichsüberladung hinzuzufügen, die Vorrang vor der Array-Version hat, indem der vorhandene Quellcode neu kompiliert wird.

Detailliertes Design

Methodenparameter

Der Abschnitt Methodenparameter wird wie folgt geändert.

formal_parameter_list
    : fixed_parameters
-    | fixed_parameters ',' parameter_array
+    | fixed_parameters ',' parameter_collection
-    | parameter_array
+    | parameter_collection
    ;

-parameter_array
+parameter_collection
-    : attributes? 'params' array_type identifier
+    : attributes? 'params' 'scoped'? type identifier
    ;

Eine parameter_collection besteht aus einem Satz optionaler Attribute, einem params-Modifizierer, einem optionalen scoped Modifizierer, einem Typ und einem Bezeichner. Eine Parameter Collection deklariert einen einzelnen Parameter des angegebenen Typs mit dem angegebenen Namen. Beim Typ der Parameterauflistung muss es sich um einen der folgenden gültigen Zieltypen für einen Auflistungsausdruck handeln (siehe https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/collection-expressions.md#conversions):

  • Ein eindimensionaler -ArraytypT[], wobei der -ElementtypT ist.
  • Ein Bereichstyp:
    • System.Span<T>
    • System.ReadOnlySpan<T>
      In diesem Fall ist der Elementtyp T
  • Ein Typ mit einer geeigneten create-Methode, auf die mindestens so zugegriffen werden kann wie auf das deklarierende Element, und einem entsprechenden Elementtyp, der sich aus dieser Determinierung ergibt.
  • Ein Struktur oder Klassentyp, der System.Collections.IEnumerable implementiert, wobei Folgendes gilt:
    • Der Typ verfügt über einen Konstruktor, der ohne Argumente aufgerufen werden kann, und der Konstruktor ist mindestens so zugänglich wie der deklarierende Member.

    • Der Typ weist eine Instanz (keine Erweiterungsmethode) Add auf, wobei Folgendes gilt:

      • Die Methode kann mit einem einzelnen Wertargument aufgerufen werden.
      • Wenn die Methode generisch ist, können die Typargumente vom Argument abgeleitet werden.
      • Die Methode ist mindestens so barrierefrei wie das deklarierende Element.

      In diesem Fall ist der Elementtyp der Iterationstyp des Typs .

  • 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 Elementtyp T

Bei einem Methodenaufruf gestattet eine Parameterauflistung entweder die Angabe eines einzelnen Arguments des angegebenen Parametertyps oder die Angabe von null oder mehr Argumenten des Elementtyps der Auflistung. Parameterauflistungen werden in Parameterauflistungen detaillierter beschrieben.

Eine parameter_collection kann nach einem optionalen Parameter auftreten, darf jedoch keinen Standardwert haben. Das Auslassen von Argumenten für eine parameter_collection würde zur Erstellung einer leeren Auflistung führen.

Parametersammlungen

Der Abschnitt Parameterarrays wird umbenannt und wie folgt geändert.

Ein Parameter, der mit einem params-Modifikator deklariert wird, ist eine Parameterauflistung. Wenn eine formale Parameterliste eine Parameterauflistung enthält, muss es sich um den letzten Parameter in der Liste handeln und muss vom Typ sein, der im Abschnitt Methodenparameter angegeben ist.

Hinweis: Es ist nicht möglich, den params Modifikator mit den Modifikatoren in, outoder refzu kombinieren. Hinweisende

Eine Parameter Collection erlaubt es, Argumente auf eine von zwei Arten in einem Methodenaufruf zu spezifizieren:

  • Das Argument für eine Parameterauflistung kann ein einzelner Ausdruck sein, der implizit in den Typ der Parameterauflistung konvertierbar ist. In diesem Fall verhält sich die Parameter Collection genau wie ein Wertparameter.
  • Alternativ kann der Aufruf null oder mehr Argumente für die Parameter Collection angeben, wobei jedes Argument ein Ausdruck ist, der implizit in den Elementtyp der Parameter Collection konvertierbar ist. In diesem Fall erstellt der Aufruf eine Instanz des Parameterauflistungstyps gemäß den in Auflistungsausdrücke angegebenen Regeln, als ob die Argumente als Ausdruckselemente in einem Auflistungsausdruck in derselben Reihenfolge verwendet würden, und verwendet die neu erstellte Auflistungsinstanz als tatsächliches Argument. Bei der Erstellung der Auflistungsinstanz werden die ursprünglichen nicht konvertierten Argumente verwendet.

Abgesehen von der Möglichkeit, eine variable Anzahl von Argumenten in einem Aufruf zuzulassen, ist eine Parameter Collection genau gleichwertig mit einem Wertparameter desselben Typs.

Bei der Überladungsauflösung kann möglicherweise eine Methode mit einer Parameterauflistung angewendet werden, entweder im normalen Format oder im erweiterten Format. Die erweiterte Form einer Methode ist nur dann verfügbar, wenn die normale Form der Methode nicht anwendbar ist und wenn eine anwendbare Methode mit derselben Signatur wie die erweiterte Form nicht bereits im selben Typ deklariert ist.

Es kann eine Mehrdeutigkeit zwischen dem normalen Format und dem erweiterten Format der Methode mit einem einzelnen Parameterauflistungsargument entstehen, wenn sie als die Parameterauflistung selbst und gleichzeitig als Element der Parameterauflistung verwendet werden kann. Die Mehrdeutigkeit stellt jedoch kein Problem dar, da sie durch Einfügen einer Umwandlung oder die Verwendung eines Auflistungsausdrucks behoben werden kann, wenn notwendig.

Signatures and overloading (Signaturen und Überladen)

Alle Regeln für den params-Modifizierer in Signaturen und Überladung bleiben erhalten.

Anwendbares Funktionselement

Der Abschnitt Anwendbares Funktionselement wird wie folgt geändert.

Wenn ein Funktionsmitglied, das eine Parameter Collection enthält, in seiner normalen Form nicht anwendbar ist, kann das Funktionsmitglied stattdessen in seiner erweiterten Form anwendbar sein:

  • Wenn es sich bei der Parameterauflistung nicht um ein Array handelt, ist eine erweiterte Form nicht für die Sprachversionen C# 12 und darunter anwendbar.
  • Das erweiterte Format wird konstruiert, indem die Parameterauflistung in der Funktionselementdeklaration durch null oder mehr Wertparameter des Elementtyps der Parameterauflistung ersetzt wird, sodass die Anzahl der Argumente in der Argumentliste A der Gesamtzahl der Parameter entspricht. Wenn A weniger Argumente als die Anzahl der festen Parameter in der Funktionsmitglieddeklaration aufweist, kann die erweiterte Form des Funktionsmitglieds nicht erstellt werden und ist somit nicht anwendbar.
  • Andernfalls ist die erweiterte Form anwendbar, wenn für jedes Argument in A eine der folgenden Bedingungen erfüllt ist:
    • der Modus der Parameterübergabe des Arguments ist identisch mit dem Modus der Parameterübergabe des entsprechenden Parameters, und
      • für einen Parameter mit festem Wert oder einen durch die Erweiterung erstellten Wertparameter existiert eine implizite Konversion vom Ausdruck des Arguments zum Typ des entsprechenden Parameters, oder
      • für einen in-, out- oder ref-Parameter ist der Typ des Ausdrucks des Arguments identisch mit dem Typ des entsprechenden Parameters.
    • der Parameterübergabemodus des Arguments ist Wert und der Parameterübergabemodus des entsprechenden Parameters ist Eingabe und es existiert eine implizite Konversion vom Ausdruck des Arguments zum Typ des entsprechenden Parameters

Besseres Funktionselement

Der Abschnitt Besseres Funktionselement wird wie folgt geändert.

Angesichts einer Argumentliste A mit einer Reihe von Argumentausdrücken {E₁, E₂, ..., Eᵥ} und zwei anwendbaren Funktionselementen Mᵥ und Mₓ mit Parametertypen {P₁, P₂, ..., Pᵥ} und {Q₁, Q₂, ..., Qᵥ} wird Mᵥ als ein besseres Funktionselement als Mₓ definiert, wenn

  • für jedes Argument ist die implizite Konversion von Eᵥ nach Qᵥ nicht besser als die implizite Konversion von Eᵥ nach Pᵥ, und
  • für mindestens ein Argument ist die Konversion von Eᵥ nach Pᵥ besser als die Konversion von Eᵥ nach Qᵥ.

Falls die Parametertypsequenzen {P₁, P₂, ..., Pᵥ} und {Q₁, Q₂, ..., Qᵥ} gleichwertig sind (d. h., jede Pᵢ verfügt über eine Identitätskonvertierung in die entsprechende Qᵢ), werden die folgenden Bindestrichregeln angewendet, um den besseren Funktionsmember zu ermitteln.

  • Wenn Mᵢ eine nicht-generische Methode ist und Mₑ eine generische Methode, dann ist Mᵢ besser als Mₑ.
  • Wenn Mᵢ im normalen Format anwendbar ist und Mₑ eine Parameterauflistung besitzt und nur im erweiterten Format anwendbar ist, dann ist Mᵢ besser als Mₑ.
  • Andernfalls, wenn beide Methoden params Collections haben und nur in ihrer erweiterten Form anwendbar sind, und wenn die params-Collection von Mᵢ weniger Elemente hat als die params-Collection von Mₑ, dann ist Mᵢ besser als Mₑ.
  • Andernfalls, wenn Mᵥ spezifischere Parametertypen hat als Mₓ, dann ist Mᵥ besser als Mₓ. Lassen Sie {R1, R2, ..., Rn} und {S1, S2, ..., Sn} die nicht instanziierten und nicht erweiterten Parametertypen von Mᵥ und Mₓdarstellen. Die Parametertypen von Mᵥ sind spezifischer als die von Mₓ, wenn für jeden Parameter Rx nicht weniger spezifisch ist als Sx und für mindestens einen Parameter Rx spezifischer ist als Sx:
    • Ein Typ-Parameter ist weniger spezifisch als ein Nicht-Typ-Parameter.
    • Rekursiv ist ein konstruierter Typ spezifischer als ein anderer konstruierter Typ (mit der gleichen Anzahl von Typargumenten), wenn mindestens ein Typargument spezifischer ist und kein Typargument weniger spezifisch ist als das entsprechende Typargument des anderen.
    • Ein Array-Typ ist spezifischer als ein anderer Array-Typ (mit der gleichen Anzahl von Dimensionen), wenn der Elementtyp des ersten spezifischer ist als der Elementtyp des zweiten.
  • Wenn ein einzelnes Element kein Non-Lifted Operator ist und das andere ein Lifted Operator ist, ist der Non-Lifted Operator besser.
  • Wenn kein Funktionsmitglied als besser eingestuft wurde und alle Parameter von Mᵥ ein entsprechendes Argument haben, während für mindestens einen optionalen Parameter in MₓStandardargumente ersetzt werden müssen, ist Mᵥ besser als Mₓ.
  • Wenn für mindestens einen Parameter Mᵥ die die bessere Wahl für die Parameterübergabe (§12.6.4.4) als der entsprechende Parameter in Mₓ verwendet und keine der Parameter in Mₓ bessere Wahl für die Parameterübergabe als Mᵥ verwenden, ist Mᵥ besser als Mₓ.
  • Andernfalls, wenn beide Methoden Paramsauflistungen aufweisen und nur in ihren erweiterten Formen anwendbar sind, ist Mᵢ besser als Mₑ, wenn derselbe Satz von Argumenten den Auflistungselementen für beide Methoden entspricht und eine der folgenden Bedingungen erfüllt ist (dies entspricht https://github.com/dotnet/csharplang/blob/main/proposals/csharp-13.0/collection-expressions-better-conversion.md):
    • beide params-Auflistungen sind nicht span_type und es gibt eine implizite Konvertierung der params-Auflistung von Mᵢ zur params-Auflistung von Mₑ
    • params-Auflistung von Mᵢ ist System.ReadOnlySpan<Eᵢ>, params-Auflistung von Mₑ ist System.Span<Eₑ> und es gibt eine Identitätskonvertierung von Eᵢ zu Eₑ
    • params-Auflistung von Mᵢ ist System.ReadOnlySpan<Eᵢ> oder System.Span<Eᵢ>, params-Auflistung von Mₑ ist array_or_array_interface__type mit dem ElementtypEₑ und es gibt eine Identitätskonvertierung von Eᵢ zu Eₑ
  • Andernfalls ist kein Funktionsmitglied besser.

Der Grund, warum die neue Entscheidungsregel an das Ende der Liste platziert wird, ist das letzte Unterelement.

  • beide params-Auflistungen sind nicht span_type und es gibt eine implizite Konvertierung der params-Auflistung von Mᵢ zur params-Auflistung von Mₑ

Es ist auf Arrays anwendbar und daher würde eine frühere Anwendung der Entscheidungsregel zu einer Verhaltensänderung bei vorhandenen Szenarien führen.

Zum Beispiel:

class Program
{
    static void Main()
    {
        Test(1);
    }

    static void Test(in int x, params C2[] y) {} // There is an implicit conversion from `C2[]` to `C1[]`
    static void Test(int x, params C1[] y) {} // Better candidate because of "better parameter-passing choice"
}

class C1 {}
class C2 : C1 {}

Wenn eine der vorherigen Entscheidungsregeln (einschließlich der Regel für „bessere Argumentumwandlungen“) angewendet wird, kann das Überladungsauflösungsergebnis anders als in dem Fall sein, in dem stattdessen ein expliziter Auflistungsausdruck als Argument verwendet wird.

Zum Beispiel:

class Program
{
    static void Test1()
    {
        M1(['1', '2', '3']); // IEnumerable<char> overload is used because `char` is an exact match
        M1('1', '2', '3');   // IEnumerable<char> overload is used because `char` is an exact match
    }

    static void M1(params IEnumerable<char> value) {}
    static void M1(params System.ReadOnlySpan<MyChar> value) {}

    class MyChar
    {
        private readonly int _i;
        public MyChar(int i) { _i = i; }
        public static implicit operator MyChar(int i) => new MyChar(i);
        public static implicit operator char(MyChar c) => (char)c._i;
    }

    static void Test2()
    {
        M2([1]); // Span overload is used
        M2(1);   // Array overload is used, not generic
    }

    static void M2<T>(params System.Span<T> y){}
    static void M2(params int[] y){}

    static void Test3()
    {
        M3("3", ["4"]); // Ambiguity, better-ness of argument conversions goes in opposite directions.
        M3("3", "4");   // Ambiguity, better-ness of argument conversions goes in opposite directions.
                        // Since parameter types are different ("object, string" vs. "string, object"), tie-breaking rules do not apply
    }

    static void M3(object x, params string[] y) {}
    static void M3(string x, params Span<object> y) {}
}

Unser primäres Anliegen sind jedoch Szenarien, in denen sich Überladungen nur durch den Parameterauflistungstyp unterscheiden, die Auflistungstypen jedoch denselben Elementtyp aufweisen. Das Verhalten sollte in diesen Fällen mit expliziten Auflistungsausdrücken übereinstimmen.

Die Bedingung „wenn derselbe Satz von Argumenten den Auflistungselementen für beide Methoden entspricht“ ist wichtig für Szenarien wie:

class Program
{
    static void Main()
    {
        Test(x: 1, y: 2); // Ambiguous
    }

    static void Test(int x, params System.ReadOnlySpan<int> y) {}
    static void Test(int y, params System.Span<int> x) {}
}

Es macht keinen Sinn, Collections zu "vergleichen", die aus verschiedenen Elementen erstellt wurden.

Dieser Abschnitt wurde bei LDM überprüft und genehmigt.

Ein Effekt dieser Regeln besteht darin, dass sie bei Aufruf mit einer leeren Argumentliste mehrdeutig sind, wenn params unterschiedlicher Elementtypen verfügbar gemacht werden. Zum Beispiel:

class Program
{
    static void Main()
    {
        // Old scenarios
        C.M1(); // Ambiguous since params arrays were introduced
        C.M1([]); // Ambiguous since params arrays were introduced

        // New scenarios
        C.M2(); // Ambiguous in C# 13
        C.M2([]); // Ambiguous in C# 13
        C.M3(); // Ambiguous in C# 13
        C.M3([]); // Ambiguous in C# 13
    }

    public static void M1(params int[] a) {
    }
    
    public static void M1(params int?[] a) {
    }
    
    public static void M2(params ReadOnlySpan<int> a) {
    }
    
    public static void M2(params Span<int?> a) {
    }
    
    public static void M3(params ReadOnlySpan<int> a) {
    }
    
    public static void M3(params ReadOnlySpan<int?> a) {
    }
}

Da wir den Elementtyp über alles andere priorisieren, scheint dies angemessen zu sein; es gibt nichts, was der Sprache mitteilt, ob der Benutzer in diesem Szenario int? gegenüber int vorziehen würde.

Dynamische Bindung

Erweiterte Formate von Kandidaten, die andere als Array-Parameterauflistungen verwenden, werden vom aktuellen C#-Laufzeitbinder nicht als gültige Kandidaten anerkannt.

Wenn der primary_expression keinen Kompilierzeittyp hatdynamic, wird der Methodenaufruf einer begrenzten Kompilierzeitüberprüfung unterzogen, wie in §12.6.5 Kompilierzeitüberprüfung des dynamischen Elementaufrufs beschrieben.

Wenn nur ein einzelner Kandidat den Test besteht, ist der Aufruf des Kandidaten statisch gebunden, wenn alle folgenden Bedingungen erfüllt sind:

  • Der Kandidat ist eine lokale Funktion.
  • der Kandidat ist entweder nicht generisch oder seine Typargumente sind explizit angegeben;
  • Es gibt keine Mehrdeutigkeit zwischen den normalen und erweiterten Formaten des Kandidaten, die zur Kompilierzeit nicht gelöst werden kann.

Andernfalls ist der invocation_expression dynamisch gebunden.

Wenn nur ein einziger Kandidat den obigen Test bestanden hat:

  • wenn dieser Kandidat eine lokale Funktion ist, tritt ein Kompilierfehler auf;
  • Wenn dieser Kandidat nur im erweiterten Format mit anderen Parameterauflistungen als Arrays verwendbar ist, tritt ein Kompilierzeitfehler auf.

Wir sollten auch die Verletzung der Spezifikation für Rückgängigmachen/Beheben berücksichtigen, die sich auf lokale Funktionen auswirkt, siehe https://github.com/dotnet/roslyn/issues/71399.

LDM hat bestätigt, dass wir diese Spezifikationsverletzung beheben wollen.

Ausdrucksbaumstrukturen

Auflistungsausdrücke werden Ausdrucksstrukturen nicht unterstützt. Entsprechend werden erweiterte Formate von anderen Parameterauflistungen als Arrays in Ausdrucksstrukturen nicht unterstützt. Wir werden die Art und Weise, wie der Compiler Lambdas für Ausdrucksstrukturen bindet, nicht ändern, um zu vermeiden, dass APIs verwendet werden, die erweiterte Formate von anderen Parameterauflistungen als Arrays verwenden.

Reihenfolge der Auswertung mit anderen Auflistungen als Arrays in nicht trivialen Szenarien

Dieser Abschnitt wurde bei LDM überprüft und genehmigt. Trotz der Tatsache, dass Arrays von anderen Collections abweichen, muss die offizielle Sprachspezifikation keine unterschiedlichen Regeln für Arrays festlegen. Die Abweichungen könnten einfach als ein Artefakt der Implementierung behandelt werden. Gleichzeitig beabsichtigen wir nicht, das vorhandene Verhalten im Hinblick auf Arrays zu ändern.

Benannte Argumente

Eine Collection Instanz wird erstellt und gefüllt, nachdem das lexikalisch vorhergehende Argument ausgewertet wurde, aber bevor das lexikalisch folgende Argument ausgewertet wird.

Zum Beispiel:

class Program
{
    static void Main()
    {
        Test(b: GetB(), c: GetC(), a: GetA());
    }

    static void Test(int a, int b, params MyCollection c) {}

    static int GetA() => 0;
    static int GetB() => 0;
    static int GetC() => 0;
}

Die Reihenfolge der Auswertung ist die folgende:

  1. GetB wird aufgerufen
  2. MyCollection wird erstellt und aufgefüllt, GetC wird dabei aufgerufen
  3. GetA wird aufgerufen
  4. Test wird aufgerufen

Beachten Sie, dass im Fall von params array das Array unmittelbar vor dem Aufruf der Zielmethode erstellt wird, nachdem alle Argumente in ihrer lexikalischen Reihenfolge ausgewertet wurden.

Verbundzuweisung

Eine Collection Instanz wird erstellt und befüllt, nachdem der lexikalisch vorhergehende Index ausgewertet wurde, aber bevor der lexikalisch folgende Index ausgewertet wird. Die Instanz wird verwendet, um Getter und Setter des Zielindexers aufzurufen.

Zum Beispiel:

class Program
{
    static void Test(Program p)
    {
        p[GetA(), GetC()]++;
    }

    int this[int a, params MyCollection c] { get => 0; set {} }

    static int GetA() => 0;
    static int GetC() => 0;
}

Die Reihenfolge der Auswertung ist die folgende:

  1. GetA wird aufgerufen und im Cache gespeichert
  2. MyCollection wird erstellt, aufgefüllt und zwischengespeichert, GetC wird dabei aufgerufen
  3. Der Getter des Indexers wird mit zwischengespeicherten Werten für Indizes aufgerufen.
  4. Das Ergebnis wird inkrementiert.
  5. Der Setter des Indexers wird mit zwischengespeicherten Werten für Indizes und dem Ergebnis des Inkrements aufgerufen.

Ein Beispiel für eine leere Sammlung:

class Program
{
    static void Test(Program p)
    {
        p[GetA()]++;
    }

    int this[int a, params MyCollection c] { get => 0; set {} }

    static int GetA() => 0;
}

Die Reihenfolge der Auswertung ist die folgende:

  1. GetA wird aufgerufen und im Cache gespeichert
  2. Ein leeres MyCollection wird erstellt und zwischengespeichert
  3. Der Getter des Indexers wird mit zwischengespeicherten Werten für Indizes aufgerufen.
  4. Das Ergebnis wird inkrementiert.
  5. Der Setter des Indexers wird mit zwischengespeicherten Werten für Indizes und dem Ergebnis des Inkrements aufgerufen.

Objektinitialisierer

Eine Collection Instanz wird erstellt und befüllt, nachdem der lexikalisch vorhergehende Index ausgewertet wurde, aber bevor der lexikalisch folgende Index ausgewertet wird. Die Instanz wird verwendet, um den Getter des Indexers so häufig wie nötig aufzurufen, wenn zutreffend.

Zum Beispiel:

class C1
{
    public int F1;
    public int F2;
}

class Program
{
    static void Test()
    {
        _ = new Program() { [GetA(), GetC()] = { F1 = GetF1(), F2 = GetF2() } };
    }

    C1 this[int a, params MyCollection c] => new C1();

    static int GetA() => 0;
    static int GetC() => 0;
    static int GetF1() => 0;
    static int GetF2() => 0;
}

Die Reihenfolge der Auswertung ist die folgende:

  1. GetA wird aufgerufen und im Cache gespeichert
  2. MyCollection wird erstellt, aufgefüllt und zwischengespeichert, GetC wird dabei aufgerufen
  3. Der Getter des Indexers wird mit zwischengespeicherten Werten für Indizes aufgerufen.
  4. GetF1 wird ausgewertet und dem Feld F1-von C1 zugewiesen, wie im vorherigen Schritt zurückgegeben
  5. Der Getter des Indexers wird mit zwischengespeicherten Werten für Indizes aufgerufen.
  6. GetF2 wird ausgewertet und dem Feld F2-von C1 zugewiesen, wie im vorherigen Schritt zurückgegeben

Beachten Sie, dass im Fall eines Parameterarrays die Elemente ausgewertet und zwischengespeichert werden. Für jeden Aufruf des Indexer-Getters wird jedoch eine neue Instanz eines Arrays (mit denselben Werten) verwendet. Für das obige Beispiel ist die Reihenfolge der Auswertung die folgende:

  1. GetA wird aufgerufen und im Cache gespeichert
  2. GetC wird aufgerufen und im Cache gespeichert
  3. Der Getter des Indexers wird mit zwischengespeicherten GetA und ein neues Array wird mit zwischengespeicherten GetC ausgefüllt.
  4. GetF1 wird ausgewertet und dem Feld F1-von C1 zugewiesen, wie im vorherigen Schritt zurückgegeben
  5. Der Getter des Indexers wird mit zwischengespeicherten GetA und ein neues Array wird mit zwischengespeicherten GetC ausgefüllt.
  6. GetF2 wird ausgewertet und dem Feld F2-von C1 zugewiesen, wie im vorherigen Schritt zurückgegeben

Ein Beispiel für eine leere Sammlung:

class C1
{
    public int F1;
    public int F2;
}

class Program
{
    static void Test()
    {
        _ = new Program() { [GetA()] = { F1 = GetF1(), F2 = GetF2() } };
    }

    C1 this[int a, params MyCollection c] => new C1();

    static int GetA() => 0;
    static int GetF1() => 0;
    static int GetF2() => 0;
}

Die Reihenfolge der Auswertung ist die folgende:

  1. GetA wird aufgerufen und im Cache gespeichert
  2. Ein leeres MyCollection wird erstellt und zwischengespeichert
  3. Der Getter des Indexers wird mit zwischengespeicherten Werten für Indizes aufgerufen.
  4. GetF1 wird ausgewertet und dem Feld F1-von C1 zugewiesen, wie im vorherigen Schritt zurückgegeben
  5. Der Getter des Indexers wird mit zwischengespeicherten Werten für Indizes aufgerufen.
  6. GetF2 wird ausgewertet und dem Feld F2-von C1 zugewiesen, wie im vorherigen Schritt zurückgegeben

Referenzsicherheit

Der Abschnitt Auflistungsausdruck-Referenzsicherheit gilt für die Erstellung von Parameterauflistungen, wenn APIs im erweiterten Format aufgerufen werden.

params-Parameter sind implizit scoped, wenn ihr Typ eine ref-Struktur ist. UnscopedRefAttribute kann verwendet werden, um das zu überschreiben.

Metadaten

In den Metadaten könnten wir params-Parameter, die keine Arrays sind, mit System.ParamArrayAttribute markieren, so wie params-Arrays heute bereits markiert sind. Es sieht jedoch so aus, als wäre es viel sicherer, ein anderes Attribut für Nicht-Array params Parameter zu verwenden. Beispielsweise kann der aktuelle VB-Compiler sie weder im normalen noch im erweiterten Format mit ParamArrayAttribute verbrauchen. Daher wird eine Hinzufügung des Modifizierers „params“ VB-Consumer wahrscheinlich unterbrechen, sehr wahrscheinlich auch Consumer in anderen Sprachen oder Tools.

Daher werden params-Parameter mit einem neuen System.Runtime.CompilerServices.ParamCollectionAttribute markiert.

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Parameter, Inherited = true, AllowMultiple = false)]
    public sealed class ParamCollectionAttribute : Attribute
    {
        public ParamCollectionAttribute() { }
    }
}

Dieser Abschnitt wurde bei LDM überprüft und genehmigt.

Offene Fragen

Stapelzuteilungen

Dies ist ein Zitat aus https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/collection-expressions.md#unresolved-questions: „Stapelzuteilungen für sehr große Auflistungen könnten den Stapel unterbrechen. Sollte der Compiler eine Heuristik für das Platzieren dieser Daten im Heap haben? Sollte die Sprache nicht spezifiziert sein, um diese Flexibilität zuzulassen? Wir sollten der Spezifikation für params Span<T> folgen.“ Wir sollten die Fragen im Kontext dieses Vorschlags beantworten.

[Gelöst] Implizit scoped Parameter

Wenn params einen ref struct-Parameter modifiziert, sollte er einem Vorschlag zufolge als scoped deklariert betrachtet werden. Es wird argumentiert, dass die Anzahl der Fälle, in denen der Parameter abgegrenzt sein sollte, nahezu 100 % beträgt, wenn die BCL-Fälle betrachtet werden. In einigen wenigen Fällen, in denen dies erforderlich ist, könnte der Standardwert mit [UnscopedRef] überschrieben werden.

Es kann jedoch unerwünscht sein, den Standardwert einfach basierend auf dem Vorhandensein des params-Modifizierers zu ändern. Dies gilt insbesondere, da in Überschreibungs-/Implementierungsszenarien der params-Modifizierer nicht übereinstimmen muss.

Lösung:

Params-Parameter werden implizit abgegrenzt – https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-11-15.md#params-improvements.

[Gelöst] Die Durchsetzung von scoped oder params für Überschreibungen sollte in Betracht gezogen werden.

Wir haben bereits festgestellt, dass params Parameter standardmäßig scoped sein sollten. Dies führt jedoch aufgrund der vorhandenen Regeln für die Neuauslegung von params zu einem seltsamem Verhalten beim Überschreiben:

class Base
{
    internal virtual Span<int> M1(scoped Span<int> s1, params Span<int> s2) => throw null!;
}

class Derived : Base
{
    internal override Span<int> M1(Span<int> s1, // Error, missing `scoped` on override
                                   Span<int> s2  // Proposal: Error: parameter must include either `params` or `scoped`
                                  ) => throw null!;
}

Es gibt einen Unterschied im Überschreibungsverhalten zwischen params und scoped: params wird implizit geerbt, und damit scoped, während scoped selbst nicht implizit geerbt wird und auf jeder Ebene wiederholt werden muss.

Vorschlag: Wir sollten durchsetzen, dass Überschreibungen von params-Parametern explizit params oder scoped angeben müssen, wenn die ursprüngliche Definition ein scoped-Parameter ist. Mit anderen Worten, s2 in Derived muss params, scopedoder beides haben.

Lösung:

Wir müssen die explizite Angabe von scoped oder params beim Überschreiben eines params-Parameters erfordern, wenn anderer Parameter als ein params-Parameter hierfür erforderlich wäre – https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-02-21.md#params-and-scoped-across-overrides.

[Gelöst] Soll das Vorhandensein erforderlicher Elemente die Deklaration des params-Parameters verhindern?

Betrachten Sie das folgende Beispiel:

using System.Collections;
using System.Collections.Generic;

public class MyCollection1 : IEnumerable<long>
{
    IEnumerator<long> IEnumerable<long>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;
    public void Add(long l) => throw null;

    public required int F; // Collection has required member and constructor doesn't initialize it explicitly
}

class Program
{
    static void Main()
    {
        Test(2, 3); // error CS9035: Required member 'MyCollection1.F' must be set in the object initializer or attribute constructor.
    }

    // Proposal: An error is reported for the parameter indicating that the constructor that is required
    // to be available doesn't initialize required members. In other words, one is able
    // to declare such a parameter under the specified conditions.
    static void Test(params MyCollection1 a)
    {
    }
}

Lösung:

Wir werden die required-Elemente anhand des Konstruktors validieren, der verwendet wird, um die Berechtigung als params-Parameter an der Deklarationsstelle zu bestimmen – https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-02-21.md#required-members-and-params-parameters.

Alternativen

Es gibt einen alternativen Vorschlag, der params nur für ReadOnlySpan<T> gilt.

Außerdem könnte man sagen, da es jetzt Auflistungsausdrücken in der Sprache gibt, keine Notwendigkeit für die Unterstützung von params gibt. Für jeden Collection-Typ. Um eine API mit Auflistungstyp zu nutzen, müssen Entwickler einfach zwei Zeichen hinzufügen, [ vor der erweiterten Liste der Argumente und ] danach. Daher könnte die Unterstützung von params übertrieben sein, insbesondere, da andere Sprachen den Verbrauch von anderen params-Parametern als Arrays in absehbarer Zeit voraussichtlich nicht unterstützen werden.