Compartir a través de


Miembros de extensión

Nota:

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 capturan en las notas pertinentes de la reunión de diseño de lenguaje (LDM).

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

Declaración

Sintaxis

class_body
    : '{' class_member_declaration* '}' ';'?
    | ';'
    ;

class_member_declaration
    : constant_declaration
    | field_declaration
    | method_declaration
    | property_declaration
    | event_declaration
    | indexer_declaration
    | operator_declaration
    | constructor_declaration
    | finalizer_declaration
    | static_constructor_declaration
    | type_declaration
    | extension_declaration // add
    ;

extension_declaration // add
    : 'extension' type_parameter_list? '(' receiver_parameter ')' type_parameter_constraints_clause* extension_body
    ;

extension_body // add
    : '{' extension_member_declaration* '}' ';'?
    ;

extension_member_declaration // add
    : method_declaration
    | property_declaration
    | operator_declaration
    ;

receiver_parameter // add
    : attributes? parameter_modifiers? type identifier?
    ;

Las declaraciones de extensión solo se declararán en clases estáticas no genéricas y no anidadas.
Se trata de un error para que un tipo se llame extension.

Reglas de ámbito

Los parámetros de tipo y el parámetro receptor de una declaración de extensión están en el ámbito dentro del cuerpo de la declaración de extensión. Es un error referirse al parámetro receptor desde dentro de un miembro estático, excepto dentro de una nameof expresión. Es un error que los miembros declaren parámetros de tipo o parámetros (así como variables locales y funciones locales dentro del cuerpo del miembro) con el mismo nombre que un parámetro de tipo o parámetro receptor en la declaración de extensión.

public static class E
{
    extension<T>(T[] ts)
    {
        public bool M1(T t) => ts.Contains(t);        // `T` and `ts` are in scope
        public static bool M2(T t) => ts.Contains(t); // Error: Cannot refer to `ts` from static context
        public void M3(int T, string ts) { }          // Error: Cannot reuse names `T` and `ts`
        public void M4<T, ts>(string s) { }           // Error: Cannot reuse names `T` and `ts`
    }
}

No es un error que los miembros en sí mismos tengan el mismo nombre que los parámetros de tipo o el parámetro receptor de la declaración de extensión envolvente. Los nombres de miembros no se encuentran directamente en una búsqueda de nombres simple dentro de la declaración de extensión; en su lugar, la búsqueda encontrará el parámetro de tipo o el parámetro receptor correspondiente a ese nombre, en vez del miembro.

Los miembros dan lugar a que los métodos estáticos se declaren directamente en la clase estática contenedora, los cuales se pueden localizar mediante una búsqueda sencilla por nombre; sin embargo, primero se localizará un parámetro de tipo de declaración de extensión o un parámetro receptor con el mismo nombre.

public static class E
{
    extension<T>(T[] ts)
    {
        public void T() { M(ts); } // Generated static method M<T>(T[]) is found
        public void M() { T(ts); } // Error: T is a type parameter
    }
}

Clases estáticas como contenedores de extensiones

Las extensiones se declaran dentro de clases estáticas no genéricas de nivel superior, al igual que los métodos de extensión actuales y pueden coexistir con métodos de extensión clásicos y miembros estáticos que no son de extensión:

public static class Enumerable
{
    // New extension declaration
    extension(IEnumerable source) { ... }
    
    // Classic extension method
    public static IEnumerable<TResult> Cast<TResult>(this IEnumerable source) { ... }
    
    // Non-extension member
    public static IEnumerable<int> Range(int start, int count) { ... } 
}

Declaraciones de extensión

Una declaración de extensión es anónima y proporciona una especificación de receptor con cualquier parámetro y restricción de tipo asociado, seguido de un conjunto de declaraciones de miembro de extensión. La especificación del receptor puede estar en forma de parámetro o, si solo se declaran miembros de extensión estáticos, un tipo:

public static class Enumerable
{
    extension(IEnumerable source) // extension members for IEnumerable
    {
        public bool IsEmpty { get { ... } }
    }
    extension<TSource>(IEnumerable<TSource> source) // extension members for IEnumerable<TSource>
    {
        public IEnumerable<T> Where(Func<TSource, bool> predicate) { ... }
        public IEnumerable<TResult> Select<TResult>(Func<TSource, TResult> selector) { ... }
    }
    extension<TElement>(IEnumerable<TElement>) // static extension members for IEnumerable<TElement>
        where TElement : INumber<TElement>
    {
        public static IEnumerable<TElement> operator +(IEnumerable<TElement> first, IEnumerable<TElement> second) { ... }
    }
}

El tipo de la especificación del receptor se conoce como el tipo de receptor y el nombre del parámetro, si está presente, se conoce como parámetro receptor.

Si se denomina el parámetro receiver , es posible que el tipo de receptor no sea estático.
No se permite que el parámetro receptor tenga modificadores si no tiene nombre y solo puede tener los modificadores de refness enumerados a continuación y scoped de lo contrario.
El parámetro receptor tiene las mismas restricciones que el primer parámetro de un método de extensión clásico.
El [EnumeratorCancellation] atributo se omite si se coloca en el parámetro receptor.

Miembros de extensión

Las declaraciones de miembros de extensión son sintácticamente idénticas a los miembros estáticos y de instancia correspondientes en las declaraciones de clase y estructura (a excepción de los constructores). Los miembros de instancia hacen referencia al receptor con el nombre del parámetro del receptor:

