Partilhar 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. Ele inclui mudanças de especificação propostas, juntamente com as informações necessárias durante o design e desenvolvimento do recurso. Estes artigos são publicados até que as alterações de especificações propostas sejam finalizadas e incorporadas na especificação ECMA atual.

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

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

Questão campeã: https://github.com/dotnet/csharplang/issues/4436

Resumo

Uma interface tem permissão para especificar membros estáticos abstratos dos quais as classes e estruturas de implementação são então necessárias para 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á nenhuma maneira de abstrair sobre membros estáticos e escrever código generalizado que se aplica a tipos que definem esses membros estáticos. Isto é particularmente problemático para tipos de membros que só existir de forma estática, nomeadamente operadores.

Esse recurso permite algoritmos genéricos sobre tipos numéricos, representados por restrições de interface que especificam a presença de determinados operadores. Os algoritmos podem, portanto, ser expressos em termos de tais 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 nas interfaces são implicitamente abstratos (ou virtuais se tiverem uma implementação padrão), mas podem opcionalmente ter um modificador abstract (ou virtual). Os membros de instância não virtuais devem ser explicitamente marcados como sealed.

Hoje em dia, os membros da interface estática são implicitamente não virtuais e não permitem modificadores abstract, virtual ou sealed.

Proposta

Membros estáticos abstratos

Os membros da interface estática que não sejam campos também podem ter o modificador abstract. Membros estáticos abstratos não podem ter um corpo (ou, no caso de propriedades, os acessadores 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

Os membros da interface estática que não sejam campos também podem ter o modificador virtual. Os membros estáticos virtuais são obrigados a 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 manter a simetria com os membros de instância não virtuais, os membros estáticos (exceto campos) devem poder ter um modificador opcional sealed, mesmo sendo não 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 hoje em vigor

Classes e structs podem implementar, de forma implícita ou explícita, membros de instância abstratos de interfaces. Um membro da interface implementado implicitamente é uma declaração normal de membro (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 da 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 através da interface.

Proposta

Nenhuma sintaxe nova é necessária em classes e estruturas para facilitar a implementação implícita de membros da interface abstrata estática. As declarações de membros estáticos existentes servem esse propósito.

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 do operador

Hoje todas as declarações de operadores unários e binários têm algum requisito que envolve pelo menos um dos seus operandos ser do tipo T ou T?, onde T é o tipo de instância do tipo envolvente.

Esses requisitos precisam ser flexibilizados para que um operando restrito possa ser de um parâmetro de tipo que conta como "o tipo de instância do tipo envolvente".

Para que um parâmetro de tipo T seja considerado como "o tipo de instância do tipo envolvente", ele deve atender aos seguintes requisitos:

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

Operadores de igualdade e conversões

Serão permitidas declarações abstratas/virtuais de operadores de == e !=, bem como declarações abstratas/virtuais de operadores de conversão implícitos e explícitos nas interfaces. Interfaces derivadas também poderão implementá-los.

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

Implementando membros abstratos estáticos

As regras para quando uma declaração de membro estático numa classe ou struct é considerada como implementando um membro de interface abstrata estática, e para quais requisitos se aplicam quando isso acontece, são as mesmas dos membros de instância.

TBD: Pode haver as regras adicionais ou diferentes necessárias aqui que ainda não considerámos.

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 torno do 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). Eis a restrição tal como foi proposta pelo https://github.com/dotnet/csharplang/issues/5955 e aprovada pelo LDM.

Uma interface que contém ou herda um membro abstrato/virtual estático 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 abstratos/virtuais estáticos tiverem implementação mais específica, a interface pode ser usada como um argumento de tipo.

Acessando membros da interface abstrata estática

Um membro abstrato estático M pode ser acedido 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;
}

No 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 fonte da consulta, desde que tenha membros estáticos para os operadores de consulta que você usa! Em outras palavras, se a sintaxe se encaixar, permitimo-la! Achamos que esse comportamento não foi intencional ou importante no LINQ original, e não queremos fazer o trabalho para apoiá-lo em parâmetros de tipo. Se houver cenários à nossa volta, seremos informados deles e poderemos eventualmente optar por abraçá-los.

Segurança da variância §18.2.3.2

As regras de segurança de variância devem aplicar-se às assinaturas de membros abstratos estáticos. O aditamento proposto em https://github.com/dotnet/csharplang/blob/main/proposals/variance-safety-for-static-interface-members.md#variance-safety deve ser ajustado a partir de

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

Para

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

§10.5.4 conversões implícitas definidas pelo usuário

