Indexadores de extensão

Note

Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ele inclui mudanças de especificação propostas, juntamente com as informações necessárias durante o design e desenvolvimento do recurso. Estes artigos são publicados até que as alterações de especificações propostas sejam finalizadas e incorporadas na especificação ECMA atual.

Pode haver algumas discrepâncias entre a especificação do recurso e a implementação concluída. Essas diferenças são capturadas nas notas pertinentes da reunião de design de linguagem (LDM).

Você pode saber mais sobre o processo de adoção de especificações de recursos no padrão de linguagem C# no artigo sobre as especificações .

Edição campeã: https://github.com/dotnet/csharplang/issues/9856

Declaração

Gramática

Os indexadores de extensão são adicionados ao conjunto de membros permitidos dentro de uma declaração de extensão estendendo a gramática da seguinte forma (relativamente a proposals/csharp-14.0/extensions.md):

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

Tal como os indexadores comuns, os indexadores de extensão não têm identificador e são identificados pela sua lista de parâmetros. Os indexadores de extensão podem usar o conjunto completo de funcionalidades que os indexadores comuns suportam atualmente (corpos de acessórios, membros com corpo de expressão, acessores que retornam ref, scoped parâmetros, atributos, etc.).

Como os indexadores são sempre membros de instância, um bloco de extensão que declara um indexador deve fornecer um parâmetro de recetor nomeado.

As restrições existentes sobre membros de extensão continuam a aplicar-se: os indexadores dentro de uma declaração de extensão não podem especificar abstract, virtual, override, newsealed, partial, , ( protected ou qualquer um dos modificadores de acessibilidade relacionados), nem init acessórios.

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

Todas as regras do padrão C# que se aplicam aos indexadores comuns aplicam-se aos indexadores de extensão, mas os membros da extensão não têm um .this

A regra de inferibilidade existente dos membros da extensão ainda se aplica: Para cada membro de extensão que não seja o método, todos os parâmetros de tipo do seu bloco de extensão devem ser usados no conjunto combinado de parâmetros da extensão e do membro.

Atributo IndexerName

IndexerNameAttribute pode ser aplicado a um indexador de extensão. O atributo não é emitido nos metadados, mas o seu valor afeta conflitos entre membros, determina o nome da propriedade e dos acessórios nos metadados, e é usado na emissão [DefaultMemberAttribute] (ver Metadados).

Consumo

Acesso ao indexador

As regras no acesso ao indexador são atualizadas: se o processamento normal do acesso ao indexador não encontrar um indexador aplicável, faz-se uma tentativa de processar a construção como um acesso ao indexador de extensão.

  1. Tente ligar usando os indexadores de instância declarados (ou herdados) no tipo de recetor. Se for encontrado um candidato aplicável, a resolução de sobrecarga seleciona entre esses membros da instância como hoje e para.
  2. Tente ligar usando os indexadores implícitos de instâncias declarados (ou herdados) no tipo de recetor. Se for encontrado um candidato aplicável, a resolução de sobrecarga seleciona entre esses membros da instância como hoje e para.
  3. Tente ligar como um acesso indexador de extensão. Se este processo encontrar um candidato aplicável, a resolução de sobrecarga seleciona entre esses membros de extensão conforme descrito abaixo e para.
  4. Tente ligar como extensão o acesso implícito ao indexador. Se este processo encontrar um candidato aplicável, a resolução de sobrecarga seleciona entre esses membros de extensão conforme descrito abaixo e para.

Nota: a secção de acesso a elementos trata do caso em que um argumento tem tipo dynamic, pelo que nunca é processado como acesso indexador.

Acesso ao indexador de extensões

Membros de extensão, incluindo indexadores de extensão, nunca são considerados quando o recetor é uma expressão base_access .

Nota: só processamos um element_access como acesso ao indexador se o recetor for uma variável ou valor, pelo que os indexadores de extensão nunca são considerados quando o recetor é um tipo.

Dado um element_accessE[A], o objetivo é identificar um indexador de extensão.

Um indexador de extensão candidato é aplicável relativamente ao recetor E e à lista A de argumentos se uma assinatura expandida, composta pelos parâmetros de tipo do bloco de extensão e uma lista de parâmetros que combina o parâmetro de extensão com os parâmetros do indexador, for aplicável relativamente a uma lista de argumentos que combina o recetor E com a lista Ade argumentos .

Reutilizamos o método de extensão scope walk: percorremos os mesmos escopos consultados para invocação do método de extensão, incluindo os escopos lexicais atuais e anexos e using namespace ou using static importações.

Considerando cada oscilação por sua vez:

  • Blocos de extensão em declarações de classe estática não genéricas no âmbito atual são considerados.
  • Os indexadores nesses blocos de extensão constituem o conjunto de candidatos.
  • Candidatos que não são acessíveis são removidos do conjunto.
  • Os candidatos que não são aplicáveis (conforme definido acima) são removidos do conjunto.
  • Se o conjunto resultante de indexadores candidatos estiver vazio, então avançamos para o próximo âmbito, ou falhamos em resolver um acesso ao indexador de extensão se chegámos ao último escopo (nesse caso, continuaremos a tentar resolver como um indexador implícito de extensão).
  • Caso contrário, aplica-se resolução de sobrecarga ao conjunto candidato. Se não for possível identificar um único melhor indexador, o acesso ao indexador de extensão é ambíguo e ocorre um erro em tempo de compilação.

Usando este único melhor indexador identificado na etapa anterior, o acesso ao indexador é então processado como uma invocação estática de método.

Dependendo do contexto em que é utilizado, um acesso ao indexador provoca a invocação do get_accessor ou do set_accessor do indexador.
Se o acesso ao indexador for o alvo de uma atribuição, o método de implementação estática set_accessor é invocado para atribuir um novo valor.
Em todos os outros casos, o método de implementação estática get_accessor é invocado para obter o valor atual.
De qualquer forma, a invocação usará argumentos genéricos inferidos durante a verificação de aplicabilidade e o recetor como primeiro argumento.

Acesso implícito ao indexador de extensão

Um acesso implícito System.Index (ou System.Range) ao indexador de extensão é aplicável se:

  1. A element_access tem um único argumento que é do tipo System.Index (ou System.Range), e
  2. uma propriedade aplicável Length ou Count (instância ou extensão) encontra-se no tipo de recetor, e
  3. Um indexador aplicável this[int] (ou Slice(int, int)) (instância ou extensão) é encontrado no tipo de recetor.

Outras formas de acesso a elementos

Qualquer construto que defere para a ligação de acesso a elementos (acesso ou atribuições de elementos nulos, atribuições de índice em inicializadores de objetos, ou padrões de lista e dispersão) participa automaticamente na resolução do indexador de extensão descrita acima.

Árvores de expressão

Indexadores de extensão não podem ser capturados em árvores de expressão.

Documentos XML

A sintaxe CREF permite referir-se a um indexador de extensão e aos seus acessores, bem como aos seus métodos de implementação.

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

Os indexadores de extensão seguem o mesmo modelo de redução que as propriedades de extensão. Para cada tipo de agrupamento de extensão ao nível de CLR que contenha pelo menos um indexador, o compilador emite:

  • Uma propriedade de extensão nomeada Item (ou o valor fornecido por IndexerNameAttribute) com corpos de acesso que throw NotImplementedException() e um [ExtensionMarkerName] atributo que referencia o tipo de marcador de extensão apropriado.
  • Métodos de implementação nomeados get_Item/set_Item na classe estática que o envolve. Estes métodos antepõem o parâmetro receptor à lista de parâmetros e contêm os corpos definidos pelo utilizador. São static e participam na resolução de sobrecarga da mesma forma que os métodos de implementação para propriedades de extensão.

Para espelhar o comportamento dos indexadores comuns, o compilador também emite [DefaultMemberAttribute] em qualquer tipo de agrupamento de extensão que contenha um ou mais indexadores de extensão. O atributo MemberName é igual ao nome dos metadados do indexador (Item por defeito, ou o valor de IndexerNameAttribute).

Example

Código-fonte:

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

Metadados emitidos (simplificados para sintaxe semelhante à de 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) => ...;
}

Questões em aberto

Secção temporária do documento relacionada com questões em aberto, incluindo discussão de designs alternativos

Lidar com params

Se tiver um indexador de extensão com params, como int this[int i, params string[] s] { get; set; }, há três formas de o usar:

  • Indexação por extensão: receiver[i: 0, "Alice", "Bob"]
  • Invocação de implementação getter: E.get_Item(receiver, i: 0, "Alice", "Bob")
  • Invocação de implementação do setter: E.set_Item(...)

Mas qual é a assinatura do método de implementação do setter?
Só faz sentido que o último parâmetro de uma assinatura de método invocável pelo utilizador tenha params, por isso não serve qualquer propósito em E.set_Item(... extension parameter ..., this i, params string[] s, int value).

Algumas opções:

  1. não permitir params para indexadores de extensão que tenham um setter
  2. Omitir o [ParamArray] atributo no método de implementação Setter
  3. Não fazer nada de especial (emitir o [ParamArray])

Eu proporia a opção 2, pois maximiza params a utilidade. O custo é apenas uma pequena diferença entre a indexação de extensões e a sintaxe de desambiguação.

Decisão (LDM 2026-02-02): emitir e [ParamArray] verificar que não há impacto negativo nas ferramentas

Impacto do valor atribuído na inferência de tipo

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

Decisão (LDM 2026-02-02): o indexador é inferido apenas dado o recetor e os argumentos na lista de argumentos (ou seja, o valor atribuído não contribui).

As propriedades de extensão Length/Count devem tornar um tipo contável?

Como lembrete, as extensões não entram em jogo ao ligar indexadores implícitos de Índice ou Intervalo:

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

Assim, a nossa posição até agora tem sido que as propriedades das extensões não devem contar como "propriedades contáveis" em padrões de lista, expressões de coleção e indexadores implícitos.

Se expormos this[Index] ou this[Range] extensionarmos indexadores em cenários de acesso a elementos, é natural esperar que o tipo alvo funcione em padrões de lista.
Os padrões de lista, no entanto, requerem uma Length propriedade ou.Count

As propriedades de extensão devem cumprir esse requisito? (isso pareceria natural)

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

Mas então, essas propriedades deveriam também contribuir para o fallback implícito do indexador (Length/ + SliceCount) que é usado quando falta um indexador explícito?Index/Range

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

Decisão (LDM 2026-02-02): as extensões devem contribuir em todo o lado, incluindo propriedades enumeráveis e o recuo implícito do indexador.

Confirme se o acesso ao indexador de extensão ocorre antes ou depois dos indexadores implícitos

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

Especifiquei e implementei o acesso ao indexador de extensão como tendo prioridade sobre indexadores implícitos, mas agora penso que deviam vir depois para evitar quebras desnecessárias de compat.

Atualização (LDM 2026-02-02): isto necessita de investigação adicional. Sim, as extensões devem vir depois dos membros que não são extensões, mas para além disso precisamos de algumas propostas concretas à luz da decisão acima para permitir que as extensões contribuam para o fallback implícito do indexador.

Decisão (LDM 2026-03-09): a ordem é indexadores de instâncias reais, depois indexadores de instâncias implícitas, depois indexadores de extensões reais, e depois indexadores de extensões implícitas. Veja https://github.com/dotnet/csharplang/blob/main/meetings/2026/LDM-2026-03-09.md#extension-indexers

Contagem/Comprimento: O nome é priorizado primeiro, ou não é extensão vs extensão?

Também temos uma alternativa existente: Length é priorizada em detrimento da Count propriedade. Deve haver uma extensão Length antes ou depois de uma propriedade que não seja extensão Count ?

Decisão (LDM 2026-03-09): vamos analisar âmbito a âmbito (começando pelo âmbito da instância e avançando pelos âmbitos de extensão), e dentro de cada âmbito procuraremos Length primeiro e depois Count. Veja https://github.com/dotnet/csharplang/blob/main/meetings/2026/LDM-2026-03-09.md#extension-indexers

Confirmar o design proposto para indexadores implícitos

Analisamos âmbito a scopo, começando pelo âmbito da instância e depois avançando para os âmbitos de extensão.
Dentro de um âmbito:

  1. Procuramos um verdadeiro indexador,
  2. caso contrário, se tivermos um único argumento do tipo certo, procuramos um indexador implícito.

Decisão (LDM 2026-03-09): não, assim que passarmos para os âmbimos de extensão, vamos usar resolução de extensão total.

Confirmar o design proposto para padrões de lista

Para um padrão de lista com spread, vamos procurar:

  1. a Length/Count
  2. um real ou implícito this[Index]
  3. um real ou implícito this[Range]

Vamos procurar cada um de forma independente. Mas quando procuramos um implícito this[Index] ou this[Range], as duas partes devem vir do mesmo âmbito:

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

Nota: o tratamento não negativo dos Length padrões entra em ação quando um tipo pode ser usado num padrão de lista (ou seja, é contável e indexável).

Decisão (LDM 2026-03-09): não, seguiremos esta ordem de pesquisa em vez disso:

  1. Os padrões de lista são resolvidos como se procurássemos individualmente por Comprimento/Contagem, indexador de Índice e indexador de Intervalo
  2. Para indexadores de Índice e Intervalo, proceda da seguinte forma: a. Apenas com pesquisa por instâncias, encontre o índice "real" se possível b. Apenas com pesquisa de instâncias, encontrar as partes do indexador implícito, se possível, c. Com pesquisa completa (instância+extensão), encontrar o índice "real" se possível d. Com pesquisa completa (instância+extensão), encontrar as partes do indexador implícito, se possível (cada uma em pesquisas individuais)

O método de extensão Slice também deve contribuir?

_ = c[1..^1];

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

Decisão (LDM 2026-03-09): sim, estamos a tratar os métodos de extensão clássicos e novos exatamente da mesma forma.

A extensão Length deve contribuir para a otimização da spread?

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

Decisão (LDM 2026-03-09): não, é improvável que uma extensão consiga implementar isto de forma eficiente, por isso não ajudaria na otimização.

Devem os indexadores implícitos entrar em jogo para tipos de array ou de string?

Proponho que os indexadores implícitos não contribuam para esses cenários, porque só são possíveis com um corlib malformado (instância Lengthem falta , GetSubArray ou GetSubstring).

Decisão (LDM/Mads por email 2026-04-07): não há indexadores de extensão em strings ou arrays.

Os indexadores de extensão devem entrar em jogo para tipos de array ou strings?

As regras atuais de especificação e base para acesso a arrays e string fazem com que os indexadores de extensão não funcionem em arrays ou strings.
No entanto, a declaração de tais indexadores de extensão é permitida.

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

Nota: não há dúvida para o acesso a elementos de apontador , uma vez que um parâmetro de extensão pode não ser um tipo de apontador.

Decisão (LDM/Mads por email 2026-04-07): não há indexadores de extensão em strings ou arrays.