public static class Enumerable
{
    extension(IEnumerable source)
    {
        // 'source' refers to receiver
        public bool IsEmpty => !source.GetEnumerator().MoveNext();
    }
}

Es un error especificar un miembro de extensión de instancia si la declaración de extensión circundante no especifica un parámetro de receptor:

public static class Enumerable
{
    extension(IEnumerable) // No parameter name
    {
        public bool IsEmpty => true; // Error: instance extension member not allowed
    }
}

Es un error especificar los modificadores siguientes en un miembro de una declaración de extensión: abstract, virtual, override, new, sealed, partialy protected (y modificadores de accesibilidad relacionados).
En las declaraciones de extensión, las propiedades no pueden tener init accesores.
No se permiten los miembros de instancia si el parámetro receiver no tiene nombre.

Es un error decorar un miembro de extensión con el [ModuleInitializer] atributo .

Referencia

De forma predeterminada, el receptor se pasa a los miembros de la extensión de instancia por valor, al igual que otros parámetros. Sin embargo, un receptor de declaración de extensión en formato de parámetro puede especificar ref, ref readonly y , siempre y incuando se sepa que el tipo de receptor es un tipo de valor.

Si ref se especifica , se puede declarar readonlyun miembro de instancia o uno de sus descriptores de acceso, lo que impide que muta el receptor:

public static class Bits
{
    extension(ref ulong bits) // receiver is passed by ref
    {
        public bool this[int index]
        {
            set => bits = value ? bits | Mask(index) : bits & ~Mask(index); // mutates receiver
            readonly get => (bits & Mask(index)) != 0;                // cannot mutate receiver
        }
    }
    static ulong Mask(int index) => 1ul << index;
}

Nulabilidad y atributos

Los tipos de receptor pueden ser o contener tipos de referencia que aceptan valores NULL y las especificaciones de receptor que están en forma de parámetros pueden especificar atributos:

public static class NullableExtensions
{
    extension(string? text)
    {
        public string AsNotNull => text is null ? "" : text;
    }
    extension([NotNullWhen(false)] string? text)
    {
        public bool IsNullOrEmpty => text is null or [];
    }
    extension<T> ([NotNull] T t) where T : class?
    {
        public void ThrowIfNull() => ArgumentNullException.ThrowIfNull(t);
    }
}

Compatibilidad con métodos de extensión clásicos

Los métodos de extensión de instancia generan artefactos que coinciden con los generados por métodos de extensión clásicos.

En concreto, el método estático generado tiene los atributos, modificadores y nombre del método de extensión declarado, así como la lista de parámetros de tipo, la lista de parámetros y la lista de restricciones concatenada de la declaración de extensión y la declaración del método en ese orden:

public static class Enumerable
{
    extension<TSource>(IEnumerable<TSource> source) // Generate compatible extension methods
    {
        public IEnumerable<TSource> Where(Func<TSource, bool> predicate) { ... }
        public IEnumerable<TSource> Select<TResult>(Func<TSource, TResult> selector)  { ... }
    }
}

Genera:

[Extension]
public static class Enumerable
{
    [Extension]
    public static IEnumerable<TSource> Where<TSource>(IEnumerable<TSource> source, Func<TSource, bool> predicate) { ... }

    [Extension]
    public static IEnumerable<TSource> Select<TSource, TResult>(IEnumerable<TSource> source, Func<TSource, TResult> selector)  { ... }
}

Operadores

Aunque los operadores de extensión tienen tipos de operando explícitos, todavía deben declararse dentro de una declaración de extensión:

public static class Enumerable
{
    extension<TElement>(IEnumerable<TElement>) where TElement : INumber<TElement>
    {
        public static IEnumerable<TElement> operator *(IEnumerable<TElement> vector, TElement scalar) { ... }
        public static IEnumerable<TElement> operator *(TElement scalar, IEnumerable<TElement> vector) { ... }
    }
}

Esto permite declarar e inferir parámetros de tipo, y es análogo a cómo se debe declarar un operador normal definido por el usuario dentro de uno de sus tipos de operando.

Comprobando

Inferencia: Para cada miembro de extensión no método, todos los parámetros de tipo de su bloque de extensión deben usarse en el conjunto combinado de parámetros de la extensión y del miembro.

Unicidad: Dentro de una clase estática envolvente determinada, el conjunto de declaraciones de miembro de extensión con el mismo tipo de receptor (conversión de identidad de módulo y sustitución de nombres de parámetro de tipo) se tratan como un único espacio de declaración similar a los miembros dentro de una declaración de clase o estructura, y están sujetos a las mismas reglas sobre la unicidad.

public static class MyExtensions
{
    extension<T1>(IEnumerable<int>) // Error! T1 not inferrable
    {
        ...
    }
    extension<T2>(IEnumerable<T2>)
    {
        public bool IsEmpty { get ... }
    }
    extension<T3>(IEnumerable<T3>?)
    {
        public bool IsEmpty { get ... } // Error! Duplicate declaration
    }
}

La aplicación de esta regla de unicidad incluye métodos de extensión clásicos dentro de la misma clase estática. Para los fines de la comparación con métodos dentro de declaraciones de extensión, el this parámetro se trata como una especificación de receptor junto con cualquier parámetro de tipo mencionado en ese tipo de receptor, y los parámetros de tipo restantes y los parámetros de método se usan para la firma del método:

