Compartilhar via


Membros abstratos estáticos em interfaces

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 pertinentes da reunião de design de idioma (LDM).

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

Problema do especialista: https://github.com/dotnet/csharplang/issues/4436

Resumo

Uma interface pode especificar membros estáticos abstratos que as classes e structs que a implementam devem fornecer uma implementação explícita ou implícita. Os membros podem ser acessados fora dos parâmetros de tipo que são restringidos pela interface.

Motivação

Atualmente, não há como abstrair sobre membros estáticos e gravar código generalizado que se aplique entre tipos que definem esses membros estáticos. Isso é um problema principalmente para tipos de membro que existem apenas em uma forma estática, por exemplo, os operadores.

Esse recurso permite algoritmos genéricos em tipos numéricos, representados por restrições de interface que especificam a presença de determinados operadores. Portanto, os algoritmos podem ser expressos em termos desses operadores:

// Interface specifies static properties and operators
interface IAddable<T> where T : IAddable<T>
{
    static abstract T Zero { get; }
    static abstract T operator +(T t1, T t2);
}

// Classes and structs (including built-ins) can implement interface
struct Int32 : …, IAddable<Int32>
{
    static Int32 IAddable.operator +(Int32 x, Int32 y) => x + y; // Explicit
    public static int Zero => 0;                          // Implicit
}

// Generic algorithms can use static members on T
public static T AddAll<T>(T[] ts) where T : IAddable<T>
{
    T result = T.Zero;                   // Call static operator
    foreach (T t in ts) { result += t; } // Use `+`
    return result;
}

// Generic method can be applied to built-in and user-defined types
int sixtyThree = AddAll(new [] { 1, 2, 4, 8, 16, 32 });

Sintaxe

Membros da interface

O recurso permite que os membros da interface estática sejam declarados como virtuais.

Regras antes do C# 11

Em versões anteriores ao C# 11, os membros da instância em interfaces são implicitamente abstratos (ou virtuais se tiverem uma implementação padrão), mas, como opção, podem ter um modificador abstract (ou virtual). Membros de instâncias não virtuais devem ser explicitamente marcados como sealed.

Os membros da interface estática hoje não são implicitamente virtuais e não permitem modificadores abstract, virtual ou sealed.

Proposal

Membros estáticos abstratos

Membros de interface estática que não sejam campos também têm permissão para ter o modificador abstract. Membros estáticos abstratos não podem ter um corpo (ou, no caso de propriedades, os acessores não podem ter um corpo).

interface I<T> where T : I<T>
{
    static abstract void M();
    static abstract T P { get; set; }
    static abstract event Action E;
    static abstract T operator +(T l, T r);
    static abstract bool operator ==(T l, T r);
    static abstract bool operator !=(T l, T r);
    static abstract implicit operator T(string s);
    static abstract explicit operator string(T t);
}
Membros estáticos virtuais

Membros de interface estática que não sejam campos também têm permissão para ter o modificador virtual. Membros virtuais estáticos devem ter um corpo.

interface I<T> where T : I<T>
{
    static virtual void M() {}
    static virtual T P { get; set; }
    static virtual event Action E;
    static virtual T operator +(T l, T r) { throw new NotImplementedException(); }
}
Membros estáticos explicitamente não virtuais

Para simetria com membros de instância não virtuais, aos membros estáticos (exceto campos) deve ser permitido um modificador sealed opcional, mesmo que eles não sejam virtuais por padrão.

interface I0
{
    static sealed void M() => Console.WriteLine("Default behavior");
    
    static sealed int f = 0;
    
    static sealed int P1 { get; set; }
    static sealed int P2 { get => f; set => f = value; }
    
    static sealed event Action E1;
    static sealed event Action E2 { add => E1 += value; remove => E1 -= value; }
    
    static sealed I0 operator +(I0 l, I0 r) => l;
}

Implementação de membros da interface

Regras de hoje

Classes e structs podem implementar membros abstratos de instância pertencentes a interfaces, seja de forma implícita ou explícita. Um membro de interface implementado implicitamente é uma declaração de membro normal (virtual ou não-virtual) da classe ou struct que por acaso também implementa o membro da interface. O membro pode até ser herdado de uma classe base e, portanto, nem mesmo estar presente na declaração de classe.

