Compartir vía


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 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/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.
Es un error que un tipo se denomine 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, por lo que pueden coexistir con métodos de extensión clásicos y miembros estáticos sin 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 adoptar la forma de un parámetro o, si solo se declaran miembros de extensión estáticos, de 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 correspondientes miembros de instancia y estáticos de las declaraciones de clases y estructuras (a excepción de los compiladores). Los miembros de instancia hacen referencia al receptor con el nombre de parámetro 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).
Es un error especificar el readonly modificador en un miembro de una declaración de extensión.
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.

Todos los miembros tendrán nombres que difieren del nombre de la clase envolvente estática y el nombre del tipo extendido si tiene uno.

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

Refness

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

Nulabilidad y atributos

Los tipos de receptor pueden ser o contener tipos de referencia anulables, 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 concatenadas a partir de la declaración de extensión y la declaración de 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)  { ... }
    }
}

Generates:

[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 operandos explícitos, aún 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 que los parámetros de tipo sean declarados e inferidos, y es análogo a cómo un operador regular definido por el usuario debe ser declarado dentro de uno de sus tipos de operandos.

Checking

Capacidad de inferencia: Para cada miembro de extensión que no sea un método, debe utilizarse todos los parámetros de tipo de su bloque de extensión en el conjunto combinado de parámetros de la extensión y el 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 los métodos de extensión clásicos dentro de la misma clase estática. A efectos de comparación con métodos dentro de declaraciones de extensión, el parámetro this se trata como una especificación de receptor junto con cualquier parámetro de tipo mencionado en ese tipo de receptor, y el resto de parámetros de tipo y parámetros de método se utilizan 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 una búsqueda de miembros de extensión, todas las declaraciones de extensión dentro de clases estáticas que son importadas por using aportan sus miembros como candidatos, independientemente del tipo de receptor. Solo como parte de la resolución se descartan los candidatos con tipos de receptor incompatibles.
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).

Directivas using static

Un using_static_directive hace que los miembros de los bloques de extensión de la declaración de tipo estén disponibles para el acceso a la extensión.

using static N.E;

new object().M();
object.M2();

_ = new object().Property;
_ = object.Property2;

C c = null;
_ = c + c;
c += 1;

namespace N
{
    static class E
    {
        extension(object o)
        {
            public void M() { }
            public static void M2() { }
            public int Property => 0;
            public static int Property2 => 0;
        }

        extension(C c)
        {
            public static C operator +(C c1, C c2) => throw null;
            public void operator +=(int i) => throw null;
        }
    }
}

class C { } 

Como antes, se puede hacer referencia directamente a los miembros estáticos accesibles (excepto los métodos de extensión) contenidos directamente en la declaración del tipo especificado.
Esto significa que los métodos de implementación (excepto los que son métodos de extensión) se pueden usar directamente como métodos estáticos:

using static E;

M();
System.Console.Write(get_P());
set_P(43);
_ = op_Addition(0, 0);
_ = new object() + new object();

static class E
{
    extension(object)
    {
        public static void M() { }
        public static int P { get => 42; set { } }
        public static object operator +(object o1, object o2) { return o1; }
    }
}

Un using_static_directive todavía no importa métodos de extensión directamente como métodos estáticos, por lo que el método de implementación para los métodos de extensión no estáticos no se puede invocar directamente como método estático.

using static E;

M(1); // error: The name 'M' does not exist in the current context

static class E
{
    extension(int i)
    {
        public void M() { }
    }
}

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 de los descriptores de acceso de la propiedad, de modo que se respete la priorización cuando esos descriptores de acceso 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 a nivel de lenguaje. Sin embargo, más allá de implementar la semántica del lenguaje debe satisfacer ciertos requisitos:

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

Estos requisitos deben perfeccionarse a medida que avanza la implementación, y es posible que haya que ceder en algunos casos para permitir un enfoque de implementación razonable.

Metadatos para las declaraciones

Objetivos

El diseño siguiente permite:

  • ida y vuelta de los símbolos de declaración de extensión a través de los metadatos (ensamblados completos y de referencia),
  • referencias estables a miembros de extensión (documentación XML),
  • determinación local de nombres emitidos (útil para EnC),
  • seguimiento de API públicas.

