Partilhar via


Membros da Extensão

Observação

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

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

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

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

Declaração

Sintaxe

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

As declarações de extensão só devem ser feitas em classes estáticas que não sejam genéricas nem aninhadas.
É um erro que um tipo seja nomeado extension.

Regras de definição do âmbito

Os parâmetros de tipo e o parâmetro recetor de uma declaração de extensão estão no âmbito do corpo da declaração de extensão. É um erro referir-se ao parâmetro receiver dentro de um membro estático, exceto numa expressão nameof. É um erro para os membros declararem parâmetros de tipo ou parâmetros (bem como variáveis locais e funções locais, diretamente no corpo do membro) com o mesmo nome que um parâmetro de tipo ou parâmetro receptor da declaração de extensão.

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

Não é incorreto que os próprios membros tenham o mesmo nome que os parâmetros de tipo ou o parâmetro recetor da declaração de extensão envolvente. Os nomes dos membros não são encontrados diretamente numa simples procura de nomes na declaração de extensão; a pesquisa, portanto, encontrará o parâmetro de tipo ou o parâmetro de receptor desse nome, em vez do membro.

Os membros dão origem a métodos estáticos que são declarados diretamente na classe estática anexa, e esses podem ser encontrados através de uma simples pesquisa de nomes; no entanto, um parâmetro de tipo de declaração de extensão ou parâmetro recetor com o mesmo nome será encontrado primeiro.

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

Classes estáticas como contêineres de extensão

As extensões são declaradas dentro de classes estáticas não genéricas de nível superior, assim como os métodos de extensão atuais, e podem, portanto, coexistir com métodos de extensão clássicos e membros estáticos não extensivos:

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

Declarações de extensão

Uma declaração de extensão é anônima e fornece uma especificação de recetor com quaisquer parâmetros de tipo associados e restrições, seguida por um conjunto de declarações de membro de extensão. A especificação do recetor pode ser na forma de um parâmetro, ou - se apenas os membros da extensão estática são declarados - um 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) { ... }
    }
}

O tipo na especificação do recetor é referido como o tipo de recetor e o nome do parâmetro, se presente, é referido como o parâmetro do recetor.

Se o parâmetro do recetor é nomeado, o tipo de recetor pode não ser estático.
O parâmetro recetor não tem permissão para ter modificadores se ele não é nomeado, e só é permitido ter os modificadores refness listados abaixo e scoped de outra forma.
O parâmetro recetor tem as mesmas restrições que o primeiro parâmetro de um método de extensão clássico.
O [EnumeratorCancellation] atributo é ignorado se for colocado no parâmetro destinatário.

Membros da Extensão

As declarações de membro de extensão são sintaticamente idênticas à instância correspondente e aos membros estáticos nas declarações de classe e struct (com exceção dos construtores). Os membros da instância referem-se ao recetor com o nome do parâmetro do recetor:

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

É um erro especificar um membro de extensão de instância se a declaração de extensão associada não especificar um parâmetro receptor.

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

É um erro especificar os seguintes modificadores em um membro de uma declaração de extensão: abstract, virtual, override, new, sealed, partiale protected (e modificadores de acessibilidade relacionados).
É um erro especificar o readonly modificador em um membro de uma declaração de extensão.
As propriedades em declarações de extensão podem não ter init acessadores.
Os membros de instância não são permitidos se o parâmetro receiver não for nomeado.

Todos os membros devem ter nomes diferentes do nome da classe anexa estática e do nome do tipo estendido, se tiver um.

É um erro decorar um membro da extensão com o [ModuleInitializer] atributo.

Refness

Por padrão, o recetor é passado para os membros da extensão da instância por valor, assim como outros parâmetros. No entanto, um recetor de declaração de extensão em forma de parâmetro pode especificar ref, ref readonly e in, desde que o tipo de recetor é conhecido por ser um tipo de valor.

Anulabilidade e atributos

Os tipos de recetor podem ser ou conter tipos de referência anuláveis, e as especificações do recetor que estão na forma de parâmetros podem 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);
    }
}

Compatibilidade com métodos clássicos de extensão

Os métodos de extensão de instância geram artefatos que correspondem aos produzidos pelos métodos de extensão clássicos.

Especificamente, o método estático gerado tem os atributos, modificadores e nome do método de extensão declarado, bem como lista de parâmetros de tipo, lista de parâmetros e lista de restrições concatenadas a partir da declaração de extensão e da declaração de método nessa ordem:

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

Gera:

