Compartilhar via


Membros de extensão

Observação

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

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

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.

Problema do especialista: 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?
    ;

Declarações de extensão devem ser feitas apenas em classes estáticas que não sejam genéricas e não sejam aninhadas.
É um erro para um tipo ser chamado de extension.

Regras de escopo

Os parâmetros de tipo e o parâmetro receptor de uma declaração de extensão estão no escopo dentro do corpo da declaração de extensão. É um erro fazer referência ao parâmetro receptor de dentro de um membro estático, exceto dentro de uma expressão nameof. É um erro para os membros declarar parâmetros ou parâmetros de tipo (bem como variáveis locais e funções locais diretamente dentro do corpo do membro) com o mesmo nome de 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 é um erro que os membros em si tenham o mesmo nome que os parâmetros de tipo ou o parâmetro receptor da declaração de extensão abrangente. Os nomes de membros não são encontrados diretamente em uma pesquisa de nome simples dentro da declaração da extensão; assim, a pesquisa encontrará o parâmetro de tipo ou o parâmetro de receptor desse nome, em vez do membro.

Os membros fazem com que métodos estáticos sejam declarados diretamente na classe estática envolvente e possam ser encontrados por meio de uma pesquisa por nome simples; no entanto, um parâmetro de tipo de declaração de extensão ou um parâmetro receptor 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 hoje, e podem, portanto, coexistir com métodos de extensão clássicos e membros estáticos não de extensão:

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 receptor com quaisquer parâmetros e restrições de tipo associados, seguido por um conjunto de declarações de membro de extensão. A especificação do receptor pode estar na forma de um parâmetro ou, se apenas membros de extensão estáticos forem 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 receptor é conhecido como o tipo de receptor e o nome do parâmetro, se presente, é conhecido como o parâmetro receptor.

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

Membros de extensão

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

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 delimitante não especificar um parâmetro de 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, partial, e protected (e modificadores de acessibilidade relacionados).
É um erro especificar o readonly modificador em um membro de uma declaração de extensão.
As propriedades nas declarações de extensão não podem ter init acessores.
Os membros de instância não são permitidos se o parâmetro receptor não tiver nome.

Todos os membros devem ter nomes que diferem do nome da classe estática que os contém e do nome do tipo estendido, se houver um.

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

Refness

Por padrão, o receptor é transmitido para membros de extensão de instância por valor, assim como outros parâmetros. No entanto, um receptor de declaração de extensão na forma de parâmetro pode especificar ref, ref readonly e in, desde que o tipo de receptor seja conhecido como um tipo de valor.

Nulidade e atributos

Os tipos de receptor podem ser ou conter tipos de referência anuláveis, e as especificações do receptor 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 de extensão clássicos

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

Especificamente, o método estático gerado tem os atributos, os modificadores e o 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 concatenada 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)  { ... }
    }
}

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

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 em um dos tipos de operando.

Checking

Inferência: para cada membro de extensão que não seja método, todos os parâmetros de tipo do 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, o conjunto de declarações de membro de extensão com o mesmo tipo de receptor (conversão de identidade de modulo e substituição de nome de parâmetro de tipo) é tratado como um único espaço de declaração semelhante aos membros dentro de uma classe ou declaração de struct e estão sujeitos à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 dessa regra de singularidade inclui métodos de extensão clássicos dentro da mesma classe estática. Para fins de comparação com métodos em declarações de extensão, o parâmetro this é tratado como uma especificação de receptor junto com quaisquer parâmetros de tipo mencionados nesse tipo de receptor, e os parâmetros de tipo e os parâmetros de método restantes 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 pesquisa de membro de extensão é tentada, todas as declarações de extensão dentro de classes estáticas importadas por usingcontribuem com os membros como candidatos, independentemente do tipo de receptor. Somente como parte da resolução, os candidatos com tipos de receptor incompatíveis são descartados.
Uma inferência completa de tipo genérico é tentada entre o tipo dos argumentos (incluindo o receptor real) e quaisquer parâmetros de tipo (combinando-os na declaração de extensão e na declaração de membro de 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 de 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) { ... }
    }
}

Da mesma forma que os métodos de extensão clássicos, os métodos de implementação emitidos podem ser invocados estaticamente.
Isso permite que o compilador desambigue os membros de 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;
    }
}