public static class Enumerable
{
    public static IEnumerable<TResult> Cast<TResult>(this IEnumerable source) { ... }
    
    extension(IEnumerable source) 
    {
        IEnumerable<TResult> Cast<TResult>() { ... } // Error! Duplicate declaration
    }
}

Consumo

Cuando se intenta realizar una búsqueda de miembros de extensión, todas las declaraciones de extensión dentro de clases estáticas que se importan usingcontribuyen a sus miembros como candidatos, independientemente del tipo de receptor. Solo como parte de la resolución son candidatos con tipos de receptor incompatibles descartados.
Se lleva a cabo una inferencia de tipo genérico completa entre el tipo de los argumentos (incluido el receptor real) y todos los parámetros de tipo (combinando los de la declaración de extensión y los de la declaración de miembro de extensión).
Cuando se proporcionan argumentos de tipo explícitos, se usan para sustituir los parámetros de tipo de la declaración de extensión y la declaración de miembro de extensión.

string[] strings = ...;

var query = strings.Select(s => s.Length); // extension invocation
var query2 = strings.Select<string, int>(s => s.Length); // ... with explicit full set of type arguments

var query3 = Enumerable.Select(strings, s => s.Length); // static method invocation
var query4 = Enumerable.Where<string, int>(strings, s => s.Length); // ... with explicit full set of type arguments
 
public static class Enumerable
{
    extension<TSource>(IEnumerable<TSource> source)
    {
        public IEnumerable<TResult> Select<TResult>(Func<T, TResult> predicate) { ... }
    }
}

De forma similar a los métodos de extensión clásicos, los métodos de implementación emitidos se pueden invocar estáticamente.
Esto permite al compilador resolver ambigüedades entre miembros de extensión con el mismo nombre y aridad.

object.M(); // ambiguous
E1.M();

new object().M2(); // ambiguous
E1.M2(new object());

_ = _new object().P; // ambiguous
_ = E1.get_P(new object());

static class E1
{
    extension(object)
    {
        public static void M() { }
        public void M2() { }
        public int P => 42;
    }
}

static class E2
{
    extension(object)
    {
        public static void M() { }
        public void M2() { }
        public int P => 42;
    }
}

Los métodos de extensión estáticos se resolverán como métodos de extensión de instancia (consideraremos un argumento adicional del tipo de receptor).
Las propiedades de extensión se resolverán como métodos de extensión, con un único parámetro (el parámetro receiver) y un único argumento (el valor real del receptor).

PrioridadResoluciónSobrecargaAtributo

Los miembros de extensión dentro de una clase estática envolvente están sujetos a la priorización según los valores ORPA. La clase estática envolvente se considera el "tipo contenedor" que tienen en cuenta las reglas ORPA.
Cualquier atributo ORPA presente en una propiedad de extensión se copia en los métodos de implementación para los accesores de la propiedad, de modo que se respete la priorización cuando esos accesores se usan mediante la sintaxis de desambiguación.

Puntos de entrada

Los métodos de los bloques de extensión no califican como candidatos de punto de entrada (consulte "Inicio de la aplicación 7.1"). Nota: el método de implementación puede seguir considerándose candidato.

Reducción

La estrategia de reducción de las declaraciones de extensión no es una decisión de nivel de lenguaje. Sin embargo, más allá de implementar la semántica del lenguaje, debe cumplir ciertos requisitos:

  • El formato de los tipos generados, miembros y metadatos debe especificarse claramente en todos los casos para que otros compiladores puedan consumirlo y generarlos.
  • Los artefactos generados deben ser estables, en el sentido de que las modificaciones posteriores razonables no deben interrumpir a los consumidores que se compilaron con versiones anteriores.

Estos requisitos necesitan un refinamiento más a medida que avanza la implementación y es posible que deban ponerse en peligro en casos de esquina para permitir un enfoque de implementación razonable.

Metadatos para declaraciones

Cada declaración de extensión se emite como un tipo de esqueleto con un método de marcador y miembros de esqueleto.
Cada miembro esqueleto va acompañado de un método de implementación estática de nivel superior con una firma modificada.
La clase estática contenedora para una declaración de extensión se marca con un [Extension] atributo .

Esqueletos

Cada declaración de extensión en el origen se emite como una declaración de extensión en los metadatos.

  • Su nombre es indescriptible y determinado en función del orden léxico en el programa.
    No se garantiza que el nombre permanezca estable en la recompilación. A continuación se usa <>E__ seguido de un índice. Por ejemplo: <>E__2.
  • Sus parámetros de tipo son los declarados en el origen (incluidos los atributos).
  • Su accesibilidad es pública.
  • Está marcado con la specialname bandera.

Las declaraciones de métodos o propiedades dentro de una declaración de extensión en el código fuente se representan como miembros base en los metadatos.
Las firmas de los métodos originales se mantienen (incluidos los atributos), pero sus cuerpos se reemplazan por throw null.
No se debe hacer referencia a ellos en IL.

Nota: Esto es similar a los ensamblados ref. El motivo del uso throw null de cuerpos (en lugar de ningún cuerpo) es que la comprobación de IL se pueda ejecutar y pasar (validando así la integridad de los metadatos).

El método de marcador de extensión codifica el parámetro receiver.

  • Es privado y estático, y se denomina <Extension>$.
  • La declaración de extensión incluye los atributos, refness, tipo y nombre del parámetro receptor.
  • Si el parámetro receiver no especifica un nombre, el nombre del parámetro está vacío.