[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

Embora os operadores de extensão tenham tipos de operando explícitos, eles ainda precisam ser declarados em uma declaração de extensão:

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

Isso permite que os parâmetros de tipo sejam declarados e inferidos, e é análogo a como um operador regular definido pelo usuário deve ser declarado dentro de um de seus tipos de operando.

Verificação

Atribuibilidade: Para cada membro de extensão não-método, todos os parâmetros de tipo de seu bloco de extensão devem ser usados no conjunto combinado de parâmetros da extensão e do membro.

Singularidade: Dentro de uma determinada classe estática anexa, o conjunto de declarações de membro de extensão com o mesmo tipo de recetor (conversão de identidade de módulo e substituição de nome de parâmetro de tipo) são tratadas como um único espaço de declaração semelhante aos membros dentro de uma declaração de classe ou struct, e estão sujeitas às mesmas regras sobre exclusividade.

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

A aplicação desta regra de exclusividade inclui métodos de extensão clássicos dentro da mesma classe estática. Para efeitos de comparação com métodos dentro de declarações de extensão, o this parâmetro é tratado como uma especificação do recetor juntamente com quaisquer parâmetros de tipo mencionados nesse tipo de recetor, e os restantes parâmetros de tipo e parâmetros de método são usados para a assinatura do 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

Quando uma procura por membro de extensão é realizada, todas as declarações de extensão dentro de classes estáticas que são using-importadas contribuem com os seus membros como candidatos, independentemente do tipo do recetor. Apenas como parte da resolução os candidatos com tipos de recetores incompatíveis são descartados.
Uma inferência de tipo genérica completa é tentada entre o tipo de argumentos (incluindo o recetor real) e quaisquer parâmetros de tipo (combinando aqueles na declaração de extensão e na declaração de membro da extensão).
Quando argumentos de tipo explícitos são fornecidos, eles são usados para substituir os parâmetros de tipo da declaração de extensão e da declaração de membro da extensão.

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

À semelhança dos métodos clássicos de extensão, os métodos de implementação emitidos podem ser invocados estaticamente.
Isso permite que o compilador desambigue entre membros da extensão com o mesmo nome e aridade.

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

Os métodos de extensão estática serão resolvidos como métodos de extensão de instância (vamos considerar um argumento extra do tipo recetor).
As propriedades de extensão serão resolvidas como métodos de extensão, com um único parâmetro (o parâmetro recetor) e um único argumento (o valor real do recetor).

using static Diretivas

Um using_static_directive disponibiliza membros de blocos de extensão na declaração de tipo para acesso à extensão.

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, os membros estáticos acessíveis (exceto métodos de extensão) contidos diretamente na declaração do tipo dado podem ser referenciados diretamente.
Isso significa que os métodos de implementação (exceto aqueles que são métodos de extensão) podem ser usados diretamente 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; }
    }
}

Um using_static_directive ainda não importa métodos de extensão diretamente como métodos estáticos, portanto, o método de implementação para métodos de extensão não estáticos não pode ser invocado diretamente como um 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() { }
    }
}

PrioridadeDeResoluçãoDeSobrecargaAtributo

Os membros da extensão dentro de uma classe estática anexa estão sujeitos à priorização de acordo com os valores ORPA. A classe estática anexa é considerada o "tipo de contenção" que as regras ORPA consideram.
Qualquer atributo ORPA presente em uma propriedade de extensão é copiado para os métodos de implementação para os acessadores da propriedade, de modo que a priorização seja respeitada quando esses acessadores são usados por meio da sintaxe de desambiguação.

Pontos de entrada

Os métodos de blocos de extensão não se qualificam como candidatos a ponto de entrada (ver "7.1 Inicialização da aplicação"). Nota: o método de execução pode ainda ser candidato.

Rebaixamento

A estratégia de redução das declarações de extensão não é uma decisão a nível linguístico. No entanto, além de implementar a semântica da linguagem, ela deve satisfazer certos requisitos:

  • O formato dos tipos, membros e metadados gerados deve ser claramente especificado em todos os casos para que outros compiladores possam consumi-los e gerá-los.
  • Os artefatos gerados devem ser estáveis, no sentido de que modificações posteriores razoáveis não devem comprometer os consumidores que compilaram contra versões anteriores.

Estes requisitos necessitam de ser mais aperfeiçoados à medida que a implementação progride e podem ter de ser comprometidos em casos isolados, a fim de permitir uma abordagem de implementação razoável.

Metadados para declarações

Objetivos

O design abaixo permite:

  • ida e volta dos símbolos da declaração de extensão através de metadados (conjuntos completos e de referência),
  • referências estáveis a membros de extensão (documentos XML),
  • determinação local dos nomes emitidos (útil para EnC),
  • rastreamento de API pública.

Para documentos XML, o identificador do documento para um membro de extensão é o identificador do documento para o membro de extensão em metadados. Por exemplo, o docID usado em cref="Extension.extension(object).M(int)" é M:Extension.<>E__ExtensionGroupingTypeNameForObject.M(System.Int32) e esse docID é estável em recompilações e reordenações de blocos de extensão. Idealmente, ele também permaneceria estável quando as restrições no bloco de extensão mudassem, mas não encontramos um design que conseguisse isso sem efeito prejudicial no design da linguagem para conflitos de membros.

Para EnC, é útil saber localmente (simplesmente olhando para um membro de extensão modificado) onde o membro de extensão atualizado é emitido nos metadados.

Para rastreamento de API pública, nomes mais estáveis reduzem o ruído. Contudo, tecnicamente, os nomes dos tipos de agrupamento de extensão não devem ter influência em tais cenários. Ao olhar para o membro Mda extensão, não importa qual é o nome do tipo de agrupamento de extensão, o que importa é a assinatura do bloco de extensão ao qual ele pertence. A assinatura pública da API não deve ser vista como Extension.<>E__ExtensionGroupingTypeNameForObject.M(System.Int32) , mas sim como Extension.extension(object).M(int). Em outras palavras, os membros da extensão devem ser vistos como tendo dois conjuntos de parâmetros de tipo e dois conjuntos de parâmetros.