En el caso de los documentos xml, el docID de un miembro de extensión es el docID del miembro de extensión en los metadatos. Por ejemplo, el docID usado en cref="Extension.extension(object).M(int)" es M:Extension.<>E__ExtensionGroupingTypeNameForObject.M(System.Int32) y ese docID es estable en recompilaciones y reordenación de bloques de extensión. Idealmente, también permanecería estable cuando las restricciones en el bloque de extensión cambian, pero no encontramos un diseño que lograra eso sin tener un efecto perjudicial en el diseño del lenguaje para conflictos de miembros.

Para EnC, es útil saber localmente (con solo mirar un miembro de extensión modificado) dónde se emite el miembro de extensión actualizado en los metadatos.

Para el seguimiento de API pública, los nombres más estables reducen el ruido. Técnicamente, los nombres de tipo de agrupación de extensiones no deberían entrar en juego en escenarios como estos. Al examinar el miembro de extensión M, no importa cuál sea el nombre del tipo de agrupación de extensiones, lo que importa es la firma del bloque de extensión al cual pertenece. La firma de API pública no debe considerarse como Extension.<>E__ExtensionGroupingTypeNameForObject.M(System.Int32) sino como Extension.extension(object).M(int). En otras palabras, los miembros de extensión deben considerarse como si tuvieran dos conjuntos de parámetros de tipo y dos conjuntos de parámetros.

Información general

Los bloques de extensión se agrupan mediante su firma de nivel CLR. Cada grupo de equivalencia de CLR se emite como un tipo de agrupación de extensiones con un nombre basado en contenido. Los bloques de extensión dentro de un grupo de equivalencia CLR se subagrupan por equivalencia en C#. Cada grupo de equivalencias de C# se emite como un tipo de marcador de extensión con un nombre basado en contenido, anidado en su tipo de agrupación de extensiones correspondiente. Un tipo de marcador de extensión contiene un único método de marcador de extensión que codifica un parámetro de extensión. El método del marcador de extensión, junto con el tipo de marcador de extensión que contiene, codifica la firma de un bloque de extensión con total fidelidad. La declaración de cada miembro de extensión se emite en el tipo de agrupación de extensión correcto, remite a un tipo de marcador de extensión por su nombre a través de un atributo, y va acompañada de un método de implementación estático de nivel superior con una firma modificada.

Esta es una introducción esquematizada de la codificación de metadatos:

[Extension]
static class EnclosingStaticClass
{
    [Extension]
    public sealed class ExtensionGroupingType1 // has type parameters with minimal constraints sufficient to keep extension member declarations below valid
    {
        public static class ExtensionMarkerType1 // has re-declared type parameters with full fidelity of C# constraints
        {
            public static void <Extension>$(... extension parameter ...) // extension marker method
        }
        ... ExtensionMarkerType2, etc ...

        ... extension members for ExtensionGroupingType1, each points to its corresponding extension marker type ...
    }

    ... ExtensionGroupingType2, etc ...

    ... implementation methods ...
}

La clase estática que la abarca se emite con el atributo [Extension].

Firma de nivel CLR frente a firma de nivel de C#.

La firma de nivel CLR de un bloque de extensión se produce a partir de:

  • normalizar los nombres de parámetro de tipo en T0, T1, etc.
  • quitar atributos
  • borrar el nombre del parámetro
  • Borrado de modificadores de parámetros (como ref, in, scoped...)
  • borrar nombres de tupla
  • borrar anotaciones de anulabilidad
  • borrando restricciones notnull

Nota: se conservan otras restricciones, como new(), struct, class, allows ref struct, unmanaged y restricciones de tipo.

Tipos de agrupación de extensiones

Se emite un tipo de agrupación de extensión a metadatos para cada conjunto de bloques de extensión en el origen con una misma firma de nivel CLR.

  • Su nombre es indescriptible y se determina en función del contenido de la firma de nivel CLR. Más detalles a continuación.
  • Sus parámetros de tipo tienen nombres normalizados (T0, T1, ...) y no tienen atributos.
  • Es público y sellado.
  • Está marcado con la specialname bandera y un atributo [Extension].