Nota: Esto permite el ciclo completo de los símbolos de declaración de extensiones a través de metadatos (ensamblados completos y de referencia).

Nota: Podemos optar por emitir solo un tipo de esqueleto de extensión en los metadatos cuando se encuentran declaraciones de extensión duplicadas en el origen.

Implementaciones

Los cuerpos de método para las declaraciones de método/propiedad en una declaración de extensión en el origen se emiten como métodos de implementación estáticos en la clase estática de nivel superior.

  • Un método de implementación tiene el mismo nombre que el método original.
  • Tiene parámetros de tipo derivados de la declaración de extensión antepuestos a los parámetros de tipo del método original (incluidos los atributos).
  • Tiene la misma accesibilidad y atributos que el método original.
  • Si implementa un método estático, tiene los mismos parámetros y el tipo de valor devuelto.
  • Si implementa un método de instancia, tiene un parámetro antepuesto a la firma del método original. Los atributos, la referencia, el tipo y el nombre de este parámetro se derivan del parámetro receptor declarado en la declaración de extensión pertinente.
  • Los parámetros de los métodos de implementación hacen referencia a parámetros de tipo propiedad del método de implementación, en lugar de a los de una declaración de extensión.
  • Si el miembro original es un método normal de instancia, el método de implementación se marca con un [Extension] atributo .

Por ejemplo:

static class IEnumerableExtensions
{
    extension<T>(IEnumerable<T> source)
    {
        public void Method() { ... }
        internal static int Property { get => ...; set => ...; }
        public int Property2 { get => ...; set => ...; }
    }

    extension(IAsyncEnumerable<int> values)
    {
        public async Task<int> SumAsync() { ... }
    }

    public static void Method2() { ... }
}

se emite como

[Extension]
static class IEnumerableExtensions
{
    public class <>E__1<T>
    {
        private static <Extension>$(IEnumerable<T> source) => throw null;
        public void Method() => throw null;
        internal static int Property { get => throw null; set => throw null; }
        public int Property2 { get => throw null; set => throw null; }
    }

    public class <>E__2
    {
        private static <Extension>$(IAsyncEnumerable<int> values) => throw null;
        public Task<int> SumAsync() => throw null;
    }

    // Implementation for Method
    [Extension]
    public static void Method<T>(IEnumerable<T> source) { ... }

    // Implementation for Property
    internal static int get_Property<T>() { ... }
    internal static void set_Property<T>(int value) { ... }

    // Implementation for Property2
    public static int get_Property2<T>(IEnumerable<T> source) { ... }
    public static void set_Property2<T>(IEnumerable<T> source, int value) { ... }

    // Implementation for SumAsync
    [Extension]
    public static int SumAsync(IAsyncEnumerable<int> values) { ... }

    public static void Method2() { ... }
}

Siempre que los miembros de la extensión se usen en el origen, los emitiremos como referencia a los métodos de implementación.
Por ejemplo: se emitiría una invocación de enumerableOfInt.Method() como una llamada estática a IEnumerableExtensions.Method<int>(enumerableOfInt).

Documentos XML

Los comentarios del documento en el bloque de extensión se emiten para el tipo denominado indescriptible (el DocID para el bloque de extensión es <>E__0'1 en el ejemplo siguiente).
Los comentarios del documento sobre los miembros de extensión se emiten para los miembros del esqueleto. Se les permite hacer referencia al parámetro de extensión y a los parámetros de tipo mediante <paramref> y <typeparamref> respectivamente).
Nota: No se permite documentar el parámetro de extensión o los parámetros de tipo (con <param> y <typeparam>) para un miembro de extensión.

Las herramientas que consumen los documentos xml son responsables de copiar los <param> y <typeparam> del bloque de extensión a los miembros de la extensión según corresponda, es decir, la información de los parámetros solo debe copiarse para los miembros de instancia.

<inheritdoc> se emite en los métodos de implementación y hace referencia al miembro relevante del esqueleto con un cref. Por ejemplo, el método de implementación de un captador hace referencia a la documentación de la propiedad skeleton. Si el miembro esqueleto no tiene comentarios de documento, entonces se omite el <inheritdoc>.

En el caso de los bloques de extensión y los miembros de la extensión, no se advierte actualmente si:

  • El parámetro de extensión está documentado, pero los parámetros del miembro de extensión no están documentados.
  • o viceversa
  • o en los escenarios equivalentes con parámetros de tipo no documentados

Por ejemplo, los siguientes comentarios de documentación:

/// <summary>Summary for E</summary>
static class E
{
    /// <summary>Summary for extension block</summary>
    /// <typeparam name="T">Description for T</typeparam>
    /// <param name="t">Description for t</param>
    extension<T>(T t)
    {
        /// <summary>Summary for M, which may refer to <paramref name="t"/> and <typeparamref name="T"/></summary>
        /// <typeparam name="U">Description for U</typeparam>
        /// <param name="u">Description for u</param>
        public void M<U>(U u) => throw null!;

        /// <summary>Summary for P</summary>
        public int P => 0;
    }
}

produce el siguiente xml:

<?xml version="1.0"?>
<doc>
    <assembly>
        <name>Test</name>
    </assembly>
    <members>
        <member name="T:E">
            <summary>Summary for E</summary>
        </member>
        <member name="T:E.&lt;&gt;E__0`1">
            <summary>Summary for extension block</summary>
            <typeparam name="T">Description for T</typeparam>
            <param name="t">Description for t</param>
        </member>
        <member name="M:E.&lt;&gt;E__0`1.M``1(``0)">
            <summary>Summary for M, which may refer to <paramref name="t"/> and <typeparamref name="T"/></summary>
            <typeparam name="U">Description for U</typeparam>
            <param name="u">Description for u</param>
        </member>
        <member name="P:E.&lt;&gt;E__0`1.P">
            <summary>Summary for P</summary>
        </member>
        <member name="M:E.M``2(``0,``1)">
            <inheritdoc cref="M:E.&lt;&gt;E__0`1.M``1(``0)"/>
        </member>
        <member name="M:E.get_P``1(``0)">
            <inheritdoc cref="P:E.&lt;&gt;E__0`1.P"/>
        </member>
    </members>
</doc>

Referencias CREF

Podemos tratar bloques de extensión como tipos anidados, que pueden ser direccionados por su firma (como si fuera un método con un único parámetro de extensión). Ejemplo: E.extension(ref int).M().

static class E
{
  extension(ref int i)
  {
    void M() { } // can be addressed by cref="E.extension(ref int).M()"
  }
  extension(ref  int i)
  {
    void M(int i2) { } // can be addressed by cref="E.extension(ref int).M(int)"
  }
}

La búsqueda sabe buscar en todos los bloques de extensión coincidentes.
Al prohibir referencias no calificadas a miembros de extensión, cref también los prohibiría.

La sintaxis sería:

member_cref
  : conversion_operator_member_cref
  | extension_member_cref // added
  | indexer_member_cref
  | name_member_cref
  | operator_member_cref
  ;

extension_member_cref // added
 : 'extension' type_argument_list? cref_parameter_list '.' member_cref
 ;

qualified_cref
  : type '.' member_cref
  ;

cref
  : member_cref
  | qualified_cref
  | type_cref
  ;

Es un error usar extension_member_cref en el nivel superior (extension(int).M) o anidado en otra extensión (E.extension(int).extension(string).M).
Nota: esto no permite cref al bloque de extensión, ya que E.extension(int) se refiere a un método llamado "extension" en el tipo E.

Cambios críticos

Es posible que los tipos y alias no se llamen "extension".

Abrir propuestas

  • Confirme extension contra extensions como palabra clave (respuesta: extension, LDM 2025-03-24)
  • Confirme que queremos no permitir [ModuleInitializer] (respuesta: Sí, no permitir, LDM 2025-06-11)
  • Confirmar que es correcto descartar bloques de extensión como candidatos para punto de entrada (respuesta: sí, descartar, LDM 2025-06-11)
  • Confirme la lógica de LangVer (omitir las nuevas extensiones, frente a considerarla y notificarlas cuando se elija) (respuesta: enlazar incondicionalmente e informar del error LangVer, excepto para los métodos de extensión de instancia, LDM 2025-06-11)
  • ¿Deben ajustarse los requisitos del receptor cuando se accede a un miembro de extensión? (comentario) Por ejemplo, new Struct() { Property = 42 }.