Visão geral

Os blocos de extensão são agrupados por sua assinatura de nível CLR. Cada grupo de equivalência CLR é emitido como um tipo de agrupamento de extensão com um nome baseado em conteúdo. Os blocos de extensão dentro de um grupo de equivalência CLR são, então, divididos em subgrupos por equivalência C#. Cada grupo de equivalência C# é emitido como um tipo de marcador de extensão com um nome baseado em conteúdo, dentro do tipo de agrupamento de extensão correspondente. Um tipo de marcador de extensão contém um único método de marcador de extensão que codifica um parâmetro de extensão. O método do marcador de extensão, juntamente com o seu tipo de marcador de extensão, codifica a assinatura de um bloco de extensão com total fidelidade. A declaração de cada membro da extensão é emitida no tipo correto de agrupamento de extensão, refere-se de volta a um tipo de marcador de extensão pelo seu nome através de um atributo e é acompanhada por um método de implementação estática de nível superior com uma assinatura modificada.

Aqui está uma visão geral esquematizada da codificação de metadados:

[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 ...
}

A classe estática que encerra é emitida com um [Extension] atributo.

Assinatura de nível CLR vs. assinatura de nível C#

A assinatura de nível CLR de um bloco de extensão é derivada de:

  • normalizando nomes de parâmetros de tipo para T0, T1, etc ...
  • removendo atributos
  • apagar o nome do parâmetro
  • apagar modificadores de parâmetros (como ref, in, , scoped...)
  • apagar nomes de tuplas
  • Apagando anotações de anulabilidade
  • Apagar notnull restrições

Nota: outras restrições são preservadas, como new(), struct, , classallows ref struct, unmanaged, e restrições de tipo.

Tipos de agrupamento de extensões

Um tipo de agrupamento de extensão é emitido para metadados para cada conjunto de blocos de extensão na origem com a mesma assinatura de nível CLR.

  • Seu nome é indescritível e determinado com base no conteúdo da assinatura de nível CLR. Mais detalhes abaixo.
  • Seus parâmetros de tipo têm nomes normalizados (T0, T1, ...) e não têm atributos.
  • É público e selado.
  • É marcado com a specialname bandeira e um [Extension] atributo.

O nome baseado em conteúdo do tipo de agrupamento de extensões é baseado na assinatura de nível CLR e inclui o seguinte:

  • O nome totalmente qualificado do CLR para o tipo do parâmetro de extensão.
    • Os nomes dos parâmetros de tipo referenciados serão normalizados para T0, T1, etc ... com base na ordem em que aparecem na declaração de tipo.
    • O nome totalmente qualificado não incluirá o assembly (conjunto) que o contém. É comum que os tipos sejam movidos entre assemblies e isso não deve quebrar as referências do documento xml.
  • Restrições de parâmetros de tipo serão incluídas e classificadas de modo que reordená-los no código-fonte não altere o nome. Mais especificamente:
    • As restrições de parâmetros de tipo serão listadas na ordem da declaração. As restrições para o parâmetro de tipo Nth ocorrerão antes do parâmetro de tipo Nth+1.
    • As restrições de tipo serão ordenadas comparando os nomes completos ordinalmente.
    • As restrições de não-tipo são ordenadas deterministicamente e são tratadas de modo a evitar qualquer ambiguidade ou colisão com restrições de tipo.
  • Uma vez que isso não inclui atributos, ele intencionalmente ignora particularidades do C# como nomes de tupla, anulabilidade, etc...

Nota: É garantido que o nome permaneça estável em recompilações, reordenações e alterações específicas da linguagem C# (isto é, que não afetam a assinatura no nível CLR).

Tipos de marcadores de extensão

O tipo de marcador redeclara os parâmetros de tipo do seu tipo de agrupamento que contém (um tipo de agrupamento de extensão) para garantir fidelidade total à visão C# de blocos de extensão.

Um tipo de marcador de extensão é emitido para metadados para cada conjunto de blocos de extensão na origem com a mesma assinatura ao nível de C#.

  • Seu nome é indescritível e determinado com base no conteúdo da assinatura de nível C# do bloco de extensão. Mais detalhes abaixo.
  • Ele rededeclara os parâmetros de tipo para o tipo de agrupamento que contém como sendo aqueles declarados na fonte (incluindo nome e atributos).
  • É público e estático.
  • Está marcado com o specialname ícone.