Métodos de extensão estáticos serão resolvidos como métodos de extensão de instância (consideraremos um argumento extra do tipo receptor).
As propriedades de extensão serão resolvidas como métodos de extensão, com um único parâmetro (o parâmetro receptor) e um único argumento (o valor real do receptor).

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

AtributoDePrioridadeDeResoluçãoDeSobrecarga

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

Pontos de entrada

Métodos de blocos de extensão não são elegíveis como possíveis pontos de entrada (consulte "7.1 Inicialização do aplicativo"). Observação: o método de implementação ainda pode ser um candidato.

Abaixamento

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

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

Esses requisitos precisam de mais refinamento conforme a implementação avança e podem precisar ser comprometidos em casos extremos para permitir uma abordagem de implementação razoável.

Metadados para declarações

Metas

O design a seguir permite:

  • transporte de ida e volta de símbolos de declaração de extensão por metadados (assemblies completos e de referência),
  • referências estáveis a membros de extensão (documentos xml),
  • determinação local de nomes emitidos (útil para EnC),
  • monitoramento de API pública

Para documentos xml, o docID de um membro de extensão é o mesmo do docID nos metadados. Por exemplo, o docID usado cref="Extension.extension(object).M(int)" é M:Extension.<>E__ExtensionGroupingTypeNameForObject.M(System.Int32) e esse docID é estável entre reescrições e reordenações de blocos de extensão. O ideal é que ele também permaneça estável quando as restrições no bloco de extensão forem alteradas, mas não encontramos um design que alcançaria isso sem efeito prejudicial no design da linguagem para conflitos de membros.

Para EnC, é útil saber localmente (apenas examinando um membro de extensão modificado) onde o membro de extensão atualizado é emitido em metadados.

Para acompanhamento de API pública, nomes mais estáveis reduzem o ruído. No entanto, tecnicamente, os nomes de tipo de agrupamento de extensão não deveriam interferir nesses cenários. Ao examinar o membro Mda extensão, não importa 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 da API pública 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 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 sub-agrupados pela 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, sendo aninhado no correspondente tipo de agrupamento de extensão. 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, usando seu tipo de marcador de extensão, codifica a assinatura de um bloco de extensão com fidelidade total. A declaração de cada membro da extensão é emitida no tipo adequado de agrupamento de extensão, refere-se a um tipo de marcador de extensão por nome usando um atributo e é acompanhada por um método estático de implementação 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 envolvente é emitida com um atributo [Extension].

Assinatura de nível CLR versus assinatura de nível C#

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

  • normalizando nomes de parâmetro de tipo para T0, T1, etc...
  • removendo atributos
  • apagando o nome do parâmetro
  • apagando modificadores de parâmetro (como ref, in, , scoped...)
  • removendo nomes de tupla
  • apagando anotações de nulidade
  • removendo restrições de notnull

Observação: outras restrições são preservadas, como new(), , struct, class, allows ref structe unmanagedrestrições de tipo.

Tipos de agrupamento de extensão

Um tipo de agrupamento de extensões é emitido em metadados para cada conjunto de blocos de extensão no código-fonte com a mesma assinatura no nível CLR.

  • Seu nome é impronunciável e determinado com base no conteúdo da assinatura no 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 lacrado.
  • Ele é marcado com o specialname sinalizador e um [Extension] atributo.

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

  • O nome CLR totalmente qualificado do tipo do parâmetro de extensão.
    • Os nomes de parâmetro de tipo referenciados serão normalizados para T0, etc T1... com base na ordem em que aparecem na declaração de tipo.
    • O nome totalmente qualificado não incluirá o assembly que o contém. É comum que os tipos sejam movidos entre assemblies e que não devem interromper as referências de 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. Especificamente:
    • As restrições de parâmetro de tipo serão listadas na ordem de 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 classificadas ao comparar os nomes completos ordinalmente.
    • Restrições 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.
  • Como isso não inclui atributos, ele ignora intencionalmente as particularidades do C#, como nomes de tupla, nulabilidade, etc...

Observação: o nome tem a garantia de permanecer estável entre recompilações, reordenações e alterações de particularidades do C# (ou seja, que não afetam a assinatura no nível CLR).

Tipos de marcador de extensão