Um membro de interface explicitamente implementado usa um nome qualificado para identificar o membro da interface em questão. A implementação não é diretamente acessível como um membro na classe ou struct, mas apenas por meio da interface.

Proposal

Nenhuma sintaxe nova é necessária em classes e structs para facilitar a implementação implícita de membros estáticos da interface abstrata. As declarações de membro estático existentes atendem a essa finalidade.

Implementações explícitas de membros da interface abstrata estática usam um nome qualificado juntamente com o modificador static.

class C : I<C>
{
    string _s;
    public C(string s) => _s = s;
    static void I<C>.M() => Console.WriteLine("Implementation");
    static C I<C>.P { get; set; }
    static event Action I<C>.E // event declaration must use field accessor syntax
    {
        add { ... }
        remove { ... }
    }
    static C I<C>.operator +(C l, C r) => new C($"{l._s} {r._s}");
    static bool I<C>.operator ==(C l, C r) => l._s == r._s;
    static bool I<C>.operator !=(C l, C r) => l._s != r._s;
    static implicit I<C>.operator C(string s) => new C(s);
    static explicit I<C>.operator string(C c) => c._s;
}

Semântica

Restrições de operador

Hoje, todas as declarações de operador unário e binário têm requisitos que exigem que pelo menos um de seus operandos seja do tipo T ou T?, em que T é o tipo de instância do tipo delimitador.

Esses requisitos precisam ser flexibilizados para que um operando restritivo tenha permissão para ser de um parâmetro de tipo que seja considerado "o tipo de instância do tipo circundante".

Para que um parâmetro de tipo T possa contar como "o tipo de instância do tipo circundante", ele deve atender aos seguintes requisitos:

  • T é um parâmetro de tipo direto na interface em que a declaração do operador ocorre e
  • T é diretamente restringido pelo que a especificação chama de "tipo de instância" – ou seja, a interface circundante com seus próprios parâmetros de tipo usados como argumentos de tipo.

Operadores e conversões de igualdade

Declarações abstratas/virtuais de operadores == e !=, bem como declarações abstratas/virtuais de operadores de conversão implícita e explícita serão permitidas em interfaces. As interfaces derivadas também podem implementá-las.

Para operadores == e !=, pelo menos um tipo de parâmetro deve ser um parâmetro de tipo que conta como "o tipo de instância do tipo delimitador", conforme definido na seção anterior.

Implementar membros abstratos estáticos

As regras sobre quando uma declaração de membro estático em uma classe ou struct é considerada como implementação de um membro de interface estático abstrato e quais requisitos se aplicam nesse caso são as mesmas que para membros de instância.

TBD: Pode ser que haja regras adicionais ou diferentes necessárias aqui que ainda não tenhamos pensado.

Interfaces como argumentos de tipo

Abordamos a questão levantada por https://github.com/dotnet/csharplang/issues/5955 e decidimos adicionar uma restrição em relação ao uso de uma interface como um argumento de tipo (https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-28.md#type-hole-in-static-abstracts). Esta é a restrição, que foi proposta por https://github.com/dotnet/csharplang/issues/5955 e aprovada pelo LDM.

Uma interface que contém ou herda um membro estático abstrato/virtual que não tem a implementação mais específica na interface não pode ser usada como um argumento de tipo. Se todos os membros estáticos abstratos/virtuais tiverem a implementação mais específica, a interface poderá ser usada como um argumento de tipo.

Acessando membros da interface abstrata estática

Um membro de interface abstrata estático M pode ser acessado em um parâmetro de tipo T usando a expressão T.M quando T é restrito por uma interface I e M é um membro abstrato estático acessível de I.

T M<T>() where T : I<T>
{
    T.M();
    T t = T.P;
    T.E += () => { };
    return t + T.P;
}

Em tempo de execução, a implementação de membro real usada é aquela que existe no tipo real fornecido como um argumento de tipo.

C c = M<C>(); // The static members of C get called

Como as expressões de consulta são especificadas como uma reescrita sintática, o C# realmente permite que você use um tipo como a origem da consulta, desde que tenha membros estáticos para os operadores de consulta que você usa. Em outras palavras, se a sintaxe é aceita, ela é permitida. Achamos que esse comportamento não foi intencional ou importante no LINQ original e não pretendemos fazer com que ele ofereça suporte em parâmetros de tipo. Se surgirem cenários, ficaremos sabendo e tomaremos outra decisão mais tarde.

