Indexer für Erweiterungen

Note

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 relevanten Sprachentwurfsbesprechungen (LDM)-Notizen 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/9856

Erklärung

Grammatik

Erweiterungsindexer werden der Gruppe zulässiger Member innerhalb einer Erweiterungsdeklaration hinzugefügt, indem die Grammatik wie folgt erweitert wird (relativ zu Vorschlägen/csharp-14.0/extensions.md):

extension_member_declaration
        : method_declaration
        | property_declaration
        | indexer_declaration // new
        | operator_declaration
        ;

Wie normale Indexer verfügen Erweiterungsindexer über keinen Bezeichner und werden durch ihre Parameterliste identifiziert. Erweiterungsindexer können den vollständigen Satz von Features verwenden, die gewöhnliche Indexer heute unterstützen (Accessor-Körper, Ausdruckskörper, zurückzugebende Accessoren, scoped Parameter, Attribute usw.).

Da Indexer immer Instanzmember sind, muss ein Erweiterungsblock, der einen Indexer deklariert, einen benannten Empfängerparameter bereitstellen.

Die vorhandenen Einschränkungen für Erweiterungsmber gelten weiterhin: Indexer innerhalb einer Erweiterungsdeklaration können weder abstractangeben noch , , virtual, override, , new, sealed, partialprotected ( oder eines der zugehörigen Barrierefreiheitsmodifizierer) oder init Accessoren angeben.

public static class BitExtensions
{
    extension(int i)
    {
        public bool this[int index]
        {
            get => ...;
        }
    }
}

Alle Regeln aus dem C#-Standard, die für normale Indexer gelten, gelten für Erweiterungsindexer, aber Erweiterungsmember verfügen nicht über eine implizite oder explizite this.

Die vorhandene Erweiterungselement-Ferrierbarkeitsregel gilt weiterhin: Für jedes Nicht-Methodenerweiterungselement müssen alle Typparameter des Erweiterungsblocks in der kombinierten Gruppe von Parametern aus der Erweiterung und dem Element verwendet werden.

IndexerName-Attribut

IndexerNameAttribute kann auf einen Erweiterungsindexer angewendet werden. Das Attribut wird nicht in Metadaten ausgegeben, aber sein Wert wirkt sich auf Konflikte zwischen Membern aus, bestimmt den Namen der Eigenschaft und accessoren in Metadaten und wird beim Emittieren [DefaultMemberAttribute] verwendet (siehe Metadaten).

Verbrauch

Indexerzugriff

Die Regeln im Indexerzugriff werden aktualisiert: Wenn die normale Verarbeitung des Indexerzugriffs keinen anwendbaren Indexer findet, wird versucht, das Konstrukt als Erweiterungsindexerzugriff zu verarbeiten.

  1. Versuchen Sie, eine Bindung mit den Instanzindexern herzustellen, die für den Empfängertyp deklariert (oder geerbt) wurden. Wenn ein zutreffender Kandidat gefunden wird, wählt die Überladungsauflösung unter diesen Instanzmitgliedern als heute aus und stoppt.
  2. Versuchen Sie, die Bindung mithilfe der impliziten Instanzindexer zu binden, die für den Empfängertyp deklariert (oder geerbt) wurden. Wenn ein zutreffender Kandidat gefunden wird, wählt die Überladungsauflösung unter diesen Instanzmitgliedern als heute aus und stoppt.
  3. Versuchen Sie, eine Bindung als Erweiterungsindexerzugriff zu erstellen. Wenn dieser Prozess einen geeigneten Kandidaten findet, wählt die Überladungsauflösung unter diesen Erweiterungsmitgliedern aus, wie unten beschrieben und beendet.
  4. Versuchen Sie, als impliziter Erweiterungsindexerzugriff zu binden. Wenn dieser Prozess einen geeigneten Kandidaten findet, wählt die Überladungsauflösung unter diesen Erweiterungsmitgliedern aus, wie unten beschrieben und beendet.

Hinweis: Der Abschnitt für den Elementzugriff behandelt den Fall, in dem ein Argument Typ dynamichat, sodass es nie als Indexerzugriff verarbeitet wird.

Erweiterungsindexerzugriff

Erweiterungsmember, einschließlich Erweiterungsindexer, werden niemals berücksichtigt, wenn der Empfänger ein base_access Ausdruck ist.

Hinweis: Wir verarbeiten eine element_access nur als Indexerzugriff, wenn der Empfänger eine Variable oder ein Wert ist, sodass Erweiterungsindexer niemals berücksichtigt werden, wenn der Empfänger ein Typ ist.

