Compartilhar via


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 classe C, o método base substituído é determinado examinando cada classe base de C, começando com a classe base direta de C 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 que M 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 interface I, o método base que está sendo substituído é determinado ao examinar cada interface base direta ou indireta de I, coletando o conjunto de interfaces que declaram um método acessível com a mesma assinatura que M 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 de E e um tipo associado que é o tipo da propriedade. Caso T seja um tipo de classe, o tipo associado é selecionado a partir da primeira declaração ou sobrescrição da propriedade encontrada ao iniciar com Te 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 de T 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 com T 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 entre T 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 interface B quando:

  • A e B são métodos e o nome, tipo e listas de parâmetros formais de A e B são idênticos.
  • A e B são propriedades, o nome e o tipo de A e B são idênticos, e A tem os mesmos acessadores que B (A é permitido ter acessadores adicionais se não for uma implementação de membro de interface explícita).
  • A e B são eventos, e o nome e o tipo de A e B são idênticos.
  • A e B são indexadores, o tipo e as listas de parâmetros formais de A e B são idênticos, e A tem os mesmos acessadores que B (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 interface B quando:

  • A e B são métodos, e o nome e as listas de parâmetros formais de A e B são idênticos e o tipo de retorno de A é conversível para o tipo de retorno de B por meio de uma identidade de conversão de referência implícita para o tipo de retorno de B.
  • A and B são propriedades, o nome de A e B são idênticos, A tem os mesmos acessadores que B (A é permitido ter acessadores adicionais se não for uma implementação de membro de interface explícita) e o tipo de A é conversível para o tipo de retorno de B por meio de uma conversão de identidade ou, se A é uma propriedade somente leitura, uma conversão de referência implícita.
  • A e B são eventos, e o nome e o tipo de A e B são idênticos.
  • A and B são indexadores, as listas de parâmetros formais de A e B são idênticos, A tem os mesmos acessadores que B (A é permitido ter acessadores adicionais se não for uma implementação de membro de interface explícita) e o tipo de A é conversível para o tipo de retorno de B por meio de uma conversão de identidade ou, se A é 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