Segurança de variância §18.2.3.2

As regras de segurança de variação devem ser aplicadas a assinaturas de membros abstratos estáticos. A adição proposta em https://github.com/dotnet/csharplang/blob/main/proposals/variance-safety-for-static-interface-members.md#variance-safety deve ser ajustada de

Essas restrições não se aplicam a ocorrências de tipos dentro de declarações de membros estáticos.

até

Essas restrições não se aplicam a ocorrências de tipos dentro de declarações de membros estáticos não virtuais e não abstratos.

#10.5.4 Conversões implícitas definidas pelo usuário

Os itens a seguir

  • Determine os tipos S, S₀ e T₀.
    • Se E tiver um tipo, que S seja considerado desse tipo.
    • Se S ou T forem tipos de valor nulos, deixe que Sᵢ e Tᵢ sejam seus tipos subjacentes, caso contrário, deixe que Sᵢ e Tᵢ sejam S e T, respectivamente.
    • Se Sᵢ ou Tᵢ forem parâmetros de tipo, permita que S₀ e T₀ sejam suas classes base efetivas, caso contrário, deixe que S₀ e T₀ sejam Sₓ e Tᵢ, respectivamente.
  • Localize o conjunto de tipos, D, do qual os operadores de conversão definidos pelo usuário serão considerados. Esse conjunto consiste em S0 (se S0 for uma classe ou struct), nas classes base de S0 (se S0 for uma classe) e T0 (se T0 for uma classe ou struct).
  • Encontre o conjunto de operadores de conversão aplicáveis, definidos pelo usuário e elevados, U. Esse conjunto consiste nos operadores de conversão implícita definidos e levantados pelo usuário, declarados pelas classes ou structs em D, que convertem de um tipo que abrange S para um tipo abrangido por T. Se U estiver vazio, a conversão será indefinida e ocorrerá um erro de tempo de compilação.

são ajustados da seguinte maneira:

  • Determine os tipos S, S₀ e T₀.
    • Se E tiver um tipo, que S seja considerado desse tipo.
    • Se S ou T forem tipos de valor nulos, deixe que Sᵢ e Tᵢ sejam seus tipos subjacentes, caso contrário, deixe que Sᵢ e Tᵢ sejam S e T, respectivamente.
    • Se Sᵢ ou Tᵢ forem parâmetros de tipo, permita que S₀ e T₀ sejam suas classes base efetivas, caso contrário, deixe que S₀ e T₀ sejam Sₓ e Tᵢ, respectivamente.
  • Encontre o conjunto de operadores de conversão aplicáveis, definidos pelo usuário e elevados, U.
    • Localize o conjunto de tipos, D1, do qual os operadores de conversão definidos pelo usuário serão considerados. Esse conjunto consiste em S0 (se S0 for uma classe ou struct), nas classes base de S0 (se S0 for uma classe) e T0 (se T0 for uma classe ou struct).
    • Encontre o conjunto de operadores de conversão aplicáveis, definidos pelo usuário e elevados, U1. Esse conjunto consiste nos operadores de conversão implícita definidos e levantados pelo usuário, declarados pelas classes ou structs em D1, que convertem de um tipo que abrange S para um tipo abrangido por T.
    • Se U1 não estiver vazio, U será U1. Senão,
      • Localize o conjunto de tipos, D2, do qual os operadores de conversão definidos pelo usuário serão considerados. Esse conjunto consiste no conjunto de interface efetivo Sᵢ e suas interfaces base (se Sᵢ for um parâmetro de tipo) e no conjunto de interface efetivo Tᵢ (se Tᵢ for um parâmetro de tipo).
      • Encontre o conjunto de operadores de conversão aplicáveis, definidos pelo usuário e elevados, U2. Esse conjunto consiste nos operadores de conversão implícita definidos e levantados pelo usuário, declarados pelas interfaces em D2, que convertem de um tipo que abrange S para um tipo abrangido por T.
      • Se U2 não estiver vazio, U será U2
  • Se U estiver vazio, a conversão será indefinida e ocorrerá um erro de tempo de compilação.

§10.3.9 Conversões Explícitas Definidas Pelo Usuário

