Retornos covariantes
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 .
Resumo
Suporte tipos de retorno covariante. Especificamente, permitir a substituição de um método para declarar um tipo de retorno mais derivado do que aquele do método que está a substituir e, da mesma forma, permitir a substituição de uma propriedade de leitura única para declarar um tipo mais derivado. As declarações de substituição que aparecem em tipos mais derivados seriam necessárias para fornecer um tipo de retorno pelo menos tão específico quanto o que aparece em 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 nomes de métodos diferentes têm que ser inventados para contornar a restrição de linguagem de que sobrecargas devem retornar o mesmo tipo que o método sobrecarregado.
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 especificação é para tipos de retorno covariante 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 invocadores do método ou propriedade receberiam de forma estática o tipo de retorno mais refinado de uma chamada, e substituições que aparecem em tipos mais derivados seriam obrigadas a fornecer um tipo de retorno pelo menos tão específico quanto o que aparece em substituições em seus tipos base.
Substituição do método de classe
A restrição existente nos métodos de substituição 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 retorno ref consulte §13.1.0.5 conversão de referência implícita para o tipo de retorno do método de base substituído.
E os seguintes requisitos adicionais são anexados 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 retorno de referência , §13.1.0.5) conversão de referência implícita para o tipo de retorno de cada substituição do método de base substituído que é declarado em um tipo de 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, ele requer um método de substituição de public
em um tipo de public
para ter um tipo de retorno public
.
Propriedade de classe e substituição do indexador
A restrição existente nas propriedades de substituição de classe (§15.7.6)
Uma declaração de propriedade prevalecente deve especificar exatamente os mesmos modificadores de acessibilidade e nome que os bens herdados, e deve haver uma conversão de identidade
entre o tipo de propriedade prevalecente e a propriedade herdada. Se a propriedade herdada tiver apenas um único acessador (ou seja, se a propriedade herdada for somente leitura ou gravação), a propriedade prevalecente deve incluir apenas esse acessador. Se a propriedade herdada incluir ambos os acessores (ou seja, se a propriedade herdada for de leitura e escrita), a propriedade substituinte pode incluir um único acessor ou ambos os acessores.
é modificado para
Uma declaração de propriedade prevalecente deve especificar exatamente os mesmos modificadores de acessibilidade e nome que a propriedade herdada, e deve haver uma conversão de identidade ou (se a propriedade herdada for somente leitura e tiver um retorno de valor - não um retorno de referência §13.1.0.5) conversão de referência implícita do tipo de propriedade prevalecente para o tipo de propriedade herdada. Se a propriedade herdada tiver apenas um único acessador (ou seja, se a propriedade herdada for somente leitura ou gravação), a propriedade prevalecente deve incluir apenas esse acessador. Se a propriedade herdada incluir ambos os acessores (ou seja, se a propriedade herdada for de leitura e escrita), a propriedade sobreposta pode incluir um único acessor ou ambos os acessores. O tipo de propriedade prevalecente deve ser pelo menos tão acessível quanto a propriedade prevalecente (Domínios de acessibilidade - §7.5.3).
O restante do projeto de especificação abaixo propõe uma extensão adicional para retornos covariantes de métodos de interface a serem considerados posteriormente.
Método de interface, propriedade e substituição do indexador
Adicionando aos tipos de membros que são permitidos em uma interface com a adição do recurso DIM no C# 8.0, adicionamos ainda mais suporte para membros override
juntamente com retornos covariantes. Estes obedecem às regras dos membros de override
conforme especificado para as classes, com as seguintes diferenças:
O seguinte texto nas aulas:
O método base que é substituído por uma declaração de substituição é conhecido como o método substituído. Para um método de substituição
M
declarado em uma classeC
, o método de base substituído é determinado examinando cada classe de 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 esteja localizado pelo menos um método acessível que tenha a mesma assinatura queM
após a substituição de argumentos de tipo.
é fornecida a especificação correspondente para as interfaces:
O método substituído por uma declaração de substituição é conhecido como o método base substituído. Para um método de sobreposição
M
declarado numa interfaceI
, determina-se o método base que foi substituído examinando cada interface de base direta ou indireta deI
, e recolhendo o conjunto de interfaces que declaram um método acessível que possui a mesma assinatura queM
, após a substituição dos argumentos de tipo. Se esse conjunto de interfaces tiver um tipo mais derivadoao qual é possível uma conversão de identidade ou referência implícita de todos os tipos nesse conjunto, e esse tipo contém uma declaração de método única desse tipo, então esse é o método base substituído.
Da mesma forma, permitimos propriedades override
e indexadores em interfaces, conforme especificado para classes no §15.7.6 Acessores virtuais, lacrados, de substituição e abstratos.
Pesquisa de nome
A pesquisa de nomes na presença de declarações de override
de classe atualmente modifica o resultado da pesquisa de nome impondo aos membros encontrados detalhes da declaração de override
mais derivada na hierarquia de classe a partir do tipo de qualificador do identificador (ou this
quando não há qualificador). Por exemplo, no §12.6.2.2 Parâmetros Correspondentes temos
Para métodos virtuais e indexadores definidos em classes, a lista de parâmetros é escolhida a partir da primeira declaração ou substituição do membro da função encontrado ao começar com o tipo estático do recetor e pesquisar através de suas classes base.
a isto acrescentamos
Para métodos virtuais e indexadores definidos em interfaces, a lista de parâmetros é escolhida a partir da 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 tempo de compilação se não existir tal tipo único.
Para o tipo de resultado de uma propriedade ou de um acesso indexador, o texto existente.
- Se
I
identificar uma propriedade de instância, o resultado será um acesso à propriedade com uma expressão de instância associada deE
e um tipo associado que é o tipo da propriedade. SeT
for um tipo de classe, o tipo associado será escolhido a partir da primeira declaração ou substituição da propriedade encontrada ao iniciar comT
e pesquisar em suas classes base.
é complementada com
Se
T
for um tipo de interface, o tipo associado será selecionado a partir da declaração ou substituição da propriedade encontrada naT
mais derivada ou nas suas interfaces base diretas ou indiretas. É um erro em tempo de compilação se não existir um tipo único.
Uma alteração semelhante deve ser feita em §12.8.12.3 Acesso ao indexador
Em §12.8.10 Expressões de invocação aumentamos o texto existente
- Caso contrário, o resultado é um valor, com um tipo associado do tipo de retorno do método ou delegado. Se a invocação for de um método de instância e o recetor for de um tipo de classe
T
, o tipo associado será escolhido a partir da primeira declaração ou sobrescrição do método encontrado ao iniciar comT
e pesquisar nas suas classes base.
com
Se a invocação for de um método de instância e o recetor for de um tipo de interface
T
, o tipo associado será escolhido a partir 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 tempo de compilação se não existir um tipo único tal.
Implementações de Interface Implícita
Esta secção da especificação
Para fins de mapeamento de interface, um membro de classe
A
corresponde a um membro da interfaceB
quando:
A
eB
são métodos, e as listas de nome, tipo e parâmetros formais deA
eB
são idênticas.A
eB
são propriedades, o nome e o tipo deA
eB
são idênticos, eA
tem os mesmos acessadores queB
(A
tem permissão para ter acessadores adicionais se não for uma implementação explícita de membro da interface).A
eB
são eventos, e o nome e o tipo deA
eB
são idênticos.A
eB
são indexadores, as listas de tipos e parâmetros formais deA
eB
são idênticas, eA
tem os mesmos acessadores queB
(A
tem permissão para ter acessadores adicionais se não for uma implementação explícita de membro da interface).
é modificada da seguinte forma:
Para fins de mapeamento de interface, um membro de classe
A
corresponde a um membro da interfaceB
quando:
A
eB
são métodos, e as listas de nomes e parâmetros formais deA
eB
são idênticas, e o tipo de retorno deA
é conversível para o tipo de retorno deB
através de uma identidade de conversão de referência implícita para o tipo de retorno deB
.A
eB
são propriedades, o nome deA
eB
são idênticos,A
tem os mesmos acessadores queB
(A
tem permissão para ter acessadores adicionais se não for uma implementação explícita de membro da interface), e o tipo deA
é conversível para o tipo de retorno deB
por meio de uma conversão de identidade ou, seA
for 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
eB
são indexadores, as listas de parâmetros formais deA
eB
são idênticas,A
tem os mesmos acessadores queB
(A
tem permissão para ter acessadores adicionais se não for uma implementação explícita de membro da interface), e o tipo deA
é conversível para o tipo de retorno deB
através de uma conversão de identidade ou, SeA
for um indexador somente leitura, uma conversão de referência implícita.
Esta é tecnicamente uma mudança de rutura, pois o programa abaixo imprime "C1.M" hoje, mas imprimiria "C2.M" com a 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 alteração de rutura, podemos considerar 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 de que uma implementação de interface explícita deve declarar um tipo de retorno não menos derivado do que o tipo de retorno declarado em qualquer substituição em suas interfaces base.
Implicações de compatibilidade de API
TBD
Questões em aberto
A especificação não diz como o chamador obtém o tipo de retorno mais refinado. Presumivelmente, isso seria feito de forma semelhante à maneira como os chamadores obtêm as especificações de parâmetros de substituição mais derivadas.
Se tivermos as seguintes interfaces:
interface I1 { I1 M(); }
interface I2 { I2 M(); }
interface I3: I1, I2 { override I3 M(); }
Observe que, em I3
, os métodos I1.M()
e I2.M()
foram "mesclados". Ao implementar I3
, é necessário implementá-los em conjunto.
Geralmente, precisamos de uma implementação explícita para nos referirmos ao método original. A questão é, em uma aula
class C : I1, I2, I3
{
C IN.M();
}
O que isso significa aqui? O que deve ser N?
Sugiro que permitamos a implementação de I1.M
ou I2.M
(mas não ambos) e tratemos isso como uma implementação de ambos.
Desvantagens
- [ ] Toda alteração de idioma deve justificar o seu custo.
- [ ] Devemos garantir que o desempenho seja razoável, mesmo no caso de hierarquias profundas de herança
- [ ] Devemos garantir que os artefatos da estratégia de tradução não afetem a semântica da linguagem, mesmo ao consumir novas IL de compiladores antigos.
Alternativas
Poderíamos flexibilizar ligeiramente as regras linguísticas para permitir, na fonte,
// 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;
}
}
Questões por resolver
- [ ] 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 em C# 9.0.
C# feature specifications