BT

Disseminando conhecimento e inovação em desenvolvimento de software corporativo.

Contribuir

Tópicos

Escolha a região

Início Notícias Futuro do .NET: Herança Múltipla

Futuro do .NET: Herança Múltipla

Uma nova proposta controversa para o .NET sugere a introdução de uma forma limitada de herança múltipla via interfaces abstratas. Essa funcionalidade foi inspirada pelos métodos padrões do Java.

O propósito de um método padrão é permitir ao desenvolvedor modificar uma interface abstrata depois de ela ter sido publicada. Normalmente isso não é permitido no Java ou no .NET, pois isso seria uma que quebra compatibilidade. Mas com os métodos padrão, o autor da interface consegue fornecer uma implementação que pode ser sobrescrita, o que alivia o problema de compatibilidade.

A versão do C# desta proposta incluí sintaxe para:

  • um corpo para métodos (uma implementação padrão)
  • um corpo para propriedades
  • métodos e propriedades estáticas
  • métodos e propriedades privadas (o acesso padrão é público)
  • sobrescrita de métodos e propriedades

Esta proposta não daria às interfaces a habilidade de possuir campos, sendo assim é uma forma limitada de herança múltipla que evita alguns dos problemas encontrados em C++ (mesmo que os campos possam ser simulados pelo ConditionalWeakTable).

Caso de Uso: IEnumerable.Count

O caso de uso mais citado para essa funcionalidade é a habilidade de adicionar a propriedade Count para um IEnumerable<T>. A ideia é, ao invés de utilizar a o método de extensão Enumerable.Count, os desenvolvedores pudessem obter Count sem custo e sobrescrevê-lo se desejarem fornecer uma alternativa mais eficiente.

interface IEnumerable<T>
{
    int Count()
    {
        int count = 0;
        foreach (var x in this)
            count++;
        return count;
    }
}
interface IList<T> ...
{
    int Count { get; }
    override int IEnumerable<T>.Count() => this.Count;
}

Como você pode ver aqui, desenvolvedores que implementam IList<T> não precisam se preocupar em sobrescrever o método IEnumerable<T>.Count() pois a propriedade IList<T>.Count será utilizada automaticamente.

Uma preocupação com essa proposta é que ela deixa a interface "inchada". Se adicionarmos Count ao IEnumerable, por que não adicionar todos os outros métodos de extensão do IEnumerable?

Eirenarch escreve:

Eu estou meio surpreso que vocês estão seriamente considerando adicionar Count() ao IEnumerable. Isso é não é o mesmo erro do Reset? Nem todos IEnumerable podem ser resetados e nem todos podem ser contados de maneira segura já que muitos são de um uso só. Pensando nisso, eu não consigo me lembrar de alguma vez que precisei utilizar Count() em um IEnumerable. Eu o utilizo somente em chamadas LINQ à banco de dados pois eu não quero arriscar Count() consumindo o próprio IEnumerable ou sendo ineficiente. Por quê encorajar o uso de Count()?

DavidArno acrescenta:

Ha ha, esse é um argumento muito bom contra essa proposta. O time do BCL já fez uma bagunça antiga com os vários tipos de coleção. Sabendo o que fizeram, eu duvido que qualquer deles consigam olhar a Barbara Liskov nos olhos considerando que eles quebraram completamente seu princípio da substituição. A ideia de dá-los uma funcionalidade como esta, que deixariam eles causar mais estragos, é assustadora!

Numa reunião do BCL:

"OK pessoal, nós queremos adicionar suporte a cons ao IEnumerable. Alguma sugestão?"

"Isso é fácil, métodos padrão de interface resolve isso para nós. Simplesmente adicione (T head, IEnumerable tail) Cons() => throw new NotImplementedException(); e pronto. Quem implementar IEnumerable pode adicionar suporte quando quiser."

"Ótimo. Trabalho encerrado. Obrigado a todos, isso conclui a reunião da semana".