nameof

  • ¿Deberíamos denegar las propiedades de extensión en nameof como hacemos los métodos clásicos y nuevos de extensión? (respuesta: nos gustaría usar 'nameof(EnclosingStaticClass.ExtensionMember). Requiere diseño, probablemente se pospone para .NET 10. LDM 2025-06-11)

construcciones basadas en patrones

Métodos

  • ¿Dónde deben entrar en juego nuevos métodos de extensión? (respuesta: los mismos lugares en los que los métodos de extensión clásicos entran en juego, LDM 2025-05-05) Esto incluye:
  • GetEnumerator / GetAsyncEnumerator en foreach
  • Deconstruct en la deconstrucción, en el patrón posicional y en el ciclo foreach
  • Add en inicializadores de colecciones
  • GetPinnableReference en fixed
  • GetAwaiter en await

Esto excluye:

  • Dispose / DisposeAsync en using y foreach
  • MoveNext / MoveNextAsync en foreach
  • Slice y int indizadores en indizadores implícitos (y posiblemente patrones de lista?)
  • GetResult en await

Propiedades e indexadores

  • ¿Dónde deben entrar en juego las propiedades de extensión y los indexadores? (respuesta: comencemos con los cuatro, LDM 2025-05-05)

Incluiríamos:

  • inicializador de objeto: new C() { ExtensionProperty = ... }
  • inicializador de diccionario: new C() { [0] = ... }
  • with: x with { ExtensionProperty = ... }
  • patrones de propiedad: x is { ExtensionProperty: ... }

Excluiríamos:

  • Current en foreach
  • IsCompleted en await
  • Count / Length propiedades e indizadores en list-pattern
  • Count / Length propiedades e indexadores en indexadores implícitos
Propiedades que devuelven delegados
  • Confirme que las propiedades de extensión de esta estructura deben utilizarse solamente en las consultas LINQ, para alinearse con el comportamiento de las propiedades de instancia. (respuesta: tiene sentido, LDM 2025-04-06)
Patrón de lista y distribución
  • Confirme que los indexadores de extensión Index/Range deben usarse en patrones de lista
Volver a visitar dónde Count/Length entran en juego las propiedades de extensión

Expresiones de colección

  • Funciona la extensión Add
  • La extensión GetEnumerator funciona para la difusión
  • La extensión GetEnumerator no afecta a la determinación del tipo de elemento (debe ser instancia)
  • Los métodos de extensión estáticos Create no deben considerarse como un método de creación autorizado.
  • ¿Deben las propiedades contables de extensión afectar a las expresiones de la colección?

Colecciones de params

  • Las extensiones Add no afectan a qué tipos se permiten con params

expresiones de diccionario

  • Confirme que los indexadores de extensión no intervienen en expresiones de diccionario, ya que la presencia del indexador es una parte integral de lo que define un tipo de diccionario.

extern

Esquema de nomenclatura y numeración para el tipo de esqueleto

Problema
El sistema de numeración actual provoca problemas con la validación de las API públicas , lo que garantiza que las API públicas coincidan entre ensamblados de solo referencia y ensamblados de implementación.

¿Deberíamos realizar uno de los siguientes cambios? (respuesta: ajustaremos la herramienta y ajustaremos la implementación de numeración, LDM 2025-05-05)

  1. ajustar la herramienta
  2. usar algún esquema de nomenclatura basado en contenido (TBD)
  3. permitir que el nombre se controle a través de alguna sintaxis

El nuevo método de conversión de extensión genérica aún no funciona en LINQ

Problema
En diseños anteriores de roles o extensiones, solo era posible especificar los argumentos de tipo del método explícitamente.
Pero ahora que nos centramos en la transición sin problemas desde los métodos de extensión clásicos, se deben proporcionar explícitamente todos los argumentos de tipo.
Esto no puede solucionar un problema con el uso de métodos de conversión de extensión en LINQ.

¿Deberíamos realizar un cambio en la característica de extensiones para dar cabida a este escenario? (respuesta: no, esto no nos hace reconsiderar el diseño de resolución para la extensión, LDM 2025-05-05)

Restricción del parámetro de extensión de un miembro de extensión

¿Deberíamos permitir lo siguiente? (respuesta: no, esto podría agregarse más adelante)

static class E
{
    extension<T>(T t)
    {
        public void M<U>(U u) where T : C<U>  { } // error: 'E.extension<T>(T).M<U>(U)' does not define type parameter 'T'
    }
}

public class C<T> { }

Nulabilidad

  • Confirme el diseño actual, es decir, portabilidad máxima/compatibilidad (respuesta: sí, LDM 2025-04-17)
    extension([System.Diagnostics.CodeAnalysis.DoesNotReturnIf(false)] bool b)
    {
        public void AssertTrue() => throw null!;
    }
    extension([System.Diagnostics.CodeAnalysis.NotNullIfNotNull("o")] ref int? i)
    {
        public void M(object? o)  => throw null!;
    }

Metadatos

  • ¿Deben lanzar métodos esqueleto NotSupportedException o lanzar alguna otra excepción estándar (ahora mismo lo estamos haciendo throw null;)? (respuesta: Sí, LDM 2025-04-17)
  • ¿Deberíamos aceptar más de un parámetro en el método de marcador en los metadatos (en caso de que las nuevas versiones agreguen más información)? (respuesta: podemos permanecer estrictos, LDM 2025-04-17)
  • ¿Deben marcarse los métodos de implementación de marcadores de extensión o de habla con un nombre especial? (respuesta: el método de marcador debe marcarse con un nombre especial y debemos comprobarlo, pero no los métodos de implementación, LDM 2025-04-17)
  • ¿Se debe agregar [Extension] atributo a la clase estática incluso cuando no hay ningún método de extensión de instancia dentro? (respuesta: Sí, LDM 2025-03-10)
  • Confirme que también deberíamos agregar el atributo [Extension] a los métodos de acceso y modificación de la implementación. (respuesta: no, LDM 2025-03-10)
  • Confirme que los tipos de esqueleto deben marcarse con un nombre especial y el compilador requerirá esta marca en los metadatos (se trata de un cambio importante de la versión preliminar).

escenario de fábrica estática

  • ¿Cuáles son las reglas de conflicto para los métodos estáticos? (respuesta: usar las reglas existentes de C# para el tipo estático envolvente, sin flexibilización, LDM 2025-03-17)

Búsqueda

  • ¿Cómo resolver las invocaciones de método de instancia ahora que tenemos nombres de implementación fáciles de pronunciar? Preferimos el método del esqueleto frente a su método de implementación correspondiente.
  • ¿Cómo resolver métodos de extensión estáticos? (respuesta: al igual que los métodos de extensión de instancia, LDM 2025-03-03)
  • ¿Cómo resolver las propiedades? (respondido en términos generales LDM 2025-03-03, pero requiere seguimiento para su mejora)
  • Reglas de ámbito y ocultación para parámetros de extensión y parámetros de tipo (respuesta: dentro del ámbito del bloque de extensión, ocultación no permitida, LDM 2025-03-10)
  • ¿Cómo se debe aplicar ORPA a los nuevos métodos de extensión? (respuesta: tratar los bloques de extensión como transparentes, el "tipo contenedor" para ORPA es la clase estática envolvente, LDM 2025-04-17)
public static class Extensions
{
    extension(Type1)
    {
        [OverloadResolutionPriority(1)]
        public void Overload(...)
    }
    extension(Type2)
    {
        public void Overload(...)
    }
}
  • ¿Debe aplicarse ORPA a las nuevas propiedades de extensión? (respuesta: Sí y ORPA deben copiarse en métodos de implementación, LDM 2025-04-23)
public static class Extensions
{
    extension(int[] i)
    {
        public P { get => }
    }
    extension(ReadOnlySpan<int> r)
    {
       [OverloadResolutionPriority(1)]
       public P { get => }
    }
}
  • ¿Cómo volver a usar las reglas de resolución de extensiones clásicas? ¿Hacemos lo mismo?
    1. actualice el estándar para los métodos de extensión clásicos y úselo para describir también nuevos métodos de extensión,
    2. mantenga el lenguaje existente para los métodos de extensión clásicos, úselo para describir también nuevos métodos de extensión, pero tenga una desviación de especificación conocida para ambos,
    3. mantenga el lenguaje existente para los métodos de extensión clásicos, pero use otro lenguaje para los nuevos métodos de extensión y solo tenga una desviación de especificación conocida para los métodos de extensión clásicos?
  • Confirme que queremos denegar argumentos de tipo explícitos en un acceso a propiedades (respuesta: sin acceso a propiedades con argumentos de tipo explícitos, descritos en WG)
string s = "ran";
_ = s.P<object>; // error

static class E
{
    extension<T>(T t)
    {
        public int P => 0;
    }
}
  • Confirme que queremos que se apliquen las reglas de mejora incluso cuando el receptor es un tipo
int.M();

static class E1
{
    extension(int)
    {
        public static void M() { }
    }
}
static class E2
{
    extension(in int i)
    {
        public static void M() => throw null;
    }
}
  • Confirme que no deseamos alguna mejora en todos los miembros antes de determinar el tipo de miembro ganador.
string s = null;
s.M(); // error

static class E
{
    extension(string s)
    {
        public System.Action M => throw null;
    }
    extension(object o)
    {
        public string M() => throw null;
    }
}
  • ¿Tenemos un receptor implícito dentro de las declaraciones de extensión? (respuesta: no, se ha tratado anteriormente en LDM)
static class E
{
    extension(object o)
    {
        public void M() 
        {
            M2();
        }
        public void M2() { }
    }
}
  • ¿Deberíamos permitir la búsqueda en el parámetro de tipo? (discusión) (respuesta: no, vamos a esperar comentarios, LDM 2025-04-16)

Accesibilidad

  • ¿Cuál es el significado de accesibilidad dentro de una declaración de extensión? (respuesta: las declaraciones de extensión no cuentan como ámbito de accesibilidad, LDM 2025-03-17)
  • ¿Deberíamos aplicar la comprobación de accesibilidad incoherente en el parámetro receiver incluso para miembros estáticos? (respuesta: Sí, LDM 2025-04-17)
public static class Extensions
{
    extension(PrivateType p)
    {
        // We report inconsistent accessibility error, 
        //   because we generate a `public static void M(PrivateType p)` implementation in enclosing type
        public void M() { } 

        public static void M2() { } // should we also report here, even though not technically necessary?
    }

    private class PrivateType { }
}

Validación de declaración de extensión

  • ¿Deberíamos relajar la validación de parámetros de tipo (inferencia: todos los parámetros de tipo deben aparecer en el tipo del parámetro de extensión) donde solo hay métodos? (respuesta: Sí, LDM 2025-04-06) Esto permitiría migrar 100% de métodos de extensión clásicos.
    Si tiene TResult M<TResult, TSource>(this TSource source), podría portarlo como extension<TResult, TSource>(TSource source) { TResult M() ... }.

  • Confirme si deberían permitirse accesores de solo inicialización en extensiones (respuesta: no permitir por ahora, LDM 2025-04-17)

  • ¿Debería permitirse que la única diferencia en el receptor sea el valor de ref-nessextension(int receiver) { public void M2() {} }extension(ref int receiver) { public void M2() {} }? (respuesta: no, mantener la regla especificada, LDM 2025-03-24)

  • ¿Deberíamos quejarnos de un conflicto como este extension(object receiver) { public int P1 => 1; }extension(object receiver) { public int P1 {set{}} }? (respuesta: Sí, mantener la regla especificada, LDM 2025-03-24)

  • ¿Debemos quejarnos de conflictos entre métodos de esqueleto que no son conflictos entre métodos de implementación? (respuesta: Sí, mantener la regla especificada, LDM 2025-03-24)

static class E
{
    extension(object)
    {
        public void Method() {  }
        public static void Method() { }
    }
}

Las reglas de conflicto actuales son: 1. compruebe que no haya ningún conflicto en extensiones similares mediante reglas de clase o estructura, 2. compruebe que no haya ningún conflicto entre los métodos de implementación en varias declaraciones de extensiones.

  • ¿Todavía necesitamos la primera parte de las reglas? (respuesta: Sí, estamos manteniendo esta estructura, ya que ayuda con el consumo de las API, LDM 2025-03-24)

Documentos XML

  • ¿Se admite paramref el parámetro receptor en los miembros de extensión? ¿Incluso en modo estático? ¿Cómo se codifica en la salida? Probablemente el método estándar <paramref name="..."/> funcionaría para un ser humano, pero existe el riesgo de que algunas herramientas existentes no estén satisfechas si no lo encuentran entre los parámetros de la API. (respuesta: sí, se permite paramref al parámetro de extensión en miembros de extensión, LDM 2025-05-05)
  • ¿Se supone que copiamos comentarios de documentación en los métodos de implementación con nombres legibles? (respuesta: sin copia, LDM 2025-05-05)
  • ¿Debe <param> copiarse el elemento correspondiente al parámetro receptor del contenedor de extensiones para los métodos de instancia? ¿Se debe copiar cualquier otra cosa del contenedor a los métodos de implementación (<typeparam> etc.) ? (respuesta: sin copia, LDM 2025-05-05)
  • ¿Se debe <param> permitir el parámetro de extensión en los miembros de extensión como invalidación? (respuesta: no, por ahora, LDM 2025-05-05)
  • ¿Aparecerá el resumen de los bloques de extensión en algún lugar?

CREF

  • Confirmar sintaxis (respuesta: propuesta es buena, LDM 2025-06-09)
  • ¿Debería ser posible hacer referencia a un bloque de extensión (E.extension(int))? (respuesta: no, LDM 2025-06-09)
  • ¿Debería ser posible hacer referencia a un miembro mediante una sintaxis no calificada: extension(int).Member? (respuesta: Sí, LDM 2025-06-09)
  • ¿Deberíamos usar caracteres diferentes para un nombre indescriptible, para evitar el escape XML? (respuesta: aplazar para el WG, LDM 2025-06-09)
  • Confirme que está bien que ambas referencias a los métodos esqueleto e implementación sean posibles: E.M frente a E.extension(int).M. Ambos parecen necesarios (propiedades de extensión y portabilidad de métodos de extensión clásicos). (respuesta: Sí, LDM 2025-06-09)
  • ¿Los nombres de metadatos de extensión son problemáticos para los documentos de control de versiones?

Adición de compatibilidad con más tipos de miembros

No es necesario implementar todo este diseño a la vez, pero puede abordarlo uno o algunos tipos de miembro a la vez. En función de los escenarios conocidos de nuestras bibliotecas principales, deberíamos trabajar en el orden siguiente:

  1. Propiedades y métodos (instancia y estática)
  2. Operadores
  3. Los indexadores (instancia y estática, se pueden realizar de forma oportunista en un punto anterior)
  4. Nada más

¿Cuánto queremos cargar por adelantado el diseño para otros tipos de miembros?

extension_member_declaration // add
    : constant_declaration
    | field_declaration
    | method_declaration
    | property_declaration
    | event_declaration
    | indexer_declaration
    | operator_declaration
    | constructor_declaration
    | finalizer_declaration
    | static_constructor_declaration
    | type_declaration
    ;

Tipos anidados

Si optamos por avanzar con tipos anidados de extensión, estas son algunas notas de las discusiones anteriores:

  • Habría un conflicto si dos declaraciones de extensión definieran tipos de extensión anidados con el mismo nombre y aridad. No tenemos una solución para representar esto en los metadatos.
  • El enfoque aproximado que analizamos para los metadatos:
    1. emitiríamos un tipo anidado de esqueleto con parámetros de tipo original y sin miembros
    2. emitiríamos un tipo anidado de implementación con parámetros de tipo antepuestos de la declaración de extensión y todas las implementaciones de miembro tal como aparecen en el origen (referencias de módulo a parámetros de tipo).

Constructores

Por lo general, los constructores se describen como un miembro de instancia en C#, ya que su cuerpo tiene acceso al valor recién creado a través de la this palabra clave . Sin embargo, esto no funciona bien para el enfoque basado en parámetros para los miembros de la extensión de instancia, ya que no hay ningún valor anterior para pasar como parámetro.

En su lugar, los constructores de extensión funcionan más como los métodos de fábrica estáticos. Se consideran miembros estáticos en el sentido de que no dependen de un nombre de parámetro receptor. Sus cuerpos necesitan crear y devolver explícitamente el resultado de la construcción. El propio miembro todavía se declara con sintaxis de constructor, pero no puede tener this inicializadores o base y no se basa en el tipo de receptor que tiene constructores accesibles.

Esto también significa que los constructores de extensión se pueden declarar para los tipos que no tienen constructores propios, como interfaces y tipos de enumeración:

public static class Enumerable
{
    extension(IEnumerable<int>)
    {
        public static IEnumerable(int start, int count) => Range(start, count);
    }
    public static IEnumerable<int> Range(int start, int count) { ... } 
}

Permite:

var range = new IEnumerable<int>(1, 100);

Formularios más cortos

El diseño propuesto evita la repetición por miembro de las especificaciones del receptor, pero termina con los miembros de extensión anidados dos niveles en una clase estática y en una declaración de extensión. Es probable que sea habitual que las clases estáticas contengan solo una declaración de extensión o que las declaraciones de extensión contengan solo un miembro, y parece probable que permitamos la abreviatura sintáctica de esos casos.

Combinar declaraciones de extensión y clase estáticas:

public static class EmptyExtensions : extension(IEnumerable source)
{
    public bool IsEmpty => !source.GetEnumerator().MoveNext();
}

Esto termina con un aspecto más parecido a lo que hemos estado llamando a un enfoque "basado en tipos", donde el contenedor para miembros de extensión se denomina.

Fusionar declaración de extensión y miembro de extensión:

public static class Bits
{
    extension(ref ulong bits) public bool this[int index]
    {
        get => (bits & Mask(index)) != 0;
        set => bits = value ? bits | Mask(index) : bits & ~Mask(index);
    }
    static ulong Mask(int index) => 1ul << index;
}
 
public static class Enumerable
{
    extension<TSource>(IEnumerable<TSource> source) public IEnumerable<TSource> Where(Func<TSource, bool> predicate) { ... }
}

Esto termina con un aspecto más parecido al que hemos estado llamando a un enfoque "basado en miembros", donde cada miembro de extensión contiene su propia especificación de receptor.