Retornos de covariante
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 aprender mais sobre o processo de adoção de especificações especiais de recursos no padrão da linguagem C# no artigo sobre as especificações .
Problema do especialista: https://github.com/dotnet/csharplang/issues/49
Resumo
Suporte tipos de retorno covariantes . Especificamente, permita a substituição de um método para declarar um tipo de retorno mais derivado do que o método que ele substitui e também permita a substituição de uma propriedade somente leitura para declarar um tipo mais derivado. Declarações de substituição que aparecem em tipos mais derivados devem fornecer um tipo de retorno pelo menos tão específico quanto os que aparecem nas substituições em seus tipos base. Os chamadores do método ou propriedade receberiam estaticamente o tipo de retorno mais refinado de uma invocação.
Motivação
É um padrão comum no código que diferentes nomes de métodos precisam ser inventados para contornar a restrição de linguagem de que as substituições devem retornar o mesmo tipo que o método substituído.
Isso seria útil no padrão de fábrica. Por exemplo, na base de código Roslyn, teríamos
class Compilation ...
{
public virtual Compilation WithOptions(Options options)...
}
class CSharpCompilation : Compilation
{
public override CSharpCompilation WithOptions(Options options)...
}
Projeto detalhado
Esta é uma especificação para tipos de retorno covariantes em C#. Nossa intenção é permitir a substituição de um método para retornar um tipo de retorno mais derivado do que o método que ele substitui e, da mesma forma, permitir a substituição de uma propriedade somente leitura para retornar um tipo de retorno mais derivado. Os chamadores do método ou propriedade receberiam estaticamente o tipo de retorno mais refinado de uma invocação, e as substituições que aparecem em tipos mais derivados seriam necessárias para fornecer um tipo de retorno pelo menos tão específico quanto aquele que aparece nas substituições em seus tipos base.
Substituição do método de classe
A restrição existente na substituição de métodos de classe (§15.6.5)
- O método de substituição e o método base substituído têm o mesmo tipo de retorno.
é modificado para
- O método de substituição deve ter um tipo de retorno que seja conversível por uma conversão de identidade ou (se o método tiver um retorno de valor - não um ref return consulte §13.1.0.5 conversão de referência implícita para o tipo de retorno do método base substituído.
E os seguintes requisitos adicionais são acrescentados a essa lista:
- O método de substituição deve ter um tipo de retorno que seja conversível por uma conversão de identidade ou (se o método tiver um retorno de valor - não um ref return, §13.1.0.5) conversão de referência implícita para o tipo de retorno de cada substituição do método base substituído que é declarado em um tipo base (direto ou indireto) do método de substituição.
- O tipo de retorno do método de substituição deve ser pelo menos tão acessível quanto o método de substituição (domínios de acessibilidade - §7.5.3).
Essa restrição permite que um método de substituição em uma classe private
tenha um tipo de retorno private
. No entanto, é necessário que um método de substituição public
em um tipo public
tenha um tipo de retorno public
.
Substituição de propriedade de classe e indexador
A restrição existente na substituição de propriedades de classe (§15.7.6)
Uma declaração de propriedade de substituição deve especificar exatamente os mesmos modificadores de acessibilidade e o mesmo nome da propriedade herdada, e deve haver uma conversão de identidade
entre o tipo da propriedade de substituição e o tipo da propriedade herdada. Se a propriedade herdada tiver apenas um único acessador (ou seja, se a propriedade herdada for somente leitura ou somente gravação), a propriedade de substituição deverá incluir somente esse acessador. Se a propriedade herdada incluir ambos os acessadores (ou seja, se a propriedade herdada for leitura-gravação), a propriedade de substituição poderá incluir um único acessador ou ambos os acessadores.
é modificado para
Uma declaração de propriedade de substituição deve especificar exatamente os mesmos modificadores de acessibilidade e o mesmo nome da propriedade herdada. Além disso, deve haver uma conversão de identidade ou, (se a propriedade herdada for somente leitura e tiver um retorno de valor - não um ref return§13.1.0.5) uma conversão implícita de referência do tipo da propriedade de substituição para o tipo da propriedade herdada. Se a propriedade herdada tiver apenas um único acessador (ou seja, se a propriedade herdada for somente leitura ou somente gravação), a propriedade de substituição deverá incluir somente esse acessador. Se a propriedade herdada incluir ambos os acessadores (ou seja, se a propriedade herdada for leitura-gravação), a propriedade de substituição poderá incluir um único acessador ou ambos os acessadores. O tipo da propriedade de substituição deve ser pelo menos tão acessível quanto a propriedade de substituição (domínios de acessibilidade – §7.5.3).
O restante do rascunho da especificação abaixo propõe uma extensão adicional para retornos covariantes de métodos de interface a ser considerada posteriormente.
Substituição de método, propriedade e indexador de interface
Ao adicionar aos tipos de membros permitidos em uma interface com o recurso DIM em C# 8.0, também oferecemos suporte adicional para membros override
, além de retornos covariantes. Eles seguem as regras dos membros override
conforme especificado para as classes, com as seguintes diferenças:
O texto a seguir nas classes é:
O método base substituído por uma declaração de substituição é conhecido como método substituído. Para um método de substituição
M
declarado em uma classeC
, o método base substituído é determinado examinando cada classe base deC
, começando com a classe base direta deC
e continuando com cada classe base direta sucessiva, até que em um determinado tipo de classe base seja localizado pelo menos um método acessível que tenha a mesma assinatura queM
após a substituição de argumentos de tipo.
é dada a especificação correspondente para interfaces:
O método base substituído por uma declaração de substituição é conhecido como método substituído. Para um método substituído
M
declarado em uma interfaceI
, o método base que está sendo substituído é determinado ao examinar cada interface base direta ou indireta deI
, coletando o conjunto de interfaces que declaram um método acessível com a mesma assinatura queM
após a substituição dos argumentos de tipo. Se esse conjunto de interfaces tiver um tipo mais derivado, para o qual existe uma identidade ou uma conversão de referência implícita de cada tipo neste conjunto, e esse tipo contiver uma declaração única desse método, então esse é o método base substituído.
De maneira semelhante, permitimos override
propriedades e indexadores em interfaces, conforme especificado para classes em §15.7.6 Acessadores virtuais, selados, substituídos e abstratos.
Pesquisa de nome
A pesquisa de nome na presença de declarações de override
de classe atualmente altera o resultado da pesquisa ao aplicar, ao membro encontrado, os detalhes da declaração de override
mais derivada na hierarquia de classe, começando do tipo do qualificador do identificador (ou this
quando não há qualificador). Por exemplo, em §12.6.2.2 Parâmetros correspondentes, temos
Para métodos virtuais e indexadores definidos em classes, a lista de parâmetros é escolhida na primeira declaração ou substituição do membro da função encontrado ao começar com o tipo estático do receptor e pesquisar nas classes base.
adicionamos o seguinte a isso
Para métodos virtuais e indexadores definidos em interfaces, a lista de parâmetros é escolhida na declaração ou substituição do membro da função encontrado no tipo mais derivado entre os tipos que contêm a declaração de substituição do membro da função. É um erro de compilação se não existir um único tipo desse tipo.
Para o tipo de resultado de um acesso de propriedade ou indexador, o texto existente
- Se
I
identificar uma propriedade de instância, o resultado será um acesso de propriedade com uma expressão de instância associada deE
e um tipo associado que é o tipo da propriedade. CasoT
seja um tipo de classe, o tipo associado é selecionado a partir da primeira declaração ou sobrescrição da propriedade encontrada ao iniciar comT
e ao procurar por suas classes base.
é aprimorado com
Se
T
for um tipo de interface, o tipo associado será escolhido na declaração ou substituição da propriedade encontrada na interface mais derivada deT
ou de suas interfaces base diretas ou indiretas. É um erro de compilação se não existir um único tipo desse tipo.
Uma alteração semelhante deve ser feita em §12.8.12.3 Acesso ao indexador
Em §12.8.10 Expressões de invocação aprimoramos o texto existente.
- Caso contrário, o resultado será um valor, associado ao tipo de retorno do método ou delegado. Se a invocação for de um método de instância e o receptor for de um tipo de classe
T
, o tipo associado será escolhido na primeira declaração ou substituição do método encontrado ao começar comT
e pesquisar por meio das classes base.
por
Se a invocação for de um método de instância e o receptor for de um tipo de interface
T
, o tipo associado será escolhido da declaração ou substituição do método encontrado na interface mais derivada entreT
e suas interfaces base diretas e indiretas. É um erro de compilação se não existir um único tipo desse tipo.
Implementações de interface implícitas
Esta seção da especificação
Para fins de mapeamento de interface, um membro de classe
A
corresponde a um membro de interfaceB
quando:
A
eB
são métodos e o nome, tipo e listas de parâmetros formais deA
eB
são idênticos.A
eB
são propriedades, o nome e o tipo deA
eB
são idênticos, eA
tem os mesmos acessadores queB
(A
é permitido ter acessadores adicionais se não for uma implementação de membro de interface explícita).A
eB
são eventos, e o nome e o tipo deA
eB
são idênticos.A
eB
são indexadores, o tipo e as listas de parâmetros formais deA
eB
são idênticos, eA
tem os mesmos acessadores queB
(A
é permitido ter acessadores adicionais se não for uma implementação de membro de interface explícita).
é modificado da seguinte maneira:
Para fins de mapeamento de interface, um membro de classe
A
corresponde a um membro de interfaceB
quando:
A
eB
são métodos, e o nome e as listas de parâmetros formais deA
eB
são idênticos e o tipo de retorno deA
é conversível para o tipo de retorno deB
por meio de uma identidade de conversão de referência implícita para o tipo de retorno deB
.A
andB
são propriedades, o nome deA
eB
são idênticos,A
tem os mesmos acessadores queB
(A
é permitido ter acessadores adicionais se não for uma implementação de membro de interface explícita) e o tipo deA
é conversível para o tipo de retorno deB
por meio de uma conversão de identidade ou, seA
é uma propriedade somente leitura, uma conversão de referência implícita.A
eB
são eventos, e o nome e o tipo deA
eB
são idênticos.A
andB
são indexadores, as listas de parâmetros formais deA
eB
são idênticos,A
tem os mesmos acessadores queB
(A
é permitido ter acessadores adicionais se não for uma implementação de membro de interface explícita) e o tipo deA
é conversível para o tipo de retorno deB
por meio de uma conversão de identidade ou, seA
é um indexador somente leitura, uma conversão de referência implícita.
Esta é tecnicamente uma alteração drástica, pois o programa abaixo imprime "C1.M" hoje, mas imprimiria "C2.M" na revisão proposta.
using System;
interface I1 { object M(); }
class C1 : I1 { public object M() { return "C1.M"; } }
class C2 : C1, I1 { public new string M() { return "C2.M"; } }
class Program
{
static void Main()
{
I1 i = new C2();
Console.WriteLine(i.M());
}
}
Devido a essa mudança significativa, podemos considerar a possibilidade de não oferecer suporte a tipos de retorno covariantes em implementações implícitas.
Restrições na implementação da interface
Precisaremos de uma regra que determine que uma implementação de interface explícita deve declarar um tipo de retorno não menos derivado que o tipo de retorno declarado em qualquer substituição em suas interfaces base.
Implicações da compatibilidade da API
A ser definido
Problemas Abertos
A especificação não diz como o chamador obtém o tipo de retorno mais refinado. Supostamente, isso seria feito de forma semelhante à maneira como os chamadores obtêm as especificações de parâmetro da substituição mais derivada.
Se tivermos as seguintes interfaces:
interface I1 { I1 M(); }
interface I2 { I2 M(); }
interface I3: I1, I2 { override I3 M(); }
Note que, em I3
, os métodos I1.M()
e I2.M()
foram "mesclados". Ao implementar I3
, é necessário implementá-los juntos.
Geralmente, exigimos uma implementação explícita para fazer referência ao método original. A questão é, em uma classe
class C : I1, I2, I3
{
C IN.M();
}
O que isso significa aqui? O que N deve ser?
Sugiro que permitamos a implementação de qualquer um I1.M
ou I2.M
(mas não ambos) e trate isso como uma implementação de ambos.
Desvantagens
- [ ] Toda alteração no idioma deve se justificar.
- [ ] Devemos garantir que o desempenho seja razoável, mesmo no caso de hierarquias de herança profundas
- [ ] Devemos garantir que os artefatos da estratégia de tradução não afetem a semântica da linguagem, mesmo ao consumir novo IL de compiladores antigos.
Alternativas
Poderíamos flexibilizar um pouco as regras de linguagem para permitir, na origem,
// Possible alternative. This was not implemented.
abstract class Cloneable
{
public abstract Cloneable Clone();
}
class Digit : Cloneable
{
public override Cloneable Clone()
{
return this.Clone();
}
public new Digit Clone() // Error: 'Digit' already defines a member called 'Clone' with the same parameter types
{
return this;
}
}
Perguntas não resolvidas
- [ ] Como as APIs que foram compiladas para usar esse recurso funcionarão em versões mais antigas da linguagem?
Reuniões de design
- alguma discussão em https://github.com/dotnet/roslyn/issues/357.
- https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-01-08.md
- Discussão offline para uma decisão de dar suporte à substituição de métodos de classe somente no C# 9.0.
C# feature specifications