Os seguintes marcadores

  • Determine os tipos S, S₀ e T₀.
    • Se E tem um tipo, que S seja esse tipo.
    • Se S ou T são tipos de valor anuláveis, deixe Sᵢ e Tᵢ seus tipos subjacentes, caso contrário, deixe Sᵢ e Tᵢ ser S e T, respectivamente.
    • Se Sᵢ ou Tᵢ são parâmetros de tipo, deixe S₀ e T₀ suas classes base efetivas, caso contrário, deixe S₀ e T₀ ser Sₓ e Tᵢ, respectivamente.
  • Encontre o conjunto de tipos, D, a partir dos quais os operadores de conversão definidos pelo usuário serão considerados. Esse conjunto compõe-se de S0 (se S0 for uma classe ou struct), das classes base de S0 (se S0 for uma classe) e de T0 (se T0 for uma classe ou struct).
  • Encontre o conjunto de operadores de conversão definidos pelo usuário e levantados aplicáveis, U. Este conjunto consiste nos operadores de conversão implícita definidos pelo utilizador e elevados declarados pelas classes ou estruturas 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 em tempo de compilação.

são ajustados do seguinte modo:

  • Determine os tipos S, S₀ e T₀.
    • Se E tem um tipo, que S seja esse tipo.
    • Se S ou T são tipos de valor anuláveis, deixe Sᵢ e Tᵢ seus tipos subjacentes, caso contrário, deixe Sᵢ e Tᵢ ser S e T, respectivamente.
    • Se Sᵢ ou Tᵢ são parâmetros de tipo, deixe S₀ e T₀ suas classes base efetivas, caso contrário, deixe S₀ e T₀ ser Sₓ e Tᵢ, respectivamente.
  • Encontre o conjunto de operadores de conversão definidos pelo utilizador e elevados aplicáveis, U.
    • Encontre o conjunto de tipos, D1, a partir dos quais 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 definidos pelo usuário e elevados que são aplicáveis, U1. Esse conjunto consiste dos operadores de conversão implícitos definidos e elevados pelo usuário, declarados pelas classes ou estruturas em D1, que convertem de um tipo que engloba S para um tipo englobado por T.
    • Se U1 não está vazio, então U é U1. Caso contrário,
      • Encontre o conjunto de tipos, D2, a partir dos quais os operadores de conversão definidos pelo usuário serão considerados. Este conjunto consiste no conjunto de interfaces efetivas Sᵢ e suas interfaces base (se Sᵢ for um parâmetro de tipo), e no conjunto de interfaces efetivas Tᵢ (se Tᵢ for um parâmetro de tipo).
      • Encontre o conjunto de operadores de conversão definidos pelo utilizador e elevados aplicáveis, U2. Este conjunto consiste nos operadores de conversão implícitos definidos pelo usuário e levantados declarados pelas interfaces em D2 que convertem de um tipo que engloba S para um tipo englobado por T.
      • Se U2 não está vazio, então U é U2
  • Se U estiver vazio, a conversão será indefinida e ocorrerá um erro em tempo de compilação.

§10.3.9 Conversões explícitas definidas pelo usuário

Os pontos a seguir

  • Determine os tipos S, S₀ e T₀.
    • Se E tem um tipo, que S seja esse tipo.
    • Se S ou T são tipos de valor anuláveis, deixe Sᵢ e Tᵢ seus tipos subjacentes, caso contrário, deixe Sᵢ e Tᵢ ser S e T, respectivamente.
    • Se Sᵢ ou Tᵢ são parâmetros de tipo, deixe S₀ e T₀ suas classes base efetivas, caso contrário, deixe S₀ e T₀ ser Sᵢ e Tᵢ, respectivamente.
  • Encontre o conjunto de tipos, D, a partir dos quais os operadores de conversão definidos pelo usuário serão considerados. Esse conjunto consiste em S0 (se S0 for uma classe ou estrutura), nas classes base de S0 (se S0 for uma classe), T0 (se T0 for uma classe ou estrutura) e nas classes base de T0 (se T0 for uma classe).
  • Encontre o conjunto de operadores de conversão aplicáveis definidos pelo usuário e promovidos, U. Esse conjunto consiste nos operadores de conversão implícitos ou explícitos definidos pelo usuário e declarados pelas classes ou estruturas em D que convertem de um tipo que engloba ou é englobado por S para um tipo que engloba ou é englobado por T. Se U estiver vazio, a conversão será indefinida e ocorrerá um erro em tempo de compilação.

