Indeksatory rozszerzeń

Note

Ten artykuł jest specyfikacją funkcji. Specyfikacja służy jako dokument projektowy dla funkcji. Zawiera proponowane zmiany specyfikacji wraz z informacjami wymaganymi podczas projektowania i opracowywania funkcji. Te artykuły są publikowane do momentu sfinalizowania proponowanych zmian specyfikacji i włączenia ich do obecnej specyfikacji ECMA.

Mogą wystąpić pewne rozbieżności między specyfikacją funkcji a ukończoną implementacją. Te różnice są zawarte w odpowiednich notatkach ze spotkania dotyczącego projektowania języka (LDM).

Więcej informacji na temat procesu wdrażania specyfikacji funkcji można znaleźć w standardzie języka C# w artykule dotyczącym specyfikacji .

Kwestia dotycząca mistrza: https://github.com/dotnet/csharplang/issues/9856

Deklaracja

Gramatyka

Indeksatory rozszerzeń są dodawane do zestawu dozwolonych elementów członkowskich wewnątrz deklaracji rozszerzenia, rozszerzając gramatykę w następujący sposób (w stosunku do propozycji/csharp-14.0/extensions.md):

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

Podobnie jak zwykłe indeksatory, indeksatory rozszerzeń nie mają identyfikatora i są identyfikowane przez ich listę parametrów. Indeksatory rozszerzeń mogą używać pełnego zestawu funkcji, które zwykłe indeksatory obsługują dzisiaj (jednostki akcesoriów, składowe wyrażeń, metody dostępu zwracające ref, scoped parametry, atrybuty itp.).

Ponieważ indeksatory są zawsze składowymi instancji, blok rozszerzenia deklarujący indeksator musi zawierać nazwany parametr odbiorcy.

Istniejące ograniczenia dotyczące składowych rozszerzeń nadal mają zastosowanie: indeksatory wewnątrz deklaracji rozszerzenia nie mogą określać abstract, , virtual, overridesealednew, , partial, protected (ani żadnego z powiązanych modyfikatorów ułatwień dostępu) ani init metod dostępu.

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

Wszystkie reguły ze standardu języka C#, które mają zastosowanie do zwykłych indeksatorów, mają zastosowanie do indeksatorów rozszerzeń, ale składowe rozszerzeń nie mają niejawnych ani jawnych thiselementów .

Istniejąca reguła wnioskowania elementu rozszerzenia nadal ma zastosowanie: dla każdego elementu członkowskiego rozszerzenia innego niż metoda wszystkie parametry typu bloku rozszerzenia muszą być używane w połączonym zestawie parametrów z rozszerzenia i elementu członkowskiego.

IndexerName atrybut

IndexerNameAttribute można zastosować do indeksatora rozszerzeń. Atrybut nie jest emitowany w metadanych, ale jego wartość wpływa na konflikty między elementami członkowskimi, określa nazwę właściwości i metod dostępu w metadanych i jest używany podczas emitowania [DefaultMemberAttribute] (zobacz Metadane).

Zużycie

Dostęp indeksatora

Reguły dostępu indeksatora są aktualizowane: jeśli normalne przetwarzanie dostępu indeksatora nie znajdzie odpowiedniego indeksatora, zostanie podjęta próba przetworzenia konstrukcji jako dostępu indeksatora rozszerzeń.

  1. Spróbuj powiązać przy użyciu zadeklarowanych (lub dziedziczynych) indeksatorów wystąpień w typie odbiorcy. Jeśli zostanie znaleziony odpowiedni kandydat, rozwiązanie przeciążenia wybiera spośród tych elementów członkowskich wystąpienia tak jak dzisiaj i zatrzymuje się.
  2. Spróbuj powiązać przy użyciu niejawnych indeksatorów wystąpień zadeklarowanych (lub odziedziczonych) w typie odbiorcy. Jeśli zostanie znaleziony odpowiedni kandydat, rozwiązanie przeciążenia wybiera spośród tych elementów członkowskich wystąpienia tak jak dzisiaj i zatrzymuje się.
  3. Spróbuj powiązać jako dostęp indeksatora rozszerzeń. Jeśli ten proces znajdzie odpowiedniego kandydata, rozwiązanie przeciążenia wybiera spośród tych elementów członkowskich rozszerzenia zgodnie z poniższym opisem i zatrzymuje się.
  4. Spróbuj powiązać jako niejawny dostęp indeksatora rozszerzenia. Jeśli ten proces znajdzie odpowiedniego kandydata, rozwiązanie przeciążenia wybiera spośród tych elementów członkowskich rozszerzenia zgodnie z poniższym opisem i zatrzymuje się.

Uwaga: sekcja dostępu do elementu obsługuje przypadek, w którym argument ma typ dynamic, więc nigdy nie jest przetwarzany jako dostęp indeksatora.