Os itens a seguir

  • Determine os tipos S, S₀ e T₀.
    • Se E tiver um tipo, que S seja considerado desse tipo.
    • Se S ou T forem tipos de valor nulos, deixe que Sᵢ e Tᵢ sejam seus tipos subjacentes, caso contrário, deixe que Sᵢ e Tᵢ sejam S e T, respectivamente.
    • Se Sᵢ ou Tᵢ forem parâmetros de tipo, permita que S₀ e T₀ sejam suas classes base efetivas, caso contrário, deixe que S₀ e T₀ sejam Sᵢ e Tᵢ, respectivamente.
  • Localize o conjunto de tipos, D, do qual os operadores de conversão definidos pelo usuário serão considerados. Esse conjunto consiste em S0 (se S0 for uma classe ou struct), as classes base de S0 (se S0 for uma classe), T0 (se T0 for uma classe ou struct) e as classes base de T0 (se T0 for uma classe).
  • Encontre o conjunto de operadores de conversão aplicáveis, definidos pelo usuário e elevados, U. Esse conjunto consiste de operadores de conversão implícita ou explícita definidos pelo usuário, declarados por classes ou structs em D, que convertem de um tipo que abrange ou é abrangido por S para um tipo que abrange ou é abrangido por T. Se U estiver vazio, a conversão será indefinida e ocorrerá um erro de tempo de compilação.

são ajustados da seguinte maneira:

  • Determine os tipos S, S₀ e T₀.
    • Se E tiver um tipo, que S seja considerado desse tipo.
    • Se S ou T forem tipos de valor nulos, deixe que Sᵢ e Tᵢ sejam seus tipos subjacentes, caso contrário, deixe que Sᵢ e Tᵢ sejam S e T, respectivamente.
    • Se Sᵢ ou Tᵢ forem parâmetros de tipo, permita que S₀ e T₀ sejam suas classes base efetivas, caso contrário, deixe que S₀ e T₀ sejam Sᵢ e Tᵢ, respectivamente.
  • Encontre o conjunto de operadores de conversão aplicáveis, definidos pelo usuário e elevados, U.
    • Localize o conjunto de tipos, D1, do qual os operadores de conversão definidos pelo usuário serão considerados. Esse conjunto consiste em S0 (se S0 for uma classe ou struct), as classes base de S0 (se S0 for uma classe), T0 (se T0 for uma classe ou struct) e as classes base de T0 (se T0 for uma classe).
    • Encontre o conjunto de operadores de conversão aplicáveis, definidos pelo usuário e elevados, U1. Esse conjunto consiste de operadores de conversão implícita ou explícita definidos pelo usuário, declarados por classes ou structs em D1, que convertem de um tipo que abrange ou é abrangido por S para um tipo que abrange ou é abrangido por T.
    • Se U1 não estiver vazio, U será U1. Senão,
      • Localize o conjunto de tipos, D2, do qual os operadores de conversão definidos pelo usuário serão considerados. Esse conjunto consiste no conjunto efetivo de interfaces Sᵢ e suas interfaces base (se Sᵢ for um parâmetro de tipo), e no conjunto efetivo de interfaces Tᵢ e suas interfaces base (se Tᵢ for um parâmetro de tipo).
      • Encontre o conjunto de operadores de conversão aplicáveis, definidos pelo usuário e elevados, U2. Esse conjunto consiste de operadores de conversão implícita ou explícita definidos pelo usuário, declarados por interfaces em D2 que convertem de um tipo que abrange ou é abrangido por S para um tipo que abrange ou é abrangido por T.
      • Se U2 não estiver vazio, U será U2
  • Se U estiver vazio, a conversão será indefinida e ocorrerá um erro de tempo de compilação.

Implementações padrão

Um recurso de adicional para essa proposta é permitir que membros virtuais estáticos em interfaces tenham implementações padrão, assim como os membros virtuais/abstratos da instância têm.

Uma complicação aqui é que as implementações padrão precisam chamar outros membros virtuais estáticos de forma "virtual". Permitir que membros virtuais estáticos sejam chamados diretamente na interface exige o fluxo de um parâmetro de tipo oculto que representa o tipo "self" no qual o método estático atual foi realmente invocado. Parece complicado, caro e possivelmente confuso.