Note que LINQ faz parte de um time separado e que não há planos de migrar funcionalidades do LINQ para o IEnumerable<T>

Essa mudança também quebraria a camada que os métodos de extensão criam atualmente. Hoje, Enumerable.Count está em System.Core, que é dois níveis mais altos que o mscorlib. Alguns podem achar que trazer parte ou até mesmo todo o LINQ para o assembly mscorlib é uma adição desnecessária.

Outra crítica é que não é necessário - nós já possuímos um padrão de projeto que permite métodos de extensão serem sobrescritos caso necessário.

O Padrão de Sobrescrita de Métodos de Extensão

Métodos de extensão que podem ser sobrescritos dependem de checagem de interface. O ideal seria que a checagem fosse necessária para apenas uma interface, mas devido ao legado o exemplo de Enumerable.Count checa duas interfaces:

public static int Count<TSource>(this IEnumerable<TSource> source) {
    var collectionoft = source as ICollection<TSource>;
    if (collectionoft != null) return collectionoft.Count;
    var collection = source as ICollection;
    if (collection != null) return collection.Count;
    int count = 0;
    using (var e = source.GetEnumerator()) {
        while (e.MoveNext()) count++;
    }
    return count;
}

(Tratamento de erros removidos para maior clareza)

A desvantagem desse padrão é que a interface opcional pode ser muito abrangente. Por exemplo, a classe que deseja sobrescrever Enumerable.Count precisa implementar toda a interface ICollection<T>. Se é uma classe que somente realiza leitura, isso significa que várias exceções NotSupported terão que ser escritas (por razões históricas o exemplo checa ICollection<T> ao invés do muito menor IReadOnlyCollection<T>).

Métodos Padrões e as APIs Públicas das Classes

A fim de evitar problemas de compatibilidade ao adicionar novos métodos, métodos padrões não são acessíveis via interface pública da classe. Considere o exemplo acima de IEnumerable.Count e esta classe:

class Countable : IEnumerable<int>
{
    public IEnumerator<int> GetEnumerator() {…}
}

Já que IEnumerable.Count não foi sobrescrito, você não pode escrever este código:

var x = new Countable();
var y = x.Count();

Ao invés disso você precisa realizar um cast:

var y = ((IEnumerable<int>)x).Count();

Isto limita a utilizada em prover implementações padrões para as classes, já que elas precisariam adicionar código redundante para expor o método da interface em sua API pública..

Sobrescrevendo Métodos Padrão com Métodos Padrão

Métodos padrões em uma interface podem sobrescrever um métodos padrão de uma outra interface. Você pode ver isso no nosso caso de uso do IEnumerable.Count.

Assim como em métodos comuns, você precisa utilizar a palavra-chave override, caso contrário o novo método será tratado como não tendo relação com o outro método.

Você pode também marcar o método de uma interface como "override abstract". Normalmente a palavra-chave abstract não é necessária, já que todos os métodos de um interface são abstratos por padrão.

Ordem de Resolução de Métodos de Extensão vs Métodos Padrão

Zippec levanta uma questão importante sobre o que acontece quando um novo método adicionado à uma interface tem o mesmo nome que um método de extensão para aquela interface:

Qual a história entre o upgrade da API atual e os métodos padrão? Eu assumo que eles tenham maior prioridade na resolução que os métodos de extensão? Peguemos Count() por exemplo. Irá valer para o IEnumerable? Se sim, irá esconder o IEnumerable.Count() do Linq significando que recompilar com essa funcionalidade irá mudar o código chamado? Isso é ok? Eu assumo poderia ser um problema para IQueryable.

Se isso foi um problema e obtivemos Count como propriedade no BCL para mitigar isso, não significaria que nós nunca poderemos ter qualquer método padrão (apenas propriedade) nas interfaces existentes do BCL, pois poderia mudar o significado de métodos de extensão próprios já existentes