Dostęp indeksatora rozszerzeń

Elementy członkowskie rozszerzenia, w tym indeksatory rozszerzeń, nigdy nie są brane pod uwagę, gdy odbiornik jest wyrażeniem base_access .

Uwaga: przetwarzamy tylko element_access jako indeksator, jeśli odbiornik jest zmienną lub wartością, więc indeksatory rozszerzeń nigdy nie są brane pod uwagę, gdy odbiornik jest typem.

Biorąc pod uwagę element_accessE[A], celem jest zidentyfikowanie indeksatora rozszerzeń.

Indeksator rozszerzenia kandydata ma zastosowanie w odniesieniu do odbiornika E i listy A argumentów, jeśli rozszerzony podpis składa się z parametrów typu bloku rozszerzenia i listy parametrów łączących parametr rozszerzenia z parametrami indeksatora, ma zastosowanie w odniesieniu do listy argumentów łączących odbiornik E z listą argumentów .A

Ponownie użyjemy przewodnika zakresu metody rozszerzenia: przechodzimy przez te same zakresy konsultowane w celu wywołania metody rozszerzenia, w tym bieżące i otaczające zakresy leksykalne oraz using przestrzeń nazw lub using static importy.

Biorąc pod uwagę każdy zakres z kolei:

  • Bloki rozszerzeń w niegenerycznych deklaracjach klas statycznych w bieżącym zakresie są brane pod uwagę.
  • Indeksatory w tych blokach rozszerzeń składają się z zestawu kandydatów.
  • Kandydaci, którzy nie są dostępni, są usuwani z zestawu.
  • Kandydaci, którzy nie mają zastosowania (zgodnie z definicją powyżej) są usuwane z zestawu.
  • Jeśli wynikowy zestaw indeksatorów kandydatów jest pusty, przejdziemy do następnego zakresu lub nie rozwiążemy problemu z dostępem indeksatora rozszerzeń, jeśli osiągniemy ostatni zakres (będziemy nadal próbować rozwiązać problem jako niejawny indeksator rozszerzenia w tym przypadku).
  • W przeciwnym razie rozwiązanie przeciążenia jest stosowane do zestawu kandydatów. Jeśli nie można zidentyfikować jednego najlepszego indeksatora, dostęp indeksatora rozszerzeń jest niejednoznaczny i wystąpi błąd czasu kompilacji.

Korzystając z tego jednego najlepszego indeksatora zidentyfikowanego w poprzednim kroku, dostęp indeksatora jest następnie przetwarzany jako wywołanie metody statycznej.

W zależności od kontekstu, w którym jest używany, dostęp indeksatora powoduje wywołanie get_accessor lub set_accessor indeksatora.
Jeśli dostęp indeksatora jest celem przypisania, wywoływana jest metoda implementacji statycznej set_accessor w celu przypisania nowej wartości.
We wszystkich innych przypadkach metoda implementacji statycznej get_accessor jest wywoływana w celu uzyskania bieżącej wartości.
Tak czy inaczej wywołanie będzie używać argumentów ogólnych wywnioskowanych podczas sprawdzania stosowania i odbiornika jako pierwszego argumentu.

Dostęp niejawnego indeksatora rozszerzenia

Dostęp niejawnego System.Index indeksatora (lub System.Range) rozszerzenia ma zastosowanie, jeśli:

  1. element_access ma jeden argument typu System.Index (lub System.Range) i
  2. właściwość odpowiedniego Length lub (wystąpienia lub Count rozszerzenia) znajduje się w typie odbiorcy i
  3. Odpowiedni this[int] indeksator (lub Slice(int, int)) (wystąpienie lub rozszerzenie) znajduje się w typie odbiorcy.

Inne formularze dostępu do elementów

Każda konstrukcja, która odchyla powiązanie dostępu do elementów (dostęp warunkowy o wartości null lub przypisania, przypisania indeksu w inicjatorach obiektów lub wzorce listy i rozkładu) automatycznie uczestniczy w rozwiązywaniu indeksatora rozszerzeń opisanym powyżej.

Drzewa wyrażeń

Indeksatory rozszerzeń nie mogą być przechwytywane w drzewach wyrażeń.

Dokumentacja XML

Składnia CREF umożliwia odwołowanie się do indeksatora rozszerzeń i jego metod dostępu, a także metod implementacji.

Example:

/// <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

Indeksatory rozszerzeń są zgodne z tym samym modelem obniżania co właściwości rozszerzenia. Dla każdego typu grupowania rozszerzeń na poziomie CLR, który zawiera co najmniej jeden indeksator, kompilator emituje:

  • Właściwość rozszerzenia o nazwie Item (lub wartość dostarczona przez IndexerNameAttributeprogram ) z ciałami dostępu, które throw NotImplementedException() i [ExtensionMarkerName] atrybut odwołujące się do odpowiedniego typu znacznika rozszerzenia.
  • Metody implementacji o nazwie get_Item/set_Item w otaczającej klasie statycznej. Te metody poprzedzają parametr odbiorcy do listy parametrów i zawierają jednostki zdefiniowane przez użytkownika. Są one static i uczestniczą w rozwiązywaniu przeciążeń w taki sam sposób, jak metody implementacji właściwości rozszerzenia.