O tipo de marcador declara novamente os parâmetros de tipo do seu tipo de agrupamento contido (um tipo de agrupamento de extensão) para obter total fidelidade da vista C# dos blocos de extensão.

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

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

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 aparecerem na declaração de extensão
  • Os atributos de 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 completo em C# do tipo estendido
    • Isso incluirá itens como anotações anuláveis, nomes de tupla etc...
    • O nome totalmente qualificado não incluirá o assembly que o 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
  • O nome completamente qualificado e os argumentos de atributo para quaisquer atributos são aplicados ao parâmetro de extensão em uma ordem determinística.

Observação: é garantido que o nome permaneça estável em recompilações e reordenações.
Observação: tipos de marcador de extensão e métodos de marcador de extensão são emitidos como parte de assemblies de referência.

Método de marcador de extensão

A finalidade do método de marcador é codificar o parâmetro de extensão do bloco de extensão. Como ele é um membro do tipo de marcador de extensão, pode se referir aos parâmetros de tipo declarados novamente 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.

  • Ele é estático, não genérico, que retorna nulo e é chamado <Extension>$.
  • Seu único parâmetro possui os atributos, refness, tipo e nome do parâmetro de extensão.
    Se o parâmetro de extensão não especificar um nome, o nome do parâmetro estará vazio.
  • Está marcado com o specialname flag.

A acessibilidade do método de marcador será a acessibilidade menos restritiva entre os membros de extensão declarados correspondentes, private será usada se nenhum for declarado.

Membros de 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ão nos metadados.

  • As assinaturas dos métodos originais são mantidas (incluindo atributos), mas os corpos são substituídos por throw NotImplementedException().
  • Esses não devem ser referenciados em IL.
  • Métodos, propriedades e seus acessadores são marcados com [ExtensionMarkerName("...")] que fazem referência ao nome do tipo de marcador de extensão correspondente ao bloco de extensão desse membro.

Métodos de implementação

Os corpos do 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áticos na classe estática de nível superior.

  • Um método de implementação tem o mesmo nome do método original.
  • Ele tem parâmetros de tipo derivados do bloco de extensão anexado aos parâmetros de tipo do método original (incluindo atributos).
  • Ele tem a mesma acessibilidade e os mesmos atributos do método original.
  • Se implementar um método estático, ele terá os mesmos parâmetros e tipo de retorno.
  • Se implementar um método de instância, ele terá um parâmetro anexado à assinatura do método original. Os atributos, a refness, o tipo e o nome desse parâmetro 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 comum de instância, o método de implementação será marcado com um [Extension] atributo.

Atributo ExtensionMarkerName

O tipo ExtensionMarkerNameAttribute é somente para uso do compilador; ele não é permitido na origem. 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; }
}

Observação: embora alguns destinos de atributo sejam incluídos para assegurar a compatibilidade futura (tipos aninhados de extensão, campos de extensão, eventos de extensão), o AttributeTargets.Constructor não está incluído, pois os construtores de extensão não atuariam como construtores.

Exemplo

Observação: usamos nomes simplificados baseados em conteúdo para o exemplo, para legibilidade. Observação: como O C# não pode representar a nova declaração de parâmetro de tipo, o código que representa 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
}

Confira a seguir 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 no código-fonte, nós os emitiremos como referência aos 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 do documento sobre o bloco de extensão são emitidos para o tipo de marcador (o DocID do bloco de extensão é E.<>E__MarkerContentName_For_ExtensionOfT'1 no exemplo abaixo).
Eles têm permissão para fazer referência ao parâmetro de extensão e aos parâmetros de tipo usando <paramref> e <typeparamref> , respectivamente).
Observação: você não pode documentar o parâmetro de extensão ou os 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 de documento também serão mesclados.

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

Um <inheritdoc> é emitido em métodos de implementação, referindo-se ao membro de extensão relevante com um cref. Por exemplo, o método de implementação de um getter refere-se à documentação da propriedade de extensão. Se o membro da extensão não tiver comentários de documentação, <inheritdoc> será omitido.

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

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

Por exemplo, os seguintes comentários do documento:

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

Podemos tratar blocos de extensão como tipos aninhados, que podem ser resolvidos 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 propriamente dito. E.extension(int) poderia se referir a um método chamado "extension" 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 aos membros da extensão, o cref também as proibiria.

A sintaxe ficaria:

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 em nível superior (extension(int).M) ou aninhado em outra extensão (E.extension(int).extension(string).M).