O nome baseado em conteúdo do tipo de marcador de extensão baseia-se no seguinte:

  • Os nomes dos parâmetros de tipo serão incluídos na ordem em que aparecem na declaração de extensão
  • Os atributos dos parâmetros de tipo serão incluídos e classificados de modo que reordená-los no código-fonte não altere o nome.
  • Restrições de parâmetros de tipo serão incluídas e classificadas de modo que reordená-los no código-fonte não altere o nome.
  • O nome totalmente qualificado C# do tipo estendido
    • Isso incluirá itens como anotações anuláveis, nomes de tuplas, etc...
    • O nome totalmente qualificado não incluirá o conjunto que contém
  • O nome do parâmetro de extensão
  • Os modificadores do parâmetro de extensão (ref, ref readonly, scoped, ...) em uma ordem determinística
  • Os argumentos de nome e de atributo totalmente qualificados para quaisquer atributos aplicados ao parâmetro de extensão numa ordem determinística

Nota: É garantido que o nome permanecerá estável em todas as recompilações e reordenações.
Nota: os tipos de marcadores de extensão e os métodos de marcação de extensão são emitidos como parte de conjuntos de referência.

Método do marcador de extensão

O objetivo do método de marcador é codificar o parâmetro de extensão do bloco de extensão. Por ser um membro do tipo de marcador de extensão, ele pode se referir aos parâmetros de tipo redeclarados do tipo de marcador de extensão.

Cada tipo de marcador de extensão contém um único método, o método de marcador de extensão.

  • É estático, não genérico, com retorno do tipo vazio e é chamado <Extension>$.
  • O seu único parâmetro possui os atributos de refness, tipo e nome do parâmetro de extensão.
    Se o parâmetro extension não especificar um nome, o nome do parâmetro estará vazio.
  • Está marcado com o specialname ícone.

A acessibilidade do método marcador será a menos restritiva entre os membros da extensão declarada correspondente. private é usado se nenhum deles for declarado.

Membros da Extensão

As declarações de método/propriedade em um bloco de extensão na origem são representadas como membros do tipo de agrupamento de extensões em metadados.

  • As assinaturas dos métodos originais são mantidas (incluindo atributos), mas seus corpos são substituídos por throw NotImplementedException().
  • Estes não devem ser referenciados na IL.
  • Métodos, propriedades e seus acessadores são marcados com [ExtensionMarkerName("...")], referindo-se ao nome do tipo de marcador de extensão correspondente ao bloco de extensão para aquele membro.

Métodos de execução

Os corpos de método para declarações de método/propriedade em um bloco de extensão na origem são emitidos como métodos de implementação estática na classe estática de nível superior.

  • Um método de implementação tem o mesmo nome que o método original.
  • Tem parâmetros de tipo derivados do bloco de extensão que são adicionados antes dos parâmetros de tipo do método original (incluindo atributos).
  • Tem a mesma acessibilidade e atributos que o método original.
  • Se ele implementa um método estático, ele tem os mesmos parâmetros e tipo de retorno.
  • Se um método de instância é implementado, recebe um parâmetro anteposto para a assinatura do método original. Os atributos deste parâmetro, refness, type e name, são derivados do parâmetro de extensão declarado no bloco de extensão relevante.
  • Os parâmetros nos métodos de implementação referem-se a parâmetros de tipo pertencentes ao método de implementação, em vez daqueles de um bloco de extensão.
  • Se o membro original for um método ordinário de instância, o método de implementação será marcado com um [Extension] atributo.

Atributo ExtensionMarkerName

O tipo ExtensionMarkerNameAttribute é apenas para uso do compilador - não é permitido no código-fonte. A declaração de tipo é sintetizada pelo compilador se ainda não estiver incluída na compilação.

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: Embora alguns destinos de atributos sejam incluídos com o intuito de garantir compatibilidade futura (tipos aninhados de extensão, campos de extensão, eventos de extensão), AttributeTargets.Constructor não está incluído, pois os construtores de extensão não seriam considerados como construtores.

Exemplo

Nota: usamos nomes simplificados baseados em conteúdo para o exemplo, para facilitar a leitura. Nota: como o C# não pode representar a redeclaração do parâmetro type, o código que representa os metadados não é um código C# válido.

Aqui está um exemplo que ilustra como o agrupamento funciona, sem membros:

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

é emitido 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
}

Aqui está um exemplo que ilustra como os membros são emitidos:

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

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

Sempre que os membros da extensão forem usados na fonte, emitiremos essas referências como métodos de implementação. Por exemplo: uma invocação de enumerableOfInt.Method() seria emitida como uma chamada estática para IEnumerableExtensions.Method<int>(enumerableOfInt).

Documentos XML

Os comentários doc sobre o bloco de extensão são emitidos para o tipo de marcador (o DocID para o bloco de extensão está E.<>E__MarkerContentName_For_ExtensionOfT'1 no exemplo abaixo).
Eles podem fazer referência ao parâmetro de extensão e aos parâmetros de tipo usando <paramref> e <typeparamref> respectivamente).
Nota: você não pode documentar o parâmetro de extensão ou parâmetros de tipo (com <param> e <typeparam>) em um membro de extensão.

Se dois blocos de extensão forem emitidos como um tipo de marcador, seus comentários doc também serão mesclados.

As ferramentas que consomem os documentos xml são responsáveis por copiar o <param> e <typeparam> do bloco de extensão para os membros da extensão, conforme apropriado (ou seja, as informações do parâmetro só devem ser copiadas para membros da instância).