Aby zdublować zachowanie zwykłych indeksatorów, kompilator emituje [DefaultMemberAttribute] również dowolny typ grupowania rozszerzeń zawierający co najmniej jeden indeksator rozszerzeń. MemberName Atrybut jest równy nazwie metadanych indeksatora (Item domyślnie lub wartości z IndexerNameAttribute).

Example

Kod źródłowy:

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

Emitowane metadane (uproszczone do składni podobnej do języka C#):

[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) => ...;
}

Otwarte problemy

Tymczasowa sekcja dokumentu związana z otwartymi problemami, w tym omówienie alternatywnych projektów

Radzenie sobie z params

Jeśli masz indeksator rozszerzeń z elementem params, takim jak int this[int i, params string[] s] { get; set; }, istnieją trzy sposoby jego użycia:

  • indeksowanie rozszerzeń: receiver[i: 0, "Alice", "Bob"]
  • wywołanie implementacji getter: E.get_Item(receiver, i: 0, "Alice", "Bob")
  • wywołanie implementacji setter: E.set_Item(...)

Ale jaki jest podpis metody implementacji ustawiającej?
Ma to sens tylko dla ostatniego parametru podpisu metody wywoływanej przez użytkownika, aby mieć paramswartość , więc nie służy do celów w pliku E.set_Item(... extension parameter ..., this i, params string[] s, int value).

Niektóre opcje:

  1. nie zezwalaj params na indeksatory rozszerzeń, które mają element ustawiający
  2. Pomijanie atrybutu [ParamArray] w metodzie implementacji ustawiającej
  3. nic specjalnego (emituj [ParamArray])

Proponuję opcję 2, ponieważ maksymalizuje params użyteczność. Koszt jest tylko niewielką różnicą między indeksowaniem rozszerzeń a składnią uściślania.

Decyzja (LDM 2026-02-02): emituj [ParamArray] i nie weryfikuje negatywnego wpływu na narzędzia

Wpływ przypisanej wartości na wnioskowanie typu

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 { } }
    }
}

Decyzja (LDM 2026-02-02): indeksator jest wnioskowany tylko przy użyciu odbiornika i argumentów na liście argumentów (tj. przypisana wartość nie przyczynia się).

Czy właściwości rozszerzenia Length/Count powinny być zliczalne dla typu?

Przypominamy, że rozszerzenia nie wchodzą w grę w przypadku powiązania niejawnych indeksów indeksów lub indeksatorów zakresu:

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!;
}

Tak więc do tej pory nasze stanowisko było takie, że właściwości rozszerzeń nie powinny być liczone jako "właściwości zliczalne" w wzorcach list, wyrażeniach kolekcji i niejawnych indeksatorach.

Jeśli uwidocznimy this[Index] lub this[Range] rozszerzymy indeksatory w scenariuszach dostępu do elementów, naturalne jest, że typ docelowy będzie działać we wzorcach listy.
Jednak wzorce listy wymagają Length właściwości lub Count .

Czy właściwości rozszerzenia powinny spełniać to wymaganie? (to wydaje się naturalne)

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 => ...;
  }
}

Ale czy te właściwości również powinny przyczynić się do niejawnego rezerwowego indeksatora (LengthSlice + Count/), który jest używany, gdy brakuje jawnego Index/Range indeksatora?

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 => ...;
  }
}

Decyzja (LDM 2026-02-02): rozszerzenia powinny współtworzyć wszędzie, w tym właściwości zliczalne i niejawny rezerwowy indeksator.

Upewnij się, czy dostęp indeksatora rozszerzeń występuje przed lub po niejawnych indeksatorach

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] => ...;
  }
}

Mam spec'ed i zaimplementowano dostęp indeksatora rozszerzeń jako o priorytekcie dla niejawnych indeksatorów, ale teraz myślę, że powinny one przyjść po, aby uniknąć niepotrzebnych przerw w zgodności.

Aktualizacja (LDM 2026-02-02): wymaga to dalszych badań. Tak, rozszerzenia powinny pochodzić po elementach członkowskich innych niż rozszerzenia, ale poza tym potrzebujemy pewnych konkretnych propozycji w świetle powyższej decyzji, aby umożliwić rozszerzeniom współtworzenie niejawnego rezerwowego indeksatora.