são ajustados do seguinte modo:

  • Determine os tipos S, S₀ e T₀.
    • Se E tem um tipo, que S seja esse tipo.
    • Se S ou T são tipos de valor anuláveis, deixe Sᵢ e Tᵢ seus tipos subjacentes, caso contrário, deixe Sᵢ e Tᵢ ser S e T, respectivamente.
    • Se Sᵢ ou Tᵢ são parâmetros de tipo, deixe S₀ e T₀ suas classes base efetivas, caso contrário, deixe S₀ e T₀ ser Sᵢ e Tᵢ, respectivamente.
  • Encontre o conjunto de operadores de conversão definidos pelo usuário e liftados aplicáveis, U.
    • Encontre o conjunto de tipos, D1, a partir dos quais 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 definidos pelo usuário e levantados aplicáveis, U1. Esse conjunto consiste nos operadores de conversão implícitos ou explícitos definidos pelo usuário e declarados pelas classes ou estruturas em D1 que convertem de um tipo que engloba ou é englobado por S para um tipo que engloba ou é englobado por T.
    • Se U1 não está vazio, então U é U1. Caso contrário,
      • Encontre o conjunto de tipos, D2, a partir dos quais os operadores de conversão definidos pelo usuário serão considerados. Este conjunto consiste no conjunto de interfaces efetivas Sᵢ e as suas interfaces base (se Sᵢ for um parâmetro de tipo), e no conjunto de interfaces efetivas Tᵢ e as suas interfaces base (se Tᵢ for um parâmetro de tipo).
      • Encontre o conjunto aplicável de operadores de conversão definidos pelo usuário e promovidos, U2. Este conjunto consiste nos operadores de conversão implícitos ou explícitos definidos pelo utilizador e promovidos, declarados pelas interfaces em D2, que convertem de um tipo que engloba ou é englobado por S para um tipo que engloba ou é englobado por T.
      • Se U2 não está vazio, então U é U2
  • Se U estiver vazio, a conversão será indefinida e ocorrerá um erro em tempo de compilação.

Implementações padrão

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

Uma complicação aqui é que as implementações padrão pretendem chamar outros membros virtuais estáticos de forma virtual. Permitir que membros virtuais estáticos sejam chamados diretamente na interface exigiria propagar um parâmetro de tipo oculto que representasse o tipo "self" no qual o método estático atual foi de fato invocado. Isto parece complicado, dispendioso 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 só podem 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 representando um tipo "self", isso não seria uma grande perda: outros membros virtuais estáticos poderiam simplesmente ser chamados nesse tipo de self. Esta versão é muito mais simples, e parece bastante factível.

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

Correspondência de padrões

Dado o código a seguir, um usuário pode razoavelmente esperar que ele imprima "True" (como faria 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 primeiro verificará o T de entrada contra int. Isso não é intuitivo, por isso é bloqueado até que uma versão futura do C# adicione um melhor tratamento para correspondência numérica com tipos derivados de INumberBase<T>. Para fazer isso, diremos 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 um padrão constante numérico com 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 const declarada ou uma constante de enumeração. Quando o valor de entrada não é um tipo aberto, a expressão constante é implicitamente convertida para o tipo da expressão correspondente; Se o tipo do valor de entrada não for compatível com o padrão com o tipo da expressão constante, a operação de correspondência de padrão será um erro. Se a expressão constante que está sendo correspondida for um valor numérico, o valor de entrada for um tipo que herda de System.Numerics.INumberBase<T>e não houver 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 é um tipo para o qual um operador relacional binário embutido adequado é definido que é aplicável com a entrada como seu operando esquerdo e a constante dada como seu operando direito, a avaliação desse operador é tomada como o significado do padrão relacional. Caso contrário, convertemos a entrada para o tipo da expressão usando uma conversão explícita que pode ser anulável ou de desencaixotamento. É um erro em tempo de compilação se não existir tal conversão. É um erro em tempo de compilação se o tipo de entrada é um parâmetro de tipo restrito ou um tipo herdado de System.Numerics.INumberBase<T> e o tipo de entrada não tem nenhum operador relacional binário interno adequado definido. Considera-se que o padrão não corresponde caso a conversão falhe. Se a conversão for bem-sucedida, o resultado da operação de correspondência de padrões é o resultado da avaliação da expressão e OP v onde e é a entrada convertida, OP é o operador relacional e v é a expressão constante.

Desvantagens

  • "abstrato estático" é um novo conceito e aumentará significativamente a carga conceitual do C#.
  • Não é uma funcionalidade barata para desenvolver. Devemos certificar-nos de que vale a pena.

Alternativas

Condicionalismos estruturais

Uma abordagem alternativa consistiria em impor "condicionalismos estruturais" direta e explicitamente à presença de operadores específicos num parâmetro de tipo. As desvantagens disso são: - Isso teria que ser escrito sempre. Ter uma restrição nomeada parece melhor. - Este é um novo tipo de restrição, enquanto o recurso proposto utiliza o conceito existente de restrições de interface. - Só funcionaria para operadores, não (facilmente) outros tipos de membros estáticos.

Questões por resolver

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