Partilhar via


Membros abstratos estáticos em interfaces

Nota

Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ele 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 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 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 .

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 aplica 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 permitiria que os membros da interface estática fossem declarados virtuais.

Regras antes do C# 11

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

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

Proposta

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.

Proposta

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 servem a essa finalidade.

Implementações explícitas de membros da interface abstrata estática usam um nome qualificado junto 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 algum requisito envolvendo pelo menos um de seus operandos para serem 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 na qual 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 poderão 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.

Implementando 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

Discutimos 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). Aqui está a restrição, pois 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 queremos fazer o trabalho para dar suporte a ele em parâmetros de tipo. Se houver cenários lá fora, ouviremos sobre eles e poderemos optar por aceitar isso 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 anuláveis, deixe que Sᵢ e Tᵢ sejam seus tipos subjacentes, caso contrário, permita 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, permita 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 anuláveis, deixe que Sᵢ e Tᵢ sejam seus tipos subjacentes, caso contrário, permita 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, permita 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. Caso contrário
      • 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 anuláveis, deixe que Sᵢ e Tᵢ sejam seus tipos subjacentes, caso contrário, permita 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, permita 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 anuláveis, deixe que Sᵢ e Tᵢ sejam seus tipos subjacentes, caso contrário, permita 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, permita 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. Caso contrário
      • 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 exigiria o fluxo de um parâmetro de tipo oculto que representa o tipo "self" no qual o método estático atual realmente foi invocado. Isso parece complicado, caro e potencialmente 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. Isso não é intuitivo, portanto, ele é bloqueado 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 fazer 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 estivermos tentando corresponder 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 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 de 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 correspondida é um valor numérico, o valor de entrada é um tipo que herda de System.Numerics.INumberBase<T>e não há nenhuma conversão constante da expressão constante para o tipo do valor de entrada, a operação de correspondência de padrões é 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. É 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 herdando 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.

Inconvenientes

  • "resumo estático" é um novo conceito e será adicionado significativamente à carga conceitual de 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" diretamente e explicitamente exigindo a presença de operadores específicos em um parâmetro de tipo. As desvantagens disso são: - Isso 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