Um <inheritdoc> é emitido nos métodos de implementação e faz referência ao membro de extensão relevante com um cref. Por exemplo, o método de implementação para um getter refere-se à documentação da propriedade de extensão. Se o membro da extensão não tiver comentários doc, então o <inheritdoc> é omitido.

Para blocos de extensão e membros de extensão, não avisamos se:

  • O parâmetro da extensão é documentado, mas os parâmetros do membro da extensão não são.
  • ou vice-versa
  • ou em cenários equivalentes com parâmetros de tipo não documentados

Por exemplo, os seguintes comentários de documentação:

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

Gere o seguinte 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>

Referências CREF

Podemos tratar blocos de extensão como tipos aninhados, que podem ser abordados por sua assinatura (como se fosse um método com um único parâmetro de extensão). Exemplo: E.extension(ref int).M().

Mas um cref não pode abordar um bloco de extensão em si. E.extension(int) pode referir-se a um método chamado "extensão" no 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
  }
}

A pesquisa é conhecida por procurar em todos os blocos de extensão correspondentes.
Como não permitimos referências não qualificadas a membros de extensão, o "cref" também não as permitiria.

A sintaxe seria a seguinte:

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
  ;

É um erro usar extension_member_cref ao nível superior (extension(int).M) ou aninhado em outra extensão (E.extension(int).extension(string).M).

Alterações de grande impacto

Tipos e aliases não podem ser nomeados "extensão".

Questões em aberto

Seção temporária do documento relacionada a questões abertas, incluindo discussão de sintaxe não finalizada e designs alternativos
  • Devemos ajustar os requisitos do recetor ao acessar um membro da extensão? (comentário)
  • Confirmar extension vs. extensions como a palavra-chave (resposta: extension, LDM 2025-03-24)
  • Confirme que queremos não permitir [ModuleInitializer] (resposta: sim, não permitir, LDM 2025-06-11)
  • Confirme que podemos descartar blocos de extensão como candidatos a ponto de entrada (resposta: sim, descartar, LDM 2025-06-11)
  • Confirme a lógica LangVer (ignore novas extensões, vs. considere e relate-as quando selecionadas) (resposta: vincule incondicionalmente e relate erro LangVer, exceto para métodos de extensão de exemplo, LDM 2025-06-11)
  • Será que partial é necessário para blocos de extensão que se fundem e têm os seus comentários de documento fundidos? (Resposta: Os comentários do documento são mesclados silenciosamente quando os blocos são mesclados, sem partial necessidade, confirmado por e-mail 2025-09-03)
  • Confirme que os membros não devem ser nomeados com base no tipo que os contém ou no tipo que está a ser estendido. (resposta: sim, confirmada por e-mail 2025-09-03)

Reveja as regras de agrupamento/conflito à luz da questão da portabilidade: https://github.com/dotnet/roslyn/issues/79043

(resposta: este cenário foi resolvido como parte do novo design de metadados com nomes de tipo baseados em conteúdo, é permitido)

A lógica atual é agrupar blocos de extensão que têm o mesmo tipo de recetor. Isso não leva em conta as restrições. Isso causa um problema de portabilidade com este cenário:

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

A proposta é usar a mesma lógica de agrupamento que estamos planejando para o design de tipo de agrupamento de extensão, ou seja, para levar em conta as restrições de nível CLR (ou seja, ignorando notnull, nomes de tupla, anotações de anulabilidade).

A refness deve ser codificada no nome do tipo de agrupamento?

  • Revisão da proposta que ref não deve ser incluída no nome do tipo de agrupamento de extensão (precisa de mais discussão após o GT revisitar as regras de agrupamento/conflito, LDM 2025-06-23) (resposta: confirmada por e-mail 2025-09-03)
public static class E
{
  extension(ref int)
  {
    public static void M()
  }
}

É emitido como:

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

E a referência CREF de terceiros para E.extension(ref int).M é emitida como M:E.<>ExtensionGroupingTypeXYZ.M(). Se ref for removido ou adicionado a um parâmetro de extensão, provavelmente não queremos que o CREF se quebre.

Não nos importamos muito com este cenário, pois qualquer uso como extensão seria uma ambiguidade:

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

Mas nos preocupamos com esse cenário (para portabilidade e utilidade), e isso deve funcionar com o design de metadados proposto depois de ajustarmos as regras de conflito:

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

Não contabilizar a refness tem um lado negativo, pois perdemos a portabilidade neste cenário:

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