Angesichts eines element_accessE[A] besteht das Ziel darin, einen Erweiterungsindexer zu identifizieren.

Ein Kandidatenerweiterungsindexer gilt in Bezug auf Empfänger E - und Argumentliste A , wenn eine erweiterte Signatur, bestehend aus den Typparametern des Erweiterungsblocks und einer Parameterliste, die den Erweiterungsparameter mit den Parametern des Indexers kombiniert, in Bezug auf eine Argumentliste anwendbar ist, die den Empfänger E mit der Argumentliste Akombiniert.

Wir verwenden die Erweiterungsmethode scope-walk: Wir durchlaufen dieselben Bereiche, die für den Aufruf der Erweiterungsmethode konsultiert werden, einschließlich der aktuellen und einschließenden lexikalischen Bereiche und Namespaces oder usingusing static Importe.

Berücksichtigen Sie die einzelnen Bereiche wiederum:

  • Erweiterungsblöcke in nicht generischen statischen Klassendeklarationen im aktuellen Bereich werden berücksichtigt.
  • Die Indexer in diesen Erweiterungsblöcken umfassen den Kandidatensatz.
  • Kandidaten, auf die nicht zugegriffen werden kann, werden aus der Gruppe entfernt.
  • Kandidaten, die (wie oben definiert) nicht anwendbar sind, werden aus der Gruppe entfernt.
  • Wenn der resultierende Satz von Kandidatenindexern leer ist, fahren wir mit dem nächsten Bereich fort oder schlagen fehl, einen Erweiterungsindexerzugriff aufzulösen, wenn der letzte Bereich erreicht ist (wir versuchen weiterhin, in diesem Fall als impliziter Erweiterungsindexer aufzulösen).
  • Andernfalls wird die Überladungsauflösung auf den Kandidatensatz angewendet. Wenn ein einzelner optimaler Indexer nicht identifiziert werden kann, ist der Erweiterungsindexerzugriff mehrdeutig, und ein Kompilierungszeitfehler tritt auf.

Mit diesem einzigen am besten identifizierten Indexer im vorherigen Schritt wird der Indexerzugriff dann als statischer Methodenaufruf verarbeitet.

Je nach Kontext, in dem sie verwendet wird, führt ein Indexerzugriff zu einem Aufruf der get_accessor oder der set_accessor des Indexers.
Wenn der Indexerzugriff das Ziel einer Zuordnung ist, wird die set_accessor statische Implementierungsmethode aufgerufen, um einen neuen Wert zuzuweisen.
In allen anderen Fällen wird die get_accessor statische Implementierungsmethode aufgerufen, um den aktuellen Wert abzurufen.
Auf beide Weise verwendet der Aufruf generische Argumente, die während der Anwendbarkeitsprüfung und des Empfängers als erstes Argument abgeleitet werden.

Impliziter Erweiterungsindexerzugriff

Ein impliziter System.Index (oder System.Range) Erweiterungsindexerzugriff ist anwendbar, wenn:

  1. die element_access ein einzelnes Argument aufweist, das vom Typ System.Index (oder System.Range) ist und
  2. eine anwendbare Length oder (Instanz- Count oder Erweiterungseigenschaft) befindet sich im Empfängertyp und
  3. Ein anwendbarer this[int] Indexer (oder Slice(int, int)) (Instanz oder Erweiterung) befindet sich im Empfängertyp.

Andere Elementzugriffsformulare

Jedes Konstrukt, das auf die Bindung für den Elementzugriff zurücksetzt (null-bedingter Elementzugriff oder Zuordnungen, Indexzuweisungen in Objektinitialisierern oder Listen- und Spreadmustern), nimmt automatisch an der oben beschriebenen Auflösung der Erweiterungsindexierung teil.

Ausdrucksbäume

Erweiterungsindexer können nicht in Ausdrucksstrukturen erfasst werden.

XML-Dokumente

CREF-Syntax ermöglicht den Verweis auf einen Erweiterungsindexer und seine Accessoren sowie seine Implementierungsmethoden.

Beispiel:

/// <see cref="E.extension(int).this[string]"/>
/// <see cref="E.extension(int).get_Item(string)"/>
/// <see cref="E.extension(int).get_Item"/>
/// <see cref="E.extension(int).set_Item(string, int)"/>
/// <see cref="E.extension(int).set_Item"/>
/// <see cref="E.get_Item(int, string)"/>
/// <see cref="E.get_Item"/>
/// <see cref="E.set_Item(int, string, int)"/>
/// <see cref="E.set_Item"/>
public static class E
{
    extension(int i)
    {
        /// <summary></summary>
        public int this[string s]
        {
            get => throw null;
            set => throw null;
        }
    }
}

Metadata

Erweiterungsindexer folgen dem gleichen niedrigeren Modell wie Erweiterungseigenschaften. Für jeden Erweiterungs-Erweiterungstyp auf CLR-Ebene, der mindestens einen Indexer enthält, gibt der Compiler Folgendes aus:

  • Eine Erweiterungseigenschaft namens Item (oder der von ) bereitgestellte IndexerNameAttributeWert mit Accessor-Textkörpern, die und ein [ExtensionMarkerName] Attribut, das throw NotImplementedException() auf den entsprechenden Erweiterungsmarkertyp verweist.
  • Implementierungsmethoden, die in der eingeschlossenen statischen Klasse benannt sind get_Item/set_Item . Diese Methoden stellen den Empfängerparameter der Parameterliste voran und enthalten die benutzerdefinierten Textkörper. Sie sind static und nehmen auf die gleiche Weise an der Überladungsauflösung teil wie Implementierungsmethoden für Erweiterungseigenschaften.

Um das Verhalten gewöhnlicher Indexer zu spiegeln, gibt der Compiler auch einen Erweiterungsgruppierungstyp [DefaultMemberAttribute] aus, der einen oder mehrere Erweiterungsindexer enthält. Das Attribut MemberName entspricht dem Metadatennamen des Indexers (Item standardmäßig oder dem Wert von IndexerNameAttribute).

Example

Quellcode:

static class BitExtensions
{
    extension<T>(T t)
    {
        public bool this[int index]
        {
            get => ...;
            set => ...;
        }
    }
}