Discutimos uma versão mais simples que mantém as limitações da proposta atual de que os membros virtuais estáticos podem apenas ser invocados em parâmetros de tipo. Como as interfaces com membros virtuais estáticos geralmente terão um parâmetro de tipo explícito que representa um tipo "self", isso não seria uma grande perda: outros membros virtuais estáticos poderiam ser chamados apenas nesse tipo "self". Esta versão é muito mais simples e parece bastante factível.

No https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-01-24.md#default-implementations-of-abstract-statics decidimos dar suporte às implementações padrão de membros estáticos seguindo e expandindo as regras estabelecidas em https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/default-interface-methods.md adequadamente.

Correspondência de padrões

Dado o código a seguir, um usuário pode razoavelmente esperar que ele imprima "True" (como aconteceria se o padrão constante fosse escrito em linha):

M(1.0);

static void M<T>(T t) where T : INumberBase<T>
{
    Console.WriteLine(t is 1); // Error. Cannot use a numeric constant
    Console.WriteLine((t is int i) && (i is 1)); 
}

No entanto, como o tipo de entrada do padrão não é double, o padrão constante 1 fará primeiro a verificação do T de entrada em relação a int. Não é intuitiva, portanto, ela é bloqueada até que uma versão futura do C# adicione melhor tratamento para correspondência numérica em relação aos tipos derivados de INumberBase<T>. Para isso, vamos dizer que reconheceremos explicitamente INumberBase<T> como o tipo do qual todos os "números" derivarão e bloquearemos o padrão se tentarmos associar a um padrão de constante numérica em relação a um tipo de número no qual não podemos representar o padrão (ou seja, um parâmetro de tipo restrito a INumberBase<T> ou um tipo de número definido pelo usuário que herda de INumberBase<T>).

Formalmente, adicionamos uma exceção à definição de compatível com padrões para padrões constantes:

Um padrão de constante testa o valor de uma expressão em relação a um valor constante. A constante pode ser qualquer expressão constante, como um literal, o nome de uma variável const declarada ou uma constante de enumeração. Quando o valor de entrada não é um tipo aberto, a expressão constante é convertida implicitamente no tipo da expressão correspondente; se o tipo do valor de entrada não for compatível com padrões com o tipo da expressão constante, a operação de correspondência de padrões será um erro. Se a expressão constante que está sendo associada for um valor numérico, o valor de entrada for um tipo que herda de System.Numerics.INumberBase<T> e não houver nenhuma conversão constante da expressão constante para o tipo do valor de entrada, a operação de correspondência de padrões será um erro.

Também adicionamos uma exceção semelhante para padrões relacionais:

Quando a entrada é de um tipo para o qual está definido um operador relacional binário interno adequado, que é aplicável com a entrada como operando esquerdo e a constante fornecida como operando direito, a avaliação desse operador é tomada como o significado do padrão relacional. Caso contrário, converteremos a entrada para o tipo da expressão usando uma conversão anulável ou de unboxing explícita. Será um erro de tempo de compilação se não houver essa conversão. Será um erro em tempo de compilação se o tipo de entrada for um parâmetro de tipo restrito ou um tipo que herda de System.Numerics.INumberBase<T> e o tipo de entrada não tiver nenhum operador relacional binário interno adequado definido. O padrão é considerado não correspondente se a conversão falhar. Se a conversão for bem-sucedida, o resultado da operação de correspondência de padrões será o resultado da avaliação da expressão e OP v, em que e é a entrada convertida, OP é o operador relacional e v é a expressão constante.

Desvantagens

  • "abstrato estático" é um novo conceito e será adicionado significativamente à carga conceitual do C#.
  • Não é um recurso barato para desenvolver. Devemos ter certeza de que vale a pena.

Alternativas

Restrições estruturais

Uma abordagem alternativa seria ter "restrições estruturais" direta e explicitamente exigindo a presença de operadores específicos em um parâmetro de tipo. As desvantagens disso são: - Teria que ser escrito todas as vezes. Ter uma restrição nomeada parece melhor. - Esse é um tipo totalmente novo de restrição, enquanto o recurso proposto utiliza o conceito existente de restrições de interface. - Ele só funcionaria para operadores, mas não funcionaria (facilmente) para outros tipos de membros estáticos.

Perguntas não resolvidas

Interfaces abstratas estáticas e classes estáticas

Consulte https://github.com/dotnet/csharplang/issues/5783 e https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-16.md#static-abstract-interfaces-and-static-classes para obter mais informações.

Reuniões de design