El nombre basado en contenido del tipo de agrupación de extensiones se basa en la firma de nivel CLR e incluye lo siguiente:

  • El nombre CLR completamente cualificado del tipo del parámetro de extensión.
    • Los nombres de parámetros de tipo a los que se hace referencia se normalizarán en T0, T1, etc. en función del orden en que aparecen en la declaración de tipo.
    • El nombre completamente cualificado no incluirá el ensamblado que lo contiene. Es habitual que los tipos se muevan entre ensamblados y esto no debe interrumpir las referencias de documento XML.
  • Las restricciones de los parámetros de tipo se incluirán y ordenarán de modo que la reordenación en el código fuente no cambie el nombre. En concreto:
    • Las restricciones de parámetro de tipo se mostrarán en orden de declaración. Las restricciones del parámetro Nth type se producirán antes del parámetro de tipo Nth+1.
    • Las restricciones de tipo se ordenarán comparando los nombres completos de forma ordinal.
    • Las restricciones no de tipo se ordenan de forma determinista y se controlan para evitar cualquier ambigüedad o colisión con restricciones de tipo.
  • Dado que esto no incluye atributos, omite intencionadamente convenciones de C# como los nombres de tuplas, la nulabilidad, etc.

Nota: Se garantiza que el nombre se mantenga estable entre recompilaciones, reordenamientos y cambios de peculiaridades de C# (es decir, que no afectan a la firma de nivel CLR).

Tipos de marcador de extensión

El tipo marcador vuelve a declarar los parámetros de tipo de su tipo de agrupación contenedor (un tipo de agrupación de extensión) para obtener una fidelidad total de la vista de C# de los bloques de extensión.

Se emite un tipo de marcador de extensión a los metadatos para cada conjunto de bloques de extensión en el código fuente con la misma firma de nivel C#.

  • Su nombre es indescriptible y se determina en función del contenido de la firma de nivel de C#del bloque de extensión. Más detalles a continuación.
  • Vuelve a declarar los parámetros de tipo para que contenga el tipo de agrupación que se declare en el origen (incluidos los atributos y el nombre).
  • Es público y estático.
  • Está marcado con la specialname bandera.

El nombre basado en contenido del tipo de marcador de extensión se basa en lo siguiente:

  • Los nombres de los parámetros de tipo se incluirán en el orden en que aparecen en la declaración de extensión.
  • Los atributos de los parámetros de tipo se incluirán y ordenarán de modo que la reordenación en el código fuente no cambie el nombre.
  • Las restricciones de los parámetros de tipo se incluirán y ordenarán de modo que la reordenación en el código fuente no cambie el nombre.
  • El nombre completamente cualificado en C# del tipo extendido
    • Esto incluirá elementos como anotaciones anulables, nombres de tuplas, etc...
    • El nombre completamente cualificado no incluirá el ensamblado que lo contiene
  • Nombre del parámetro de extensión
  • Modificadores del parámetro de extensión (ref, ref readonly, scoped, ...) en un orden determinista
  • El nombre completamente cualificado y los argumentos de atributo para cualquier atributo aplicado al parámetro de extensión en un orden determinista

Nota: Se garantiza que el nombre permanece estable en las recompilaciones y reordenación.
Nota: los tipos de marcadores de extensión y los métodos de marcadores de extensión se emiten como parte de ensamblados de referencia.

Método del marcador de extensión

El propósito del método de marcador es codificar el parámetro de extensión del bloque de extensión. Dado que es un miembro del tipo de marcador de extensión, puede hacer referencia a los parámetros de tipo redeclarados del tipo de marcador de extensión.

Cada tipo de marcador de extensión contiene un único método, el método de marcador de extensión.

  • Es estático, no genérico, devuelto por void y se denomina <Extension>$.
  • Su único parámetro tiene los atributos, referencia, tipo y nombre del parámetro de extensión.
    Si el parámetro de extensión no especifica un nombre, el nombre del parámetro está vacío.
  • Está marcado con la specialname bandera.

La accesibilidad del método de marcador será la accesibilidad menos restrictiva entre los miembros de extensión correspondientes que se hayan declarado, private se usa si no se ha declarado ninguna.

Miembros de extensión

Las declaraciones de métodos o propiedades de un bloque de extensión en el origen se representan como miembros del tipo de agrupación de extensiones en metadatos.

  • Las firmas de los métodos originales se mantienen (incluyendo atributos), pero sus cuerpos son reemplazados con throw NotImplementedException().
  • Estos no deben ser referenciados en IL.
  • Los métodos, las propiedades y sus accesores se marcan con [ExtensionMarkerName("...")], haciendo referencia al nombre del tipo de marcador de extensión correspondiente al bloque de extensión para ese miembro.

Métodos de implementación

Los cuerpos de método para las declaraciones de método/propiedad en un bloque de extensión en el código fuente 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 del bloque 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 tipo de retorno.
  • Si implementa un método de instancia, tiene un parámetro añadido 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 de extensión declarado en el bloque de extensión correspondiente.
  • 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 un bloque 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 .