Enquanto raro, alguns desenvolvedores criam suas próprias bibliotecas de métodos de extensão que imitam os encontrados no LINQ mas com comportamento diferente. A habilidade de trocar uma biblioteca de extensão por outra seria perdida se fosse movida para a interface como métodos padrões.

Caso de Uso: INotifyPropertyChanged

Aqui vai um outro possível uso da nova funcionalidade alguns estiveram considerando:

interface INotifyPropertyChanged
{
    event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(PropertyChangedEventArgs args) => PropertyChanged?.Invoke(args);
    protected void OnPropertyChanged([CallerMemberName] string propertyName) => OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
    protected void SetProperty<T>(ref T storage, T value, PropertyChangedEventArgs args)
    {
        if (!EqualityComparer<T>.Default.Equals(storage, value))
        {
            storage = value;
            OnPropertyChanged(args);
        }
    }
    protected void SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName) => SetProperty(ref storage, value, new PropertyChangedEventArgs(propertyName));
}

No entanto, isso na verdade não irá funcionar pois a interface não possui uma maneira de disparar o evento. Interfaces somente declaram funcionalidade para adição e remoção de evento, e não o delegate utilizado para armazenar os handlers do evento..

Mesmo não fazendo parte da proposta, isto pode mudar. O CLR possui um slot para armazenar um “método de acesso de disparo”, apesar de VB ser a única linguagem principal a utilizá-lo.

Apoio Adicional para a Proposta

HaloFour escreve:

Isso parece um argumento muito ideológico. Existem problemas sabidos que o time não conseguiu tratar desde que o .NET 1.0 foi lançado. As soluções típicas soluções são conhecidas há muito tempo. Eles com frequência fazem uma bagunça de API. IFoo, IFoo2, IFoo3, IFooEx, IFooSpecial, IFooWithBar, etc., etc., etc. Métodos de extensão conseguem ir longe em tratar esses problemas mas falta-os especialização fora do que pode ser acessado no método de extensão.

Métodos de extensão resolvem bem esses problemas. Eles permitiram ao time do Java rever muitas de suas interfaces que existem há muito tempo com métodos auxiliares adicionais, muitos deles que são especializados por várias implementações, e.g. Map#computeIfPresent.

Outras Críticas da Proposta

HerpDerpImARedditor escreve:

Ih, isso irá resultar em sério código espaguete. Me desculpe se eu perdi algo, mas o que esse padrão resolve na camada de implementação? Quase parece que isso aniquila a bela diferença entre interface e implementação concreta. A IDE será severamente específica sobre de onde a implementação em tempo de execucão virá? Não consigo enxergar isso funcionando com IoC.

Eu amo .NET, vim de ASP clássico/VB direto para .NET 1. Esta é uma das primeiras adições de especificação que eu não concordo (eu estremeci um pouco quando 'dynamic' entrou em cena mas vi seu caso de uso). Eu vejo alguns dizendo que eles ignorarão que essa funcionalidade existe; minha preocupação é o código dos outros que eu venha ter que lidar não ignorarão [essa funcionalidade].

Ah bem, eu acho que teremos que vê-lo em ação antes de qualquer julgamento verdadeiro ser feito.

Canthros escreve:

Eu acho que isso me entristece.

A discussão do github indica que isso foi iniciado sobre a insatisfação com a bagunça que é a implementação dos vários métodos de extensão do LINQ, particularmente pois há de se espionar o tipo para prover implementações otimizadas. Generalizando que uma característica de uma linguagem pode salvar aos implementadores do .NET Core um trabalho significativo. Sob o custo de sobrecarregar a linguagem com uma distinção confusa entre interfaces e classes abstratas, e uma característica que traz problemas fortes.

Parece que isso é substancialmente mitigada pela proposta de Shapes, mas eu não tenho tempo para realmente pensar sobre tudo isso, nesse momento.

Avalie esse artigo

Relevância
Estilo/Redação

Conteúdo educacional

BT