Alterações da falha

Tipos e aliases não podem ser chamados de "extensão".

Problemas abertos

Seção temporária do documento relacionada a problemas abertos, incluindo discussão sobre sintaxe não finalizada e designs alternativos
  • Devemos ajustar as configurações do receptor ao acessar um membro de extensão? (comentário)
  • Confirme extension vs. extensions como a palavra-chave (resposta: extension, LDM 2025-03-24)
  • Confirme se queremos não permitir [ModuleInitializer] (resposta: sim, não permitido, LDM 2025-06-11)
  • Confirme se não há problema em descartar blocos de extensão como candidatos de ponto de entrada (resposta: sim, descartar, LDM 2025-06-11)
  • Confirmar a lógica LangVer (ignorar novas extensões, vs. considerá-las e denunciá-las quando escolhidas) (resposta: associar incondicionalmente e relatar erro LangVer, exceto por métodos de extensão de instância, LDM 2025-06-11)
  • Deve partial ser obrigatório para blocos de extensão que se mesclam e têm seus comentários de documentação unificados? (resposta: os comentários do documento são mesclados silenciosamente quando os blocos são mesclados, sem partial necessidade, confirmados pelo email 2025-09-03)
  • Confirme que os membros não devem ser nomeados com base nos tipos envolvidos ou estendidos. (resposta: sim, confirmado pelo email 2025-09-03)

Revisite regras de agrupamento/conflito à luz do problema de portabilidade: https://github.com/dotnet/roslyn/issues/79043