Atributo ExtensionMarkerName

El tipo ExtensionMarkerNameAttribute es solo para el uso del compilador; no se permite en el origen. El compilador sintetiza la declaración de tipo si aún no está incluida en la compilación.

namespace System.Runtime.CompilerServices;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Interface | AttributeTargets.Delegate, Inherited = false)]
public sealed class ExtensionMarkerNameAttribute : Attribute
{
    public ExtensionMarkerNameAttribute(string name)
        => Name = name;

    public string Name { get; }
}

Nota: Aunque algunos objetivos de atributo se incluyen a prueba de futuro (tipos anidados de extensión, campos de extensión, eventos de extensión), AttributeTargets.Compilador no se incluye ya que los compiladores de extensión no serían compiladores.

Ejemplo

Nota: usamos nombres simplificados basados en contenido para el ejemplo, para mejorar la legibilidad. Nota: Dado que C# no puede representar la nueva declaración del parámetro de tipo, el código que representa los metadatos no es un código de C# válido.

Este es un ejemplo que ilustra cómo funciona la agrupación, sin miembros:

class E
{
    extension<T>(IEnumerable<T> source)
    {
        ... member in extension<T>(IEnumerable<T> source)
    }

    extension<U>(ref IEnumerable<U?> p)
    {
        ... member in extension<U>(ref IEnumerable<U?> p)
    }

    extension<T>(IEnumerable<U> source)
        where T : IEquatable<U>
    {
        ... member in extension<T>(IEnumerable<U> source) where T : IEquatable<U>
    }
}

se emite como

[Extension]
class E
{
    [Extension, SpecialName]
    public sealed class <>E__ContentName_For_IEnumerable_T<T0>
    {
        [SpecialName]
        public static class <>E__ContentName1 // note: re-declares type parameter T0 as T
        {
            [SpecialName]
            public static void <Extension>$(IEnumerable<T> source) { }
        }

        [SpecialName]
        public static class <>E__ContentName2 // note: re-declares type parameter T0 as U
        {
            [SpecialName]
            public static void <Extension>$(ref IEnumerable<U?> p) { }
        }

        [ExtensionMarkerName("<>E__ContentName1")]
        ... member in extension<T>(IEnumerable<T> source)

        [ExtensionMarkerName("<>E__ContentName2")]
        ... member in extension<U>(ref IEnumerable<U?> p)
    }

    [Extension, SpecialName]
    public sealed class <>ContentName_For_IEnumerable_T_With_Constraint<T0>
       where T0 : IEquatable<T0>
    {
        [SpecialName]
        public static class <>E__ContentName3 // note: re-declares type parameter T0 as U
        {
            [SpecialName]
            public static void <Extension>$(IEnumerable<U> source) { }
        }

        [ExtensionMarkerName("ContentName3")]
        public static bool IsPresent(U value) => throw null!;
    }

    ... implementation methods
}

Aquí hay un ejemplo que ilustra cómo se emiten los miembros:

static class IEnumerableExtensions
{
    extension<T>(IEnumerable<T> source) where T : notnull
    {
        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
{
    [Extension, SpecialName]
    public sealed class <>E__ContentName_For_IEnumerable_T<T0>
    {
        // Extension marker type is emitted as a nested type and re-declares its type parameters to include C#-isms
        // In this example, the type parameter `T0` is re-declared as `T` with a `notnull` constraint:
        // .class <>E__IEnumerableOfT<T>.<>E__ContentName_For_IEnumerable_T_Source
        // .typeparam T
        //     .custom instance void NullableAttribute::.ctor(uint8) = (...)
        [SpecialName]
        public static class <>E__ContentName_For_IEnumerable_T_Source
        {
            [SpecialName]
            public static <Extension>$(IEnumerable<T> source) => throw null;
        }

        [ExtensionMarkerName("<>E__ContentName_For_IEnumerable_T_Source")]
        public void Method() => throw null;

        [ExtensionMarkerName("<>E__ContentName_For_IEnumerable_T_Source")]
        internal static int Property
        {
            [ExtensionMarkerName("<>E__ContentName_For_IEnumerable_T_Source")]
            get => throw null;
            [ExtensionMarkerName("<>E__ContentName_For_IEnumerable_T_Source")]
            set => throw null;
        }

        [ExtensionMarkerName("<>E__ContentName_For_IEnumerable_T_Source")]
        public int Property2
        {
            [ExtensionMarkerName("<>E__ContentName_For_IEnumerable_T_Source")]
            get => throw null;
            [ExtensionMarkerName("<>E__ContentName_For_IEnumerable_T_Source")]
            set => throw null;
        }
    }

    [Extension, SpecialName]
    public sealed class <>E__ContentName_For_IAsyncEnumerable_Int
    {
        [SpecialName]
        public static class <>E__ContentName_For_IAsyncEnumerable_Int_Values
        {
            [SpecialName]
            public static <Extension>$(IAsyncEnumerable<int> values) => throw null;
        }

        [ExtensionMarkerName("<>E__ContentName_For_IAsyncEnumerable_Int_Values")]
        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 se utilicen miembros de extensión en el código fuente, los emitiremos como referencia a métodos de implementación. Por ejemplo: una invocación de enumerableOfInt.Method() se emitiría como una llamada estática a IEnumerableExtensions.Method<int>(enumerableOfInt).

Documentos XML

Los comentarios de la documentación del bloque de extensión se emiten para el tipo de marcador (el DocID del bloque de extensión es E.<>E__MarkerContentName_For_ExtensionOfT'1 en el ejemplo siguiente).
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.

Si se emiten dos bloques de extensión como un tipo de marcador, también se combinan sus comentarios de documento.

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.

Se emite un <inheritdoc> en los métodos de implementación y hace referencia al miembro de extensión correspondiente con un cref. Por ejemplo, el método de implementación de un getter remite a la documentación de la propiedad de extensión. Si el miembro de extensión no tiene comentarios doc, se omite <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__MarkerContentName_For_ExtensionOfT`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__MarkerContentName_For_ExtensionOfT`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__MarkerContentName_For_ExtensionOfT`1.P">
            <summary>Summary for P</summary>
        </member>
        <member name="M:E.M``2(``0,``1)">
            <inheritdoc cref="M:E.&lt;&gt;E__MarkerContentName_For_ExtensionOfT`1.M``1(``0)"/>
        </member>
        <member name="M:E.get_P``1(``0)">
            <inheritdoc cref="P:E.&lt;&gt;E__MarkerContentName_For_ExtensionOfT`1.P"/>
        </member>
    </members>
</doc>

Referencias CREF

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

Pero una cref no puede abordar un bloque de extensión. E.extension(int) podría hacer referencia a un método denominado "extensión" en el tipo E.

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

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).

Cambios críticos

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

Abrir propuestas

Sección temporal del documento relacionado con problemas abiertos, incluida la explicación de la sintaxis nofinalizada y los diseños alternativos
  • ¿Deben ajustarse los requisitos del receptor cuando se accede a un miembro de extensión? (comentario)
  • 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 (omita las nuevas extensiones, frente a considerar 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)
  • ¿Debe partial ser necesario para los bloques de extensión que se combinan y tienen sus comentarios de documento combinados? (respuesta: los comentarios de la documentación se combinan silenciosamente cuando los bloques se combinan, no partial es necesario, confirmados por correo electrónico 2025-09-03)
  • Confirme que los miembros no deben tener nombre después de los tipos contenedores o extendidos. (respuesta: Sí, confirmado por correo electrónico 2025-09-03)

Vuelva a consultar las reglas de agrupación o conflicto a la luz del problema de portabilidad: https://github.com/dotnet/roslyn/issues/79043

(respuesta: este escenario se resolvió como parte del nuevo diseño de metadatos con nombres de tipo basados en contenido, se permite).

La lógica actual consiste en agrupar bloques de extensión que tienen el mismo tipo de receptor. Esto no tiene en cuenta las restricciones. Esto provoca un problema de portabilidad con este escenario:

static class E
{
   extension<T>(ref T) where T : struct
      void M()
   extension<T>(T) where T : class
      void M()
}

La propuesta es usar la misma lógica de agrupación que estamos planeando para el diseño del tipo de agrupación de extensiones, es decir, tener en cuenta las restricciones de nivel CLR (es decir, omitir notnull, nombres de tupla, anotaciones de nulabilidad).

¿Debe codificarse refness en el nombre del tipo de agrupación?

  • Revisar la propuesta que ref no se incluya en el nombre del tipo de agrupación de extensiones (necesita más información después de que WG revise las reglas de agrupación y conflictos, LDM 2025-06-23) (respuesta: confirmada por correo electrónico 2025-09-03)
public static class E
{
  extension(ref int)
  {
    public static void M()
  }
}

Se emite como:

public static class E
{
  public static class <>ExtensionTypeXYZ
  {
    .. marker method ...
    void M()
  }
}

Y la referencia CREF de terceros para E.extension(ref int).M se emite como M:E.<>ExtensionGroupingTypeXYZ.M() Si ref se quita o se agrega a un parámetro de extensión, probablemente no queramos que se interrumpa la CREF.

No nos importa mucho este escenario, ya que cualquier uso como extensión sería una ambigüedad:

public static class E
{
  extension(ref int)
    static void M()
  extension(int)
    static void M()
}

Pero nos preocupamos por este escenario (por portabilidad y utilidad), y esto debería funcionar con el diseño de metadatos propuesto después de ajustar las reglas de conflicto:

static class E
{
   extension<T>(ref T) where T : struct
      void M()
   extension<T>(T) where T : class
      void M()
}

No tener en cuenta el refness tiene una desventaja, ya que perdemos portabilidad en este escenario:

static class E
{
   extension<T>(ref T)
      void M()
   extension<T>(T)
      void M()
}
// portability issue: since we're grouping without accounting for refness, the emitted extension members conflict (not implementation members). Mitigation: keep as classic extensions or split to another static class

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 reproducirse en patrones de lista (respuesta: no relevantes para C# 14)
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. (respuesta: no relevante para C# 14)

extern

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

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: estamos adoptando un esquema de nomenclatura basado en contenido para aumentar la estabilidad de la API pública y las herramientas seguirán siendo necesarias para tener en cuenta los métodos de marcador).

  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 extensión 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) (respuesta: aprobada, LDM 2025-06-23)

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;
    }
}
  • Confirmar que queremos que las reglas de betterness se apliquen incluso cuando el receptor es un tipo (respuesta: un parámetro de extensión solo de tipo debería ser considerado cuando se resuelven miembros de extensión estáticos, LDM 2025-06-23)
int.M();

static class E1
{
    extension(int)
    {
        public static void M() { }
    }
}
static class E2
{
    extension(in int i)
    {
        public static void M() => throw null;
    }
}
  • Confirmar que estamos de acuerdo con tener una ambigüedad cuando ambos métodos y propiedades son aplicables (respuesta: debemos diseñar una propuesta para hacerlo mejor que el status quo, señalando a .NET 10, LDM 2025-06-23)
  • Confirmar que no queremos una mejora en todos los miembros antes de determinar el tipo de miembro ganador (respuesta: señalar desde .NET 10, WG 2025-07-02)
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, anteriormente se discutió 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? (respuesta: Sí, vamos a alejarnos de los ordinales y usar un esquema de nomenclatura estable basado en contenido)

Añadir soporte para más tipos de miembros

No necesitamos implementar todo este diseño a la vez, sino que podemos abordarlo de uno en uno o de unos pocos tipos de miembros cada vez. Basándonos en escenarios conocidos en nuestras bibliotecas básicas, deberíamos trabajar en el siguiente orden:

  1. Propiedades y métodos (de instancia y estáticos)
  2. Operadores
  3. Indexadores (de instancia y estáticos, pueden realizarse de forma oportunista en un momento anterior)
  4. Algo más

¿Hasta qué punto queremos adelantar el diseño de 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 decidimos seguir adelante con los tipos de extensión anidados, he aquí algunas notas de 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 metadatos.
  • El enfoque aproximado que analizamos para los metadatos:
    1. emitiríamos un tipo anidado esqueleto con parámetros de tipo originales y ningún miembro
    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

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

En su lugar, los compiladores de extensión funcionan más como 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 miembro en sí todavía se declara con la sintaxis de compilador, pero no puede tener inicializadores this o base y no depende de que el tipo de receptor tenga compiladores accesibles.

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

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

Allows:

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

Formularios más cortos

El diseño propuesto evita la repetición de las especificaciones del receptor por miembro, pero termina con los miembros de extensión anidados en dos niveles dentro de una clase estática y una declaración de extensión. Probablemente será 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 nos parece plausible permitir la abreviación 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 acaba pareciéndose más a lo que hemos estado llamando un enfoque "basado en tipos", en el que el contenedor para los miembros de la extensión se nombra a sí mismo.

Combinar 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 acaba pareciéndose más a lo que hemos estado llamando un enfoque "basado en miembros", donde cada miembro de extensión contiene su propia especificación de receptor.