Nome de

  • Devemos desautorizar propriedades de extensão em nameof como fazemos com métodos de extensão clássicos e novos? (resposta: gostaríamos de usar 'nameof(EnclosingStaticClass.ExtensionMember). Necessita de design, provavelmente será adiado do .NET 10. LDM 2025-06-11)

construções baseadas em padrões

Metodologia

  • Onde devem entrar em jogo novos métodos de extensão? (resposta: mesmos lugares onde os métodos clássicos de extensão entram em jogo, LDM 2025-05-05)

Isto inclui:

  • GetEnumerator / GetAsyncEnumerator em foreach
  • Deconstruct na desconstrução, no padrão posicional e no foreach
  • Add em inicializadores de coleção
  • GetPinnableReference no fixed
  • GetAwaiter no await

Isto exclui:

  • Dispose / DisposeAsync em using e foreach
  • MoveNext / MoveNextAsync em foreach
  • Slice e int indexadores em indexações implícitas (e possivelmente padrões de listas?)
  • GetResult no await

Propriedades e indexadores

  • Onde as propriedades de extensão e os indexadores devem entrar em jogo? (resposta: comecemos pelos quatro, LDM 2025-05-05)

Nós incluiríamos:

  • inicializador de objeto: new C() { ExtensionProperty = ... }
  • dicionário inicializador: new C() { [0] = ... }
  • with: x with { ExtensionProperty = ... }
  • Padrões de propriedade: x is { ExtensionProperty: ... }

Excluimos:

  • Current no foreach
  • IsCompleted no await
  • Count / Length Propriedades e indexadores no padrão de lista
  • Count / Length Propriedades e indexadores em indexadores implícitos
Propriedades que devolvem delegados
  • Confirme se as propriedades de extensão desta forma devem apenas ser utilizadas em consultas LINQ, para corresponder ao comportamento das propriedades de instância. (resposta: faz sentido, LDM 2025-04-06)
Padrão de lista e spread
  • Confirme se os indexadores de extensão Index/Range devem ser reproduzidos em padrões de lista (resposta: não relevante para C# 14)
Revisite onde Count/Length as propriedades de extensão entram em jogo

Expressões de coleção

  • Extensão Add funciona
  • Extensão GetEnumerator funciona para divulgação
  • A extensão GetEnumerator não afeta a determinação do tipo de elemento (deve ser instância)
  • Métodos de extensão estática Create não devem contar como um método de criação aprovado
  • Devem as propriedades contáveis da extensão afetar as expressões de coleção?

params coleções

  • As extensões Add não afetam os tipos permitidos com params

expressões de dicionário

  • Confirme que os indexadores de extensão não têm função em expressões de dicionário, pois a presença do indexador é parte integrante do que define um tipo de dicionário. (resposta: não relevante para C# 14)

extern

Esquema de nomenclatura/numeração para o tipo de extensão

Questão
O sistema de numeração atual causa problemas com a validação de APIs públicas , o que garante que as APIs públicas correspondam entre assemblies somente de referência e assemblies de implementação.

Devemos fazer uma das seguintes alterações? (resposta: estamos adotando um esquema de nomenclatura baseado em conteúdo para aumentar a estabilidade da API pública, e as ferramentas ainda precisarão ser atualizadas para levar em conta os métodos de marcador)

  1. Ajustar a ferramenta
  2. usar algum esquema de nomenclatura baseado em conteúdo (TBD)
  3. deixe o nome ser controlado através de alguma sintaxe

Novo método genérico de extensão Cast ainda não pode funcionar no LINQ

Questão
Em designs anteriores de funções/extensões, era possível especificar apenas os argumentos de tipo do método explicitamente.
Mas agora que estamos a concentrar-nos na transição sem problemas dos métodos clássicos de extensão, todos os argumentos de tipo devem ser dados explicitamente.
Isso não resolve um problema com o uso do método Cast de extensão no LINQ.

Devemos fazer uma alteração no recurso de extensões para acomodar esse cenário? (resposta: não, isso não nos faz revisitar o projeto de resolução de extensão, LDM 2025-05-05)

Limitando o parâmetro de extensão em um membro de extensão

Devemos permitir o seguinte? (resposta: não, isso poderia ser adicionado mais tarde)

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

Anulabilidade

  • Confirme o design atual, ou seja, portabilidade/compatibilidade máxima (resposta: sim, 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!;
    }

Metadados

  • Os métodos de esqueleto devem lançar NotSupportedException ou alguma outra exceção padrão (agora nós fazemos throw null;)? (resposta: sim, LDM 2025-04-17)
  • Devemos aceitar mais de um parâmetro no método de marcador em metadados (no caso de novas versões adicionarem mais informações)? (resposta: podemos permanecer rigorosos, LDM 2025-04-17)
  • O marcador de extensão ou os métodos de implementação faláveis devem ser marcados com um nome especial? (resposta: o método do marcador deve ser marcado com nome especial e devemos verificá-lo, mas não métodos de implementação, LDM 2025-04-17)
  • Devemos adicionar [Extension] atributo na classe estática mesmo quando não há nenhum método de extensão de instância dentro? (resposta: sim, LDM 2025-03-10)
  • Confirme se devemos adicionar [Extension] atributos aos getters e setters de implementação também. (resposta: não, LDM 2025-03-10)
  • Confirme que os tipos de extensão devem ser marcados com um nome especial e que o compilador exigirá este sinalizador nos metadados (esta é uma alteração significativa em relação à visualização) (resposta: aprovado, LDM 2025-06-23)

Cenário estático de fábrica

  • Quais são as regras de conflito para métodos estáticos? (resposta: utilize as regras existentes de C# para o tipo estático envolvente, sem relaxamento, LDM 2025-03-17)

Busca

  • Como resolver invocações de método de instância agora que temos nomes de implementação faláveis? Preferimos o método esqueleto ao seu método de implementação correspondente.
  • Como resolver métodos de extensão estática? (resposta: assim como os métodos de extensão de instância, LDM 2025-03-03)
  • Como resolver propriedades? (respondido em traços largos LDM 2025-03-03, mas precisa de acompanhamento para melhoria)
  • Regras de escopo e sombreamento para parâmetros de extensão e parâmetros de tipo (resposta: no escopo do bloco de extensão, sombreamento não permitido, LDM 2025-03-10)
  • Como o ORPA deve ser aplicado aos novos métodos de extensão? (resposta: trate os blocos de extensão como transparentes, o "tipo de contenção" para ORPA é a classe estática anexa, LDM 2025-04-17)
public static class Extensions
{
    extension(Type1)
    {
        [OverloadResolutionPriority(1)]
        public void Overload(...)
    }
    extension(Type2)
    {
        public void Overload(...)
    }
}
  • O ORPA deve ser aplicado a novas propriedades de extensão? (resposta: sim e ORPA deve ser copiado para métodos de implementação, LDM 2025-04-23)
public static class Extensions
{
    extension(int[] i)
    {
        public P { get => }
    }
    extension(ReadOnlySpan<int> r)
    {
       [OverloadResolutionPriority(1)]
       public P { get => }
    }
}
  • Como alterar ou rever as regras clássicas de resolução de extensões? Nós
    1. atualizar o padrão para métodos de extensão clássicos e usá-lo para também descrever novos métodos de extensão,
    2. manter a linguagem existente para métodos de extensão clássicos, usá-la para também descrever novos métodos de extensão, mas ter um desvio de especificação conhecido para ambos,
    3. manter a linguagem existente para métodos de extensão clássicos, mas usar linguagem diferente para novos métodos de extensão, e só tem um desvio de especificação conhecido para métodos de extensão clássicos?
  • Confirme se queremos proibir argumentos de tipo explícitos num acesso a uma propriedade (resposta: nenhum acesso a propriedades com argumentos de tipo explícitos, em discussão no WG)
string s = "ran";
_ = s.P<object>; // error

static class E
{
    extension<T>(T t)
    {
        public int P => 0;
    }
}
  • Confirme que queremos que as regras de melhoria se apliquem mesmo quando o recetor é um tipo (resposta: um parâmetro de extensão apenas de tipo deve ser considerado ao resolver membros de extensão estática, 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;
    }
}
  • Confirme que estamos bem em ter uma ambiguidade quando ambos os métodos e propriedades são aplicáveis (resposta: devemos projetar uma proposta para fazer melhor do que o status quo, trocando o .NET 10, LDM 2025-06-23)
  • Confirme que não queremos melhorias em todos os membros antes de determinar o tipo de membro vencedor (resposta: exclusão do .NET 10, WG 02-07-2025)
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;
    }
}
  • Temos um recetor implícito nas declarações de extensão? (resposta: não, foi discutido anteriormente em LDM)
static class E
{
    extension(object o)
    {
        public void M() 
        {
            M2();
        }
        public void M2() { }
    }
}
  • Devemos permitir a pesquisa no parâmetro type? (discussão) (resposta: não, vamos aguardar feedback, LDM 2025-04-16)

Acessibilidade

  • Qual é o significado de acessibilidade dentro de uma declaração de extensão? (resposta: declarações de extensão não contam como um escopo de acessibilidade, LDM 2025-03-17)
  • Devemos aplicar a verificação de "acessibilidade inconsistente" no parâmetro do recetor, mesmo para membros estáticos? (resposta: sim, 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 { }
}

Validação da declaração de extensão

  • Devemos relaxar a validação de tipo de parâmetro (capacidade de inferência: todos os parâmetros de tipo devem aparecer no tipo do parâmetro de extensão) quando há apenas métodos? (resposta: sim, LDM 2025-04-06) Isso permitiria a transferência de 100% dos métodos clássicos de extensão.
    Se você tiver TResult M<TResult, TSource>(this TSource source), você pode portá-lo como extension<TResult, TSource>(TSource source) { TResult M() ... }.

  • Confirme se os acessadores somente de inicialização devem ser permitidos em extensões (resposta: ok para não permitir por enquanto, LDM 2025-04-17)

  • Deve ser permitida a única diferença na ref-ness do recetor extension(int receiver) { public void M2() {} }extension(ref int receiver) { public void M2() {} }? (resposta: não, manter a regra especificada, LDM 2025-03-24)

  • Devemos queixar-nos de um conflito como este extension(object receiver) { public int P1 => 1; }extension(object receiver) { public int P1 {set{}} }? (resposta: sim, manter a regra especificada, LDM 2025-03-24)

  • Devemos reclamar de conflitos entre métodos esqueleto que não são conflitos entre métodos de implementação? (resposta: sim, manter a regra especificada, LDM 2025-03-24)

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

As normas atuais de conflitos são: 1. Verifique se não há conflito dentro de extensões semelhantes usando regras de classe/struct, 2. Verifique se não há conflito entre os métodos de implementação em várias declarações de extensões.

  • Será que precisamos da primeira parte das regras? (resposta: sim, estamos mantendo essa estrutura, pois ajuda no consumo das APIs, LDM 2025-03-24)

Documentos XML

  • É suportado o parâmetro paramref pelo recetor em membros de extensão? Mesmo em situações estáticas? Como ele é codificado na saída? Provavelmente a maneira <paramref name="..."/> padrão funcionaria para um ser humano, mas há um risco de que algumas ferramentas existentes não fiquem felizes em não encontrá-lo entre os parâmetros na API. (resposta: sim, paramref para parâmetro de extensão é permitido para membros da extensão, LDM 2025-05-05)
  • Devemos copiar comentários de documentação para os métodos de implementação com nomes que possam ser falados? (resposta: sem cópia, LDM 2025-05-05)
  • O elemento <param> correspondente ao parâmetro recetor deve ser copiado do container de extensão para métodos de instância? Qualquer outra coisa deve ser copiada do contêiner para os métodos de implementação (<typeparam> etc.) ? (resposta: sem cópia, LDM 2025-05-05)
  • O parâmetro de extensão deve <param> ser permitido em membros de extensão como uma substituição? (resposta: não, por enquanto, LDM 2025-05-05)
  • O resumo sobre os blocos de extensão apareceria em algum lugar?

CREF

  • Confirmar sintaxe (resposta: proposta é boa, LDM 2025-06-09)
  • Deveria ser possível referir-se a um bloco de extensão (E.extension(int))? (resposta: não, LDM 2025-06-09)
  • Deveria ser possível referir-se a um membro usando uma sintaxe não qualificada: extension(int).Member? (resposta: sim, LDM 2025-06-09)
  • Devemos usar caracteres diferentes para nome indizível, para evitar a fuga de XML? (resposta: deferir WG, LDM 2025-06-09)
  • Confirme se não há problema em que ambas as referências ao esqueleto e aos métodos de implementação sejam possíveis: E.M vs. E.extension(int).M. Ambos parecem necessários (propriedades de extensão e portabilidade dos métodos clássicos de extensão). (resposta: sim, LDM 2025-06-09)
  • Os nomes de metadados de extensão são problemáticos para documentos de versionamento? (resposta: sim, vamos nos afastar dos ordinais e usar um esquema de nomenclatura estável baseado em conteúdo)

Adicionar suporte para mais tipos de membros

Não precisamos implementar todo esse design de uma só vez; podemos optar por trabalhar nele por componentes, um de cada vez ou alguns ao mesmo tempo. Com base em cenários conhecidos em nossas bibliotecas principais, devemos trabalhar na seguinte ordem:

  1. Propriedades e métodos (de instância e estáticos)
  2. Operadores
  3. Indexadores (de instância e estáticos, podem ser feitos oportunisticamente num ponto anterior)
  4. Mais alguma coisa

Quanto queremos priorizar o design para outros tipos de membros?

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 aninhados

Se optarmos por avançar com tipos aninhados de extensão, aqui estão algumas notas de discussões anteriores:

  • Haveria um conflito se duas declarações de extensão declarassem tipos de extensão aninhados com os mesmos nomes e aridade. Não temos uma solução para representar isso em metadados.
  • A abordagem aproximada que discutimos para os metadados:
    1. emitiríamos um tipo aninhado de esqueleto com parâmetros de tipo originais e sem membros
    2. Emitiríamos um tipo aninhado de implementação com parâmetros de tipo adicionados no início da declaração de extensão e todas as implementações de membros tal como aparecem na fonte, exceto pelas referências aos parâmetros de tipo.

Construtores

Os construtores são geralmente descritos como um membro de instância em C#, uma vez que seu corpo tem acesso ao valor recém-criado por meio da palavra-chave this . No entanto, isso não funciona bem para a abordagem baseada em parâmetros para membros de extensão de instância, uma vez que não há nenhum valor anterior para passar como parâmetro.

Em vez disso, os construtores de extensão funcionam mais como métodos de fábrica estáticos. Eles são considerados membros estáticos no sentido de que não dependem de um nome de parâmetro recetor. Seus corpos precisam explicitamente criar e devolver o resultado da construção. O membro em si ainda é declarado com sintaxe de construtor, mas não pode ter this ou base inicializadores e não depende do tipo de recetor ter construtores acessíveis.

Isso também significa que os construtores de extensão podem ser declarados para tipos que não têm construtores próprios, como interfaces e tipos de 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) { ... } 
}

Permite:

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

Formulários mais curtos

O design proposto evita a repetição por membro das especificações do recetor, mas acaba com os membros da extensão sendo aninhados a duas profundidades em uma classe estática e uma declaração de extensão. Provavelmente será comum que as classes estáticas contenham apenas uma declaração de extensão ou que as declarações de extensão contenham apenas um membro, e parece plausível para nós permitir a abreviação sintática desses casos.

Mesclar classes estáticas e declarações de extensão:

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

Isso acaba se parecendo mais com o que temos chamado de uma abordagem "baseada em tipo", onde o container para os membros da extensão é propriamente nomeado.

Unir declaração de extensão e membro de extensão:

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

Isso acaba se parecendo mais com o que temos chamado de abordagem "baseada em membros", onde cada membro da extensão contém sua própria especificação de recetor.