(resposta: esse 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 receptor. Isso não explica 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 planejamos para o tipo de design de agrupamento de extensão, ou seja, levando em consideração as restrições de nível CLR (ou seja, ignorando notnull, nomes de tupla, anotações de nulidade).

A refness deveria ser incorporada no nome do tipo de agrupamento?

  • Revise a proposta que não será incluída no nome do tipo de agrupamento de extensão (precisa de mais discussão depois que ref o WG revisita regras de agrupamento/conflito, LDM 2025-06-23) (resposta: confirmada pelo email 2025-09-03)
public static class E
{
  extension(ref int)
  {
    public static void M()
  }
}

Ele é emitido como:

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

E referências cref de terceiros para E.extension(ref int).M são emitidas 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 seja interrompido.

Não nos importamos muito com esse 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 levar em conta a refness tem uma desvantagem, 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 não permitir propriedades de extensão no nameof como fazemos com métodos de extensão clássicos e novos? (resposta: gostaríamos de usar 'nameof(EnclosingStaticClass.ExtensionMember). Precisa de design, provavelmente será adiado para .NET 10. LDM 2025-06-11)

constructos baseados em padrão

Métodos

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

Isso inclui:

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

Isso exclui:

  • Dispose / DisposeAsync em using e foreach
  • MoveNext / MoveNextAsync em foreach
  • Slice e int indexadores em indexadores implícitos (e possivelmente padrões de lista?)
  • GetResult em await

Propriedades e indexadores

  • Onde as propriedades de extensão e os indexadores devem entrar em jogo? (resposta: vamos começar com o 4, LDM 2025-05-05)

Incluímos:

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

Excluiríamos:

  • Current em foreach
  • IsCompleted em await
  • Count / Length propriedades e indexadores no padrão de lista
  • Count / Length propriedades e indexadores em indexadores implícitos
Propriedades de retorno de delegado
  • Confirme se as propriedades de extensão desta forma só devem ser aplicadas em consultas LINQ para serem consistentes com o que as propriedades da instância fazem. (resposta: faz sentido, LDM 2025-04-06)
Listar e difundir padrão
  • Confirme se os indexadores de extensão Index/Range devem ser reproduzidos em padrões de lista (resposta: não relevante para C# 14)
Revisitar onde as propriedades de extensão Count/Length entram em jogo

Expressões de coleção.

  • A extensão Add funciona
  • A extensão GetEnumerator funciona para distribuiçã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áticos Create não devem ser considerados como um método de criação recomendado
  • As propriedades de contagem de extensão devem afetar as expressões de coleção?

params coleções

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

expressões de dicionário

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

extern

Esquema de nomenclatura/numeração para 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 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 considerar os métodos de marcador)

  1. ajustar a ferramenta
  2. usar algum TBD (esquema de nomenclatura baseado em conteúdo)
  3. permitir que o nome seja controlado por meio de alguma sintaxe

O novo método de extensão genérica "Cast" ainda não funciona 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 nos concentrando na transição sem aparência dos métodos de extensão clássicos, todos os argumentos de tipo devem ser fornecidos explicitamente.
Isto não consegue tratar um problema com o uso do método de extensão Cast no LINQ.

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

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

Devemos permitir o seguinte? (resposta: não, isso pode 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> { }

Nulidade

  • 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 esqueleto devem lançar NotSupportedException ou outra exceção padrão (atualmente fazemos throw null;)? (resposta: sim, LDM 2025-04-17)
  • Devemos aceitar mais de um parâmetro no método de marcador em metadados (caso novas versões adicionem mais informações)? (resposta: podemos permanecer estritos, LDM 2025-04-17)
  • O marcador de extensão ou os métodos de implementação 'falável' devem ser marcados com uma designação especial? (resposta: o método de 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 o atributo [Extension] aos getters e setters de implementação também. (resposta: não, LDM 2025-03-10)
  • Confirme se os tipos de extensão devem ser marcados com o nome especial e o compilador exigirá esse sinalizador nos metadados (essa é uma alteração significativa da versão prévia) (resposta: aprovado, LDM 2025-06-23)

Cenário de fábrica estático

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

Busca

  • Como lidar com as invocações de métodos de instância agora que temos nomes de implementação pronunciáveis? Preferimos o método esqueleto ao método de implementação correspondente.
  • Como resolver métodos de extensão estáticos? (resposta: assim como os métodos de extensão de instância, LDM 2025-03-03)
  • Como resolver propriedades? (respondida de forma genérica LDM 2025-03-03, mas precisa de acompanhamento para aprimoramento)
  • Regras de escopo e sombreamento para parâmetros de extensão e parâmetros de tipo (resposta: dentro do bloco de extensão, não é permitido o sombreamento, LDM 2025-03-10)
  • Como o ORPA deve ser aplicado a 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 envolvente, LDM 2025-04-17)
public static class Extensions
{
    extension(Type1)
    {
        [OverloadResolutionPriority(1)]
        public void Overload(...)
    }
    extension(Type2)
    {
        public void Overload(...)
    }
}
  • O ORPA deve se aplicar 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 reformular as clássicas regras de resolução de extensão? Nós?
    1. atualize o padrão para métodos de extensão clássicos e use-o para também descrever novos métodos de extensão,
    2. manter a linguagem existente para métodos de extensão clássicos, usá-lo para também descrever novos métodos de extensão, mas ter um desvio de especificação conhecido para ambos,
    3. manter o idioma existente para métodos de extensão clássicos, mas usar linguagem diferente para novos métodos de extensão e ter apenas um desvio de especificação conhecido para métodos de extensão clássicos?
  • Confirme se queremos impedir argumentos de tipo explícitos ao acessar uma propriedade (resposta: sem acesso à propriedade com argumentos de tipo explícitos, discutido no WG)
string s = "ran";
_ = s.P<object>; // error

static class E
{
    extension<T>(T t)
    {
        public int P => 0;
    }
}
  • Confirme se queremos que as regras de aprimoramento sejam aplicadas mesmo quando o receptor é um tipo (resposta: um parâmetro de extensão apenas de tipo deve ser considerado ao resolver membros de extensão 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;
    }
}
  • Confirme se estamos de acordo em aceitar uma ambiguidade quando os métodos e propriedades são aplicáveis (Resposta: devemos projetar uma proposta para melhorar o status quo, deixando fora do .NET 10, LDM 2025-06-23)
  • Confirme que não queremos alguma melhora em todos os membros antes de determinarmos o tipo de membro vencedor (resposta: deixando fora do .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;
    }
}
  • Temos um receptor implícito dentro de declarações de extensão? (resposta: não, foi discutido anteriormente no LDM)
static class E
{
    extension(object o)
    {
        public void M() 
        {
            M2();
        }
        public void M2() { }
    }
}
  • Devemos permitir a pesquisa no parâmetro de tipo? (discussão) (resposta: não, vamos aguardar os comentários, LDM 2025-04-16)

Acessibilidade

  • Qual é o significado da acessibilidade em 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 receptor 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 do parâmetro de tipo (inferência: todos os parâmetros de tipo devem aparecer no tipo do parâmetro de extensão) quando houver apenas métodos? (resposta: sim, LDM 2025-04-06) Isso permitiria a transferência de 100% de métodos de extensão clássicos.
    Se você tiver TResult M<TResult, TSource>(this TSource source), você poderia migrar para extension<TResult, TSource>(TSource source) { TResult M() ... }.

  • Confirme se acessadores de inicialização (init-only) devem ser permitidos em extensões (resposta: aceitável não permitir por enquanto, LDM 2025-04-17)

  • A única diferença no ref-ness do receptor deve ser permitida 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 reclamar 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 esqueletos 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 regras de conflito atuais são: 1. Verifique se há conflitos em extensões semelhantes usando regras de classe/struct, 2. verifique se há algum conflito entre métodos de implementação em declarações de várias extensões.

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

Documentos XML

  • O parâmetro de receptor tem paramref suporte em membros de extensão? Até mesmo quando estático? Como ele é codificado na saída? Provavelmente a maneira padrão <paramref name="..."/> funcionaria para um humano, mas há o risco de que algumas ferramentas existentes não fiquem satisfeitas por não encontrá-la entre os parâmetros da API. (resposta: sim, paramref para o parâmetro de extensão é permitido em membros de extensão, LDM 2025-05-05)
  • Devemos copiar os comentários da documentação para os métodos de implementação com nomes fáceis de pronunciar? (resposta: sem cópia, LDM 2025-05-05)
  • O elemento <param> correspondente ao parâmetro do receptor deve ser copiado do contêiner de extensão para métodos de instância? Qualquer coisa adicional 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 aparecerá em algum lugar?

CREF (Conselho Regional de Educação Física)

  • Confirmar sintaxe (resposta: proposta é boa, LDM 2025-06-09)
  • Deve ser possível fazer referência a um bloco de extensão (E.extension(int))? (resposta: não, LDM 2025-06-09)
  • Deve ser possível fazer referência a um membro usando uma sintaxe não qualificada: extension(int).Member? (resposta: sim, LDM 2025-06-09)
  • Devemos usar caracteres diferentes para um nome indescritível, para evitar o escape de XML? (resposta: adiar para WG, LDM 2025-06-09)
  • Confirme se está tudo bem que ambas as referências a skeleton 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 de extensão clássicos). (resposta: sim, LDM 2025-06-09)
  • Os nomes de metadados de extensão são problemáticos para documentos de controle de versão? (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, mas podemos abordá-lo de um ou alguns tipos de membros por vez. Com base em cenários conhecidos em nossas bibliotecas principais, devemos trabalhar na seguinte ordem:

  1. Propriedades e métodos (instância e estático)
  2. Operadores
  3. Indexadores (instância e estático, podem ser feitos de forma oportunista em um ponto anterior)
  4. Mais alguma coisa

Quanto queremos antecipar 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 que tivessem o mesmo nome e aridade. Não temos uma solução para representar isso em metadados.
  • A abordagem aproximada que discutimos para metadados:
    1. emitiríamos um tipo aninhado de esqueleto com parâmetros de tipo original e sem membros
    2. emitiríamos um tipo aninhado de implementação com parâmetros de tipo predefinidos da declaração de extensão e todas as implementações de membro à medida que aparecem na origem (referências de modulo a parâmetros de tipo)

Construtores

Os construtores geralmente são descritos como um membro de instância em C#, pois o 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, pois não há nenhum valor anterior a ser passado como um 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 porque não dependem de um nome de parâmetro receptor. Os corpos precisam criar e retornar explicitamente o resultado da construção. O membro em si ainda é declarado com sintaxe de construtor, mas não pode ter inicializadores this ou base e não depende do tipo de receptor com 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 enumeração:

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

Formulários mais curtos

O design proposto evita a repetição das especificações do receptor por cada membro, mas acaba resultando em membros da extensão sendo aninhados duas vezes 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 permitirmos a abreviação sintática desses casos.

Mesclar declarações de classe estática e extensão:

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

Isso acaba se parecendo mais com o que chamamos de abordagem "baseada em tipo", em que o próprio contêiner para membros da extensão é nomeado.

Mesclar 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 chamamos de abordagem "baseada em membros", em que cada membro de extensão contém sua própria especificação de receptor.