Ausgelassene Metadaten (vereinfacht in C#-ähnliche Syntax):

[Extension]
static class BitExtensions
{
    [Extension, SpecialName, DefaultMember("Item")]
    public sealed class <G>$T0 // grouping type
    {
        [SpecialName]
        public static class <M>$T_t // marker type
        {
            [SpecialName]
            public static void <Extension>$(T t) { } // marker method
        }

        [ExtensionMarkerName("<M>$T_t")]
        public bool this[int index] // extension indexer
        {
            get => throw new NotImplementedException();
            set => throw new NotImplementedException();
        }
    }

    // accessor implementation methods
    public static bool get_Item<T>(T t, int index) => ...;
    public static void set_Item<T>(T t, int index, bool value) => ...;
}

Offene Probleme

Temporärer Abschnitt des Dokuments im Zusammenhang mit offenen Problemen, einschließlich Diskussion über alternative Designs

Umgang mit params

Wenn Sie über einen Erweiterungsindexer verfügen, paramsz int this[int i, params string[] s] { get; set; }. B. haben Sie drei Möglichkeiten, sie zu verwenden:

  • Erweiterungsindizierung: receiver[i: 0, "Alice", "Bob"]
  • Getter-Implementierungsaufruf: E.get_Item(receiver, i: 0, "Alice", "Bob")
  • Setterimplementierungsaufruf: E.set_Item(...)

Aber was ist die Signatur der Setterimplementierungsmethode?
Es ist nur sinnvoll, den letzten Parameter einer benutzerdefinierten Methodensignatur zu haben params, sodass er keinen Zweck in E.set_Item(... extension parameter ..., this i, params string[] s, int value).

Einige Optionen:

  1. params Unzulässig für Erweiterungsindexer mit einem Setter
  2. Auslassen des Attributs [ParamArray] für die Setterimplementierungsmethode
  3. nichts Besonderes tun (emit the [ParamArray])

Ich würde Option 2 vorschlagen params , da sie die Nützlichkeit maximiert. Die Kosten sind nur ein kleiner Unterschied zwischen Erweiterungsindizierung und Mehrdeutigkeitssyntax.

Entscheidung (LDM 2026-02-02): Emittieren sie und [ParamArray] überprüfen Sie keine negativen Auswirkungen auf die Werkzeugerstellung.

Auswirkungen des zugewiesenen Werts auf Typeinschluss

int i = 0;
i[42, null] = new object(); // fails inference
E.set_Item(i, 42, null, new object()); // infer `E.set_Item<object>`

public static class E
{
    extension<T>(int i)
    {
        public T this[int j, T t] { set { } }
    }
}
#nullable enable

int i = 0;
i[new object()] = null; // infer `E.extension<object!>` and warn on conversion of null literal to `object!`
E.set_Item(i, new object(), null); // infer `E.set_Item<object?>`

public static class E
{
    extension<T>(int i)
    {
        public T this[T t] { set { } }
    }
}

Entscheidung (LDM 2026-02-02):Der Indexer wird nur aufgrund des Empfängers und der Argumente in der Argumentliste abgeleitet (dh der zugewiesene Wert trägt nicht bei).

Sollen Erweiterungseigenschaften Length/Count einen Typ zählbar machen?

Als Erinnerung treten Erweiterungen nicht ins Spiel, wenn impliziter Index oder Bereichsindexer gebunden werden:

C c = new C();
_ = c[..]; // Cannot apply indexing with [] to an expression of type 'C'

class C
{
    public int Length => 0;
}

static class E
{
    public static C Slice(this C c, int i, int j) => null!;
}

So war unsere Position bisher, dass Erweiterungseigenschaften nicht als "zählbare Eigenschaften" in Listenmustern, Sammlungsausdrücken und impliziten Indexern zählen sollten.

Wenn wir Indexer in Elementzugriffsszenarien verfügbar machen this[Index] oder this[Range] erweitern, ist es natürlich, dass der Zieltyp in Listenmustern funktioniert.
Listenmuster erfordern jedoch eine Length oder Count eine Eigenschaft.

Sollten Erweiterungseigenschaften diese Anforderung erfüllen? (das scheint natürlich zu sein)

C c = new C();
var x1 = c[^1];
var x2 = c[1..];

if (c is [.., var y1]) { }
if (c is [_, .. var y2]) { }

class C { }

static class E
{
  extension(C c)
  {
    object this[System.Index] => ...;
    C this[System.Range] => ...;
    int Length => ...;
  }
}

Aber sollten diese Eigenschaften auch zum impliziten Indexer-Fallback (LengthSlice + /Count) beitragen, das verwendet wird, wenn ein expliziter Index/Range Indexer fehlt?

C c = new C();
if (c is [var y1, .. var y2]) { }

class C
{
  C Slice(int i, int j) => ...;
}

static class E
{
  extension(C c)
  {
    object this[System.Index] => ...;
    int Length => ...;
  }
}

Entscheidung (LDM 2026-02-02-02): Erweiterungen sollten überall mitwirken, einschließlich zählbarer Eigenschaften und impliziter Indexerfallback.

Überprüfen, ob der Erweiterungsindexerzugriff vor oder nach impliziten Indexern kommt

C c = new C();
_ = c[^1];

class C
{
  public int Length => ...;
  public int this[int i] => ...;
}

static class E
{
  extension(C c)
  {
    public int this[System.Index i] => ...;
  }
}

Ich habe den Erweiterungsindexerzugriff als Priorität gegenüber impliziten Indexern festgelegt und implementiert, aber denken Sie jetzt, dass sie kommen sollten, um unnötige Komppat-Unterbrechungen zu vermeiden.

Update (LDM 2026-02-02): Dies erfordert weitere Untersuchungen. Ja, Erweiterungen sollten nach Nichterweiterungsmitgliedern kommen, aber darüber hinaus brauchen wir einige konkrete Vorschläge im Lichte der obigen Entscheidung, um Erweiterungen zur impliziten Indexerfallback beizutragen.

Entscheidung (LDM 2026-03-09): Die Reihenfolge ist echte Instanzindexer, dann implizite Instanzindexer, dann echte Erweiterungsindexer und dann implizite Erweiterungsindexer. Siehe https://github.com/dotnet/csharplang/blob/main/meetings/2026/LDM-2026-03-09.md#extension-indexers.

Anzahl/Länge: Ist der Name zuerst priorisiert oder nicht erweiterungsfrei oder erweiterungsfrei?

Wir haben auch einen vorhandenen Fallback: Length wird über Count die Eigenschaft priorisiert. Sollte eine Erweiterung Length vor oder nach einer Nichterweiterungseigenschaft Count erfolgen?

Entscheidung (LDM 2026-03-09): Wir sehen bereichsspezifischen Bereich (beginnend vom Instanzbereich und fortfahren durch Erweiterungsbereiche), und innerhalb jedes Bereichs werden wir zuerst und dann Countdanach suchenLength. Siehe https://github.com/dotnet/csharplang/blob/main/meetings/2026/LDM-2026-03-09.md#extension-indexers.

Vorgeschlagenes Design für implizite Indizierungselemente bestätigen

Wir betrachten bereichsspezifische Bereiche, beginnend mit dem Instanzbereich und gehen dann zu Erweiterungsbereichen.
Innerhalb eines Bereichs:

  1. wir suchen nach einem echten Indexer,
  2. andernfalls, wenn ein einzelnes Argument des richtigen Typs vorhanden ist, suchen wir nach einem impliziten Indexer.

Entscheidung (LDM 2026-03-09): Nein, sobald wir in Erweiterungsbereiche umsteigen, werden wir die All-the-way-through-Erweiterungsauflösung verwenden.

Vorgeschlagenes Design für Listenmuster bestätigen

Für ein Listenmuster mit einer Spread suchen wir nach:

  1. a Length/Count
  2. eine reale oder implizite this[Index]
  3. eine reale oder implizite this[Range]

Wir werden nach jedem unabhängigen Suchen suchen. Aber wenn wir nach einer impliziten this[Index] oder this[Range]suchen, müssen die beiden Teile aus demselben Bereich stammen:

  1. Length/Count
  2. this[int]/Slice(int, int)

Hinweis: Die nicht negative Behandlung für Length Muster tritt ein, wenn ein Typ in einem Listenmuster verwendet werden kann (dh es kann gezählt und indiziert werden).

Entscheidung (LDM 2026-03-09): Nein, wir folgen stattdessen dieser Nachschlagereihenfolge:

  1. Listenmuster werden aufgelöst, als ob wir einzeln nach Länge/Anzahl, Indexindexer und Bereichsindexer suchen
  2. Gehen Sie für Index- und Range-Indexer wie folgt vor: a. Suchen Sie bei der Instanzsuche nur den "echten" Index, falls möglich b. Suchen Sie bei der Instanzsuche nur die Teile des impliziten Indexers, falls möglich c. Suchen Sie bei vollständiger Suche (Instanz+Erweiterung) den "echten" Index, falls möglich d. Suchen Sie bei vollständiger Suche (Instanz+Erweiterung) die Teile des impliziten Indexers nach Möglichkeit (jeweils in einzelnen Nachschlagevorgängen).

Soll auch die Erweiterungsmethode Slice beitragen?

_ = c[1..^1];

static class E
{
  extension(C c)
  {
    public int Length => 3;
  }
  public static C Slice(this C c, int i, int j) => ...;
}

Entscheidung (LDM 2026-03-09): Ja, wir behandeln klassische und neue Erweiterungsmethoden genau so.

Soll die Erweiterung Length zur Spread-Optimierung beitragen?

C c = new C();
int[] i = [0, .. c]; // Uses Length, if available, to allocate the right size

Entscheidung (LDM 2026-03-09): Nein, es ist unwahrscheinlich, dass eine Erweiterung dies in einer leistungsfähigen Weise implementieren kann, sodass sie nicht zur Optimierung beitragen würde.

Sollten implizite Indexer für Array- oder Zeichenfolgentypen verwendet werden?

Ich schlage vor, dass implizite Indexer nicht zu diesen Szenarien beitragen, da sie nur mit einer falsch formatierten Corlib (fehlende Instanz LengthGetSubArray oder GetSubstring) möglich sind.

Entscheidung (LDM/Mads per E-Mail 2026-04-07): Keine Erweiterungsindexer für Zeichenfolgen oder Arrays.

Sollten Erweiterungsindexer für Array- oder Zeichenfolgentypen verwendet werden?

Die aktuellen Spezifikations- und Basisplanregeln für den Arrayzugriff und den Zeichenfolgenzugriff bedeuten, dass Erweiterungsindexer nicht für Arrays oder Zeichenfolgen funktionieren.
Die Deklaration solcher Erweiterungsindexer ist jedoch zulässig.

int[] i = ...;
_ = i[new C()];

public static class E
{
    extension(int[] i)
    {
        public int this[C c] => throw null;
    }
}
string s = "...;
_ = s[new C()];

public static class E
{
    extension(int[] i)
    {
        public int this[C c] => throw null;
    }
}

Hinweis: Es gibt keine Frage für den Zeigerelementzugriff , da ein Erweiterungsparameter möglicherweise kein Zeigertyp ist.

Entscheidung (LDM/Mads per E-Mail 2026-04-07): Keine Erweiterungsindexer für Zeichenfolgen oder Arrays.