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₀
eT₀
.- Se
E
tem um tipo, queS
seja esse tipo. - Se
S
ouT
são tipos de valor anuláveis, deixeSᵢ
eTᵢ
seus tipos subjacentes, caso contrário, deixeSᵢ
eTᵢ
serS
eT
, respectivamente. - Se
Sᵢ
ouTᵢ
são parâmetros de tipo, deixeS₀
eT₀
suas classes base efetivas, caso contrário, deixeS₀
eT₀
serSₓ
eTᵢ
, respectivamente.
- Se
- 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 deS0
(seS0
for uma classe ou struct), das classes base deS0
(seS0
for uma classe) e deT0
(seT0
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 emD
, que convertem de um tipo que abrangeS
para um tipo abrangido porT
. SeU
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₀
eT₀
.- Se
E
tem um tipo, queS
seja esse tipo. - Se
S
ouT
são tipos de valor anuláveis, deixeSᵢ
eTᵢ
seus tipos subjacentes, caso contrário, deixeSᵢ
eTᵢ
serS
eT
, respectivamente. - Se
Sᵢ
ouTᵢ
são parâmetros de tipo, deixeS₀
eT₀
suas classes base efetivas, caso contrário, deixeS₀
eT₀
serSₓ
eTᵢ
, respectivamente.
- Se
- 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 emS0
(seS0
for uma classe ou struct), nas classes base deS0
(seS0
for uma classe) eT0
(seT0
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 emD1
, que convertem de um tipo que englobaS
para um tipo englobado porT
. - Se
U1
não está vazio, entãoU
é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 efetivasSᵢ
e suas interfaces base (seSᵢ
for um parâmetro de tipo), e no conjunto de interfaces efetivasTᵢ
(seTᵢ
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 emD2
que convertem de um tipo que englobaS
para um tipo englobado porT
. - Se
U2
não está vazio, entãoU
éU2
- Encontre o conjunto de tipos,
- Encontre o conjunto de tipos,
- 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₀
eT₀
.- Se
E
tem um tipo, queS
seja esse tipo. - Se
S
ouT
são tipos de valor anuláveis, deixeSᵢ
eTᵢ
seus tipos subjacentes, caso contrário, deixeSᵢ
eTᵢ
serS
eT
, respectivamente. - Se
Sᵢ
ouTᵢ
são parâmetros de tipo, deixeS₀
eT₀
suas classes base efetivas, caso contrário, deixeS₀
eT₀
serSᵢ
eTᵢ
, respectivamente.
- Se
- 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 emS0
(seS0
for uma classe ou estrutura), nas classes base deS0
(seS0
for uma classe),T0
(seT0
for uma classe ou estrutura) e nas classes base deT0
(seT0
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 emD
que convertem de um tipo que engloba ou é englobado porS
para um tipo que engloba ou é englobado porT
. SeU
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₀
eT₀
.- Se
E
tem um tipo, queS
seja esse tipo. - Se
S
ouT
são tipos de valor anuláveis, deixeSᵢ
eTᵢ
seus tipos subjacentes, caso contrário, deixeSᵢ
eTᵢ
serS
eT
, respectivamente. - Se
Sᵢ
ouTᵢ
são parâmetros de tipo, deixeS₀
eT₀
suas classes base efetivas, caso contrário, deixeS₀
eT₀
serSᵢ
eTᵢ
, respectivamente.
- Se
- 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 emS0
(seS0
for uma classe ou struct), as classes base deS0
(seS0
for uma classe),T0
(seT0
for uma classe ou struct) e as classes base deT0
(seT0
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 emD1
que convertem de um tipo que engloba ou é englobado porS
para um tipo que engloba ou é englobado porT
. - Se
U1
não está vazio, entãoU
é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 efetivasSᵢ
e as suas interfaces base (seSᵢ
for um parâmetro de tipo), e no conjunto de interfaces efetivasTᵢ
e as suas interfaces base (seTᵢ
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 emD2
, que convertem de um tipo que engloba ou é englobado porS
para um tipo que engloba ou é englobado porT
. - Se
U2
não está vazio, entãoU
éU2
- Encontre o conjunto de tipos,
- Encontre o conjunto de tipos,
- 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 deSystem.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
- https://github.com/dotnet/csharplang/blob/master/meetings/2021/LDM-2021-02-08.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-04-05.md
- https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-06-29.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-01-24.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-16.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-28.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-04-06.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-06-06.md
C# feature specifications