Decyzja (LDM 2026-03-09): kolejność to indeksatory wystąpień rzeczywistych, a następnie niejawne indeksatory wystąpień, a następnie indeksatory rozszerzeń rzeczywistych, a następnie niejawne indeksatory rozszerzeń. Zobacz https://github.com/dotnet/csharplang/blob/main/meetings/2026/LDM-2026-03-09.md#extension-indexers

Liczba/długość: czy nazwa jest priorytetowa, czy nienależące do rozszerzenia?

Mamy również istniejący rezerwowy: Length ma priorytet nad Count właściwością. Czy rozszerzenie Length powinno znajdować się przed lub po właściwości innej niż rozszerzenie Count ?

Decyzja (LDM 2026-03-09): przyjrzymy się zakresowi według zakresu (począwszy od zakresu wystąpienia i przechodzimy przez zakresy rozszerzeń) i w każdym zakresie, który Length najpierw wyszukamy, a następnie Count. Zobacz https://github.com/dotnet/csharplang/blob/main/meetings/2026/LDM-2026-03-09.md#extension-indexers

Potwierdzanie proponowanego projektu dla niejawnych indeksatorów

Szukamy zakresu według zakresu, zaczynając od zakresu wystąpienia, a następnie przechodząc do zakresów rozszerzeń.
W zakresie:

  1. szukamy rzeczywistego indeksatora,
  2. w przeciwnym razie, jeśli mamy jeden argument odpowiedniego typu, szukamy niejawnego indeksatora.

Decyzja (LDM 2026-03-09): nie, gdy przejdziemy do zakresów rozszerzeń, użyjemy rozwiązania rozszerzeń typu all-the-way-through.

Potwierdzanie proponowanego projektu dla wzorców list

W przypadku wzorca listy z rozrzutem wyszukamy:

  1. a Length/Count
  2. rzeczywiste lub niejawne this[Index]
  3. rzeczywiste lub niejawne this[Range]

Będziemy szukać każdego niezależnie. Jednak gdy szukamy niejawnego this[Index] lub this[Range], dwie części muszą pochodzić z tego samego zakresu:

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

Uwaga: nieujemna obsługa Length wzorców rozpoczyna się, gdy typ może być używany w wzorcu listy (tj. jest zliczalny i indeksowalny).

Decyzja (LDM 2026-03-09): nie, zamiast tego zastosujemy tę kolejność wyszukiwania:

  1. Wzorce list są rozpoznawane tak, jakbyśmy szukali indywidualnie indeksatora długości/liczby, indeksatora indeksatora i indeksatora zakresu
  2. W przypadku indeksatorów indeksów i zakresów wykonaj następujące czynności: a. W przypadku wyszukiwania wystąpień znajdź tylko indeks "rzeczywisty", jeśli to możliwe b. W przypadku wyszukiwania wystąpień znajdź tylko części niejawnego indeksatora, jeśli to możliwe, c. W przypadku pełnego wyszukiwania (wystąpienia+rozszerzenia) znajdź indeks "rzeczywisty", jeśli jest to możliwe, d. Za pomocą pełnego wyszukiwania (wystąpienie+rozszerzenie) znajdź części niejawnego indeksatora, jeśli to możliwe (każdy w poszczególnych odnośnikach)

Czy metoda rozszerzenia powinna również współtworzyć Slice ?

_ = c[1..^1];

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

Decyzja (LDM 2026-03-09): tak, traktujemy metody klasyczne i nowe rozszerzenia dokładnie takie same.

Czy rozszerzenie Length powinno przyczynić się do optymalizacji rozproszonej?

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

Decyzja (LDM 2026-03-09): nie, jest mało prawdopodobne, aby rozszerzenie mogłoby wdrożyć to w wydajny sposób, więc nie pomoże to w optymalizacji.

Czy niejawne indeksatory powinny być odtwarzane dla typów tablic lub ciągów?

Proponuję, aby niejawne indeksatory nie przyczyniły się do tych scenariuszy, ponieważ są one możliwe tylko z nieprawidłowo sformułowanym corlibem (brak wystąpienia Lengthlub GetSubArrayGetSubstring).

Decyzja (LDM/Mads przez e-mail 2026-04-07): brak indeksatorów rozszerzeń w ciągach ani tablicach.

Czy indeksatory rozszerzeń powinny być odtwarzane dla typów tablic lub ciągów?

Bieżące reguły specyfikacji i linii bazowej dla dostępu do tablicy i dostępu do ciągów oznaczają, że indeksatory rozszerzeń nie działają na tablicach ani ciągach.
Jednak deklaracja takich indeksatorów rozszerzeń jest dozwolona.

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;
    }
}

Uwaga: nie ma wątpliwości co do dostępu do elementu wskaźnika , ponieważ parametr rozszerzenia może nie być typem wskaźnika.

Decyzja (LDM/Mads przez e-mail 2026-04-07): brak indeksatorów rozszerzeń w ciągach ani tablicach.