Indexadores de extensión

Note

Este artículo es una especificación de características. La especificación actúa como documento de diseño de la característica. Incluye cambios de especificación propuestos, junto con la información necesaria durante el diseño y el desarrollo de la característica. Estos artículos se publican hasta que se finalizan los cambios de especificación propuestos y se incorporan en la especificación ECMA actual.

Puede haber algunas discrepancias entre la especificación de características y la implementación completada. Esas diferencias se recogen en las notas de la reunión de diseño de idioma (LDM) pertinentes.

Puede obtener más información sobre el proceso de adopción de especificaciones de características en el estándar del lenguaje C# en el artículo sobre las especificaciones.

Problema planteado por el experto: https://github.com/dotnet/csharplang/issues/9856

Declaración

Gramática

Los indexadores de extensión se agregan al conjunto de miembros permitidos dentro de una declaración de extensión mediante la extensión como se indica a continuación (en relación con las propuestas/csharp-14.0/extensions.md):

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

Al igual que los indexadores normales, los indexadores de extensión no tienen ningún identificador y se identifican mediante su lista de parámetros. Los indexadores de extensión pueden usar el conjunto completo de características que los indexadores normales admiten hoy (cuerpos de descriptor de acceso, miembros con forma de expresión, descriptores de acceso que devuelven referencias, scoped parámetros, atributos, etc.).

Dado que los indexadores siempre son miembros de instancia, un bloque de extensión que declara un indexador debe proporcionar un parámetro receptor con nombre.

Las restricciones existentes en los miembros de extensión siguen aplicándose: los indexadores dentro de una declaración de extensión no pueden especificar abstract, , newoverridevirtual, sealed, , partialprotected o cualquiera de los modificadores de accesibilidad relacionados o init descriptores de acceso.

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

Todas las reglas del estándar de C# que se aplican a los indexadores normales se aplican a los indexadores de extensión, pero los miembros de extensión no tienen un elemento implícito o explícito this.

La regla de inferencia de miembro de extensión existente todavía se aplica: para cada miembro de extensión que no sea de método, todos los parámetros de tipo de su bloque de extensión se deben usar en el conjunto combinado de parámetros de la extensión y el miembro.

atributo IndexerName

IndexerNameAttribute se puede aplicar a un indexador de extensión. El atributo no se emite en los metadatos, pero su valor afecta a conflictos entre miembros, determina el nombre de la propiedad y los descriptores de acceso en los metadatos y se usa al emitir [DefaultMemberAttribute] (vea Metadatos).

Consumo

Acceso a indizador

Las reglas del acceso al indexador se actualizan: si el procesamiento normal del acceso al indexador no encuentra ningún indexador aplicable, se intenta procesar la construcción como acceso al indexador de extensión.

  1. Intente enlazar mediante los indexadores de instancia declarados (o heredados) en el tipo de receptor. Si se encuentra un candidato aplicable, la resolución de sobrecarga selecciona entre esos miembros de instancia como hoy y se detiene.
  2. Intente enlazar mediante los indexadores de instancia implícitos declarados (o heredados) en el tipo de receptor. Si se encuentra un candidato aplicable, la resolución de sobrecarga selecciona entre esos miembros de instancia como hoy y se detiene.
  3. Intente enlazar como acceso de indexador de extensión. Si este proceso encuentra un candidato aplicable, la resolución de sobrecarga selecciona entre esos miembros de extensión, tal como se describe a continuación y se detiene.
  4. Intente enlazar como acceso implícito al indexador de extensión. Si este proceso encuentra un candidato aplicable, la resolución de sobrecarga selecciona entre esos miembros de extensión, tal como se describe a continuación y se detiene.

Nota: la sección de acceso al elemento controla el caso en el que un argumento tiene el tipo dynamic, por lo que nunca se procesa como acceso de indizador.

Acceso al indexador de extensiones

Los miembros de extensión, incluidos los indexadores de extensión, nunca se consideran cuando el receptor es una expresión base_access .

Nota: Solo procesamos un element_access como acceso de indexador si el receptor es una variable o un valor, por lo que los indexadores de extensión nunca se consideran cuando el receptor es un tipo.

Dado un element_accessE[A], el objetivo es identificar un indexador de extensión.

Un indexador de extensión candidata se aplica con respecto a la lista A de argumentos y receptor E si una firma expandida, formada por los parámetros de tipo del bloque de extensión y una lista de parámetros que combina el parámetro de extensión con los parámetros del indexador, es aplicable con respecto a una lista de argumentos que combina el receptor E con la lista Ade argumentos .

Reutilizamos el ámbito del método de extensión: recorremos los mismos ámbitos consultados para la invocación del método de extensión, incluidos los ámbitos léxicos actuales y envolventes y using el espacio de nombres o using static las importaciones.

Teniendo en cuenta cada ámbito a su vez:

  • Los bloques de extensión en declaraciones de clase estática no genéricas en el ámbito actual se consideran.
  • Los indexadores de esos bloques de extensión componen el conjunto candidato.
  • Los candidatos que no son accesibles se quitan del conjunto.
  • Los candidatos que no son aplicables (como se definió anteriormente) se quitan del conjunto.
  • Si el conjunto resultante de indexadores candidatos está vacío, vamos al siguiente ámbito o no se puede resolver un acceso de indexador de extensión si llegamos al último ámbito (continuaremos intentando resolverlo como un indexador implícito de extensión en ese caso).
  • De lo contrario, la resolución de sobrecarga se aplica al conjunto candidato. Si no se puede identificar un único indexador mejor, el acceso al indexador de extensión es ambiguo y se produce un error en tiempo de compilación.

Con este único mejor indexador identificado en el paso anterior, el acceso al indexador se procesa después como una invocación de método estático.

Dependiendo del contexto en el que se use, un acceso de indexador provoca la invocación de la get_accessor o el set_accessor del indexador.
Si el acceso del indexador es el destino de una asignación, se invoca al set_accessor método de implementación estático para asignar un nuevo valor.
En todos los demás casos, se invoca el get_accessor método de implementación estática para obtener el valor actual.
En cualquier caso, la invocación usará argumentos genéricos inferidos durante la comprobación de aplicabilidad y el receptor como primer argumento.

Acceso al indexador implícito de extensión

Si se aplica un acceso implícito System.Index de extensión (o System.Range) al indizador:

  1. el element_access tiene un único argumento de tipo System.Index (o System.Range) y
  2. Se encuentra una propiedad aplicable Length o Count (instancia o extensión) en el tipo de receptor y
  3. Se encuentra un indexador aplicable this[int] (o Slice(int, int)) (instancia o extensión) en el tipo de receptor.

Otros formularios de acceso a elementos

Cualquier construcción que se aplaza al enlace de acceso a elementos (acceso o asignaciones de elementos condicionales NULL, asignaciones de índice en inicializadores de objetos o patrones de lista y propagación) participa automáticamente en la resolución del indexador de extensión descrita anteriormente.

Árboles de expresión

Los indexadores de extensión no se pueden capturar en árboles de expresión.

Documentos XML

La sintaxis CREF permite hacer referencia a un indexador de extensión y sus descriptores de acceso, así como a sus métodos de implementación.

Ejemplo:

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

Los indexadores de extensión siguen el mismo modelo de reducción que las propiedades de la extensión. Para cada tipo de agrupación de extensiones de nivel CLR que contenga al menos un indexador, el compilador emite:

  • Una propiedad de extensión denominada Item (o el valor proporcionado por IndexerNameAttribute) con cuerpos de descriptor de acceso que y un [ExtensionMarkerName] atributo que throw NotImplementedException() hace referencia al tipo de marcador de extensión adecuado.
  • Métodos de implementación denominados get_Item/set_Item en la clase estática envolvente. Estos métodos anteponen el parámetro receiver a la lista de parámetros y contienen los cuerpos definidos por el usuario. Son static y participan en la resolución de sobrecargas de la misma manera que los métodos de implementación para las propiedades de extensión.

Para reflejar el comportamiento de los indexadores normales, el compilador también emite [DefaultMemberAttribute] en cualquier tipo de agrupación de extensiones que contenga uno o varios indexadores de extensión. El atributo MemberName es igual al nombre de metadatos del indexador (Item de forma predeterminada, o el valor de IndexerNameAttribute).

Example

Código fuente:

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

Metadatos emitidos (simplificado a la sintaxis similar a 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) => ...;
}

Problemas abiertos

Sección temporal del documento relacionado con problemas abiertos, incluida la discusión de diseños alternativos

Tratar con params

Si tiene un indexador de extensión con params, como int this[int i, params string[] s] { get; set; }, hay tres maneras de usarlo:

  • indexación de extensión: receiver[i: 0, "Alice", "Bob"]
  • invocación de implementación getter: E.get_Item(receiver, i: 0, "Alice", "Bob")
  • invocación de implementación de establecedor: E.set_Item(...)

¿Pero cuál es la firma del método de implementación del establecedor?
Solo tiene sentido que el último parámetro de una firma de método invocable por el usuario tenga params, por lo que no sirve para ningún propósito en E.set_Item(... extension parameter ..., this i, params string[] s, int value).

Algunas opciones:

  1. no permitir params para los indexadores de extensión que tienen un establecedor
  2. omitir el [ParamArray] atributo en el método de implementación del establecedor
  3. no hacer nada especial (emitir el [ParamArray])

Propondría la opción 2, ya que maximiza params la utilidad. El costo es solo una pequeña diferencia entre la indexación de extensiones y la sintaxis de desambiguación.

Decisión (LDM 2026-02-02): emita [ParamArray] y compruebe ningún impacto negativo en las herramientas

Impacto del valor asignado a la inferencia de tipos

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

Decisión (LDM 2026-02-02): el indexador solo se deduce según el receptor y los argumentos de la lista de argumentos (es decir, el valor asignado no contribuye).

¿Deben las propiedades de extensión Length/Count hacer que se pueda contar un tipo?

Como recordatorio, las extensiones no entran en juego al enlazar indexadores implícitos o range:

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

Por lo tanto, nuestra posición hasta ahora ha sido que las propiedades de extensiones no deben contar como "propiedades countables" en patrones de lista, expresiones de colección e indizadores implícitos.

Si exponemos this[Index] indizadores de extensión o this[Range] en escenarios de acceso a elementos, es natural esperar que el tipo de destino funcione en patrones de lista.
Sin embargo, los patrones de lista requieren una Length propiedad o Count .

¿Deben cumplir las propiedades de extensión ese requisito? (eso parecería 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 => ...;
  }
}

Pero, ¿esas propiedades también deben contribuir a la reserva implícita del indexador (Length/ + SliceCount) que se usa cuando falta un 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 => ...;
  }
}

Decisión (LDM 2026-02-02): las extensiones deben contribuir en todas partes, incluidas las propiedades que se pueden contar y la reserva implícita del indexador.

Confirmar si el acceso al indexador de extensión viene antes o después de los 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] => ...;
  }
}

He especificado e implementado el acceso del indexador de extensión como prioridad sobre los indexadores implícitos, pero ahora cree que deben venir después de evitar interrupciones de compatibilidad innecesarias.

Actualización (LDM 2026-02-02): esto necesita una investigación más detallada. Sí, las extensiones deben venir después de los miembros que no son de extensión, pero más allá de eso necesitamos algunas propuestas concretas a la luz de la decisión anterior para permitir que las extensiones contribuyan a la reserva implícita del indexador.

Decisión (LDM 2026-03-09): el orden es indizadores de instancia reales, luego indizadores de instancia implícitos, indizadores de extensión reales y, a continuación, indexadores de extensión implícitos. Vea https://github.com/dotnet/csharplang/blob/main/meetings/2026/LDM-2026-03-09.md#extension-indexers.

Count/Length: ¿el nombre tiene prioridad en primer lugar o no es de extensión frente a extensión?

También tenemos una reserva existente: Length se da prioridad a la Count propiedad. ¿Debe aparecer una extensión Length antes o después de una propiedad que no sea de extensión Count ?

Decisión (LDM 2026-03-09): buscaremos ámbito por ámbito (a partir del ámbito de instancia y continuar a través de ámbitos de extensión), y dentro de cada ámbito buscaremos Length primero y, a continuación, Count. Vea https://github.com/dotnet/csharplang/blob/main/meetings/2026/LDM-2026-03-09.md#extension-indexers.

Confirmación del diseño propuesto para indizadores implícitos

Buscamos el ámbito por ámbito, empezando por el ámbito de instancia y, a continuación, avanzamos a los ámbitos de extensión.
Dentro de un ámbito:

  1. buscamos un indexador real,
  2. de lo contrario, si tenemos un único argumento del tipo derecho, buscamos un indexador implícito.

Decisión (LDM 2026-03-09): no, una vez que pasemos a ámbitos de extensión, vamos a usar la resolución de extensión todo el camino.

Confirmación del diseño propuesto para los patrones de lista

Para obtener un patrón de lista con una propagación, buscaremos:

  1. a Length/Count
  2. un valor real o implícito this[Index]
  3. un valor real o implícito this[Range]

Buscaremos cada uno de forma independiente. Pero cuando buscamos un elemento implícito this[Index] o this[Range], las dos partes deben provenir del mismo ámbito:

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

Nota: el control no negativo de Length los patrones se inicia cuando se puede usar un tipo en un patrón de lista (es decir, se puede contar e indizar).

Decisión (LDM 2026-03-09): no, seguiremos este orden de búsqueda en su lugar:

  1. Los patrones de lista se resuelven como si buscamos Length/Count, Indexer y Range indexer individualmente.
  2. En Index y Range indexadores, continúe de la siguiente manera: a. Solo con la búsqueda de instancias, busque el índice "real" si es posible b. Solo con la búsqueda de instancias, busque las partes del indexador implícito si es posible c. Con la búsqueda completa (instancia+extensión), busque el índice "real" si es posible d. Con la búsqueda completa (instancia+extensión), busque las partes del indexador implícito si es posible (cada una en búsquedas individuales).

¿Debe contribuir también el método de extensión 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) => ...;
}

Decisión (LDM 2026-03-09): sí, tratamos los métodos de extensión clásicos y nuevos exactamente iguales.

¿Debe contribuir la extensión Length a la optimización de la propagación?

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

Decisión (LDM 2026-03-09): no, es poco probable que una extensión pueda implementar esto de forma eficaz, por lo que no sería útil para la optimización.

¿Deben entrar en juego los indexadores implícitos para los tipos de matriz o cadena?

Propongo que los indizadores implícitos no contribuyan a esos escenarios, ya que solo son posibles con una corlib con formato incorrecto (falta la instancia Lengtho GetSubArrayGetSubstring).

Decisión (LDM/Mads por correo electrónico 2026-04-07): no hay indizadores de extensión en cadenas o matrices.

¿Deben jugar los indexadores de extensión para los tipos de matriz o cadena?

Las reglas actuales de especificación y línea de base para el acceso a matrices y el acceso a cadenas significan que los indizadores de extensión no funcionan en matrices ni cadenas.
Sin embargo, se permite la declaración de estos indexadores de extensión.

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: no hay ninguna pregunta para el acceso al elemento de puntero , ya que es posible que un parámetro de extensión no sea un tipo de puntero.

Decisión (LDM/Mads por correo electrónico 2026-04-07): no hay indizadores de extensión en cadenas o matrices.