Genéricos vs interface comum?


20

Não me lembro de quando escrevi aula genérica da última vez. Toda vez que penso que preciso, depois de pensar um pouco, concluo que não preciso.

A segunda resposta a essa pergunta me fez pedir esclarecimentos (como ainda não posso comentar, fiz uma nova pergunta).

Então, vamos dar o código fornecido como um exemplo de caso em que precisamos de genéricos:

public class Repository<T> where T : class, IBusinessOBject
{
  T Get(int id)
  void Save(T obj);
  void Delete(T obj);
}

Possui restrições de tipo: IBusinessObject

Minha maneira usual de pensar é: a classe está restrita a usar IBusinessObject, assim como as classes que usam isso Repository. O repositório armazena esses IBusinessObjects, provavelmente os clientes Repositorydesejam obter e usar objetos através da IBusinessObjectinterface. Então, por que não apenas

public class Repository
{
  IBusinessOBject Get(int id)
  void Save(IBusinessOBject obj);
  void Delete(IBusinessOBject obj);
}

Tho, o exemplo não é bom, pois é apenas outro tipo de coleção e a coleção genérica é clássica. Nesse caso, a restrição de tipo também parece estranha.

De fato, o exemplo class Repository<T> where T : class, IBusinessbBjecté bem parecido class BusinessObjectRepositorycomigo. Qual é o objetivo dos genéricos de corrigir.

O ponto principal é: os genéricos são bons para qualquer coisa, exceto coleções, e as restrições de tipo não tornam genéricos tão especializados, como o uso dessa restrição de tipo em vez do parâmetro de tipo genérico dentro da classe?

Respostas:


24

Vamos primeiro falar sobre polimorfismo paramétrico puro e entrar no polimorfismo limitado posteriormente.

Polimorfismo paramétrico

O que significa polimorfismo paramétrico? Bem, isso significa que um tipo, ou melhor, tipo construtor é parametrizado por um tipo. Como o tipo é passado como parâmetro, você não pode saber antecipadamente o que pode ser. Você não pode fazer nenhuma suposição com base nisso. Agora, se você não sabe o que pode ser, então qual é a utilidade? O que você pode fazer com isso?

Bem, você pode armazenar e recuperar, por exemplo. É o caso que você já mencionou: coleções. Para armazenar um item em uma lista ou matriz, não preciso saber nada sobre o item. A lista ou matriz pode ser completamente alheia ao tipo.

Mas e o Maybetipo? Se você não estiver familiarizado, Maybeé um tipo que talvez tenha um valor e talvez não. Onde você o usaria? Bem, por exemplo, ao retirar um item de um dicionário: o fato de um item não estar no dicionário não é uma situação excepcional; portanto, você realmente não deve lançar uma exceção se o item não estiver lá. Em vez disso, você retorna uma instância de um subtipo de Maybe<T>, que possui exatamente dois subtipos: Nonee Some<T>. int.Parseé outro candidato a algo que realmente deve retornar um em Maybe<int>vez de lançar uma exceção ou toda a int.TryParse(out bla)dança.

Agora, você pode argumentar que isso Maybeé meio que uma lista que só pode ter zero ou um elemento. E assim meio que meio que uma coleção.

Então que tal Task<T>? É um tipo que promete retornar um valor em algum momento no futuro, mas não necessariamente tem um valor no momento.

Ou sobre o quê Func<T, …>? Como você representaria o conceito de uma função de um tipo para outro, se você não pode abstrair os tipos?

Ou, de maneira mais geral: considerando que a abstração e a reutilização são as duas operações fundamentais da engenharia de software, por que você não gostaria de abstrair sobre os tipos?

Polimorfismo limitado

Então, agora vamos falar sobre polimorfismo limitado. O polimorfismo limitado é basicamente onde o polimorfismo paramétrico e o polimorfismo de subtipo se encontram: em vez de um construtor de tipo estar completamente alheio a seu parâmetro de tipo, você pode vincular (ou restringir) o tipo a um subtipo de algum tipo especificado.

Vamos voltar às coleções. Pegue uma hashtable. Dissemos acima que uma lista não precisa saber nada sobre seus elementos. Bem, uma hashtable faz: ela precisa saber que pode fazer hash. (Nota: em C #, todos os objetos são hasháveis, assim como todos os objetos podem ser comparados quanto à igualdade. Isso não é verdade para todos os idiomas e, às vezes, é considerado um erro de design, mesmo em C #.)

Portanto, você deseja restringir seu parâmetro de tipo para o tipo de chave na hashtable para ser uma instância de IHashable:

class HashTable<K, V> where K : IHashable
{
  Maybe<V> Get(K key);
  bool Add(K key, V value);
}

Imagine se você tivesse o seguinte:

class HashTable
{
    object Get(IHashable key);
    bool Add(IHashable key, object value);
}

O que você faria com a valuesaída de lá? Você não pode fazer nada com isso, você sabe apenas que é um objeto. E se você iterar sobre tudo, tudo o que obtém é um par de algo que você sabe que é um IHashable(o que não ajuda muito porque ele tem apenas uma propriedade Hash) e algo que você conhece é um object(o que ajuda ainda menos).

Ou algo baseado no seu exemplo:

class Repository<T> where T : ISerializable
{
    T Get(int id);
    void Save(T obj);
    void Delete(T obj);
}

O item precisa ser serializável porque será armazenado em disco. Mas e se você tiver isso:

class Repository
{
    ISerializable Get(int id);
    void Save(ISerializable obj);
    void Delete(ISerializable obj);
}

Com o caso genérico, se você colocar um BankAccountem, você tem uma BankAccountvolta, com métodos e propriedades como Owner, AccountNumber, Balance, Deposit, Withdraw, etc. Algo que você pode trabalhar com ele. Agora, o outro caso? Você coloca em um BankAccount, mas você recebe de volta um Serializable, que tem apenas uma propriedade: AsString. O que você vai fazer com isso?

Existem também alguns truques interessantes que você pode fazer com o polimorfismo limitado:

Polimorfismo ligado a F

A quantificação limitada por F é basicamente onde a variável de tipo aparece novamente na restrição. Isso pode ser útil em algumas circunstâncias. Por exemplo, como você escreve uma ICloneableinterface? Como você escreve um método em que o tipo de retorno é o tipo da classe de implementação? Em um idioma com um recurso MyType , é fácil:

interface ICloneable
{
    public this Clone(); // syntax I invented for a MyType feature
}

Em uma linguagem com polimorfismo limitado, você pode fazer algo assim:

interface ICloneable<T> where T : ICloneable<T>
{
    public T Clone();
}

class Foo : ICloneable<Foo>
{
    public Foo Clone()
    {
        // …
    }
}

Observe que isso não é tão seguro quanto a versão MyType, porque não há nada que impeça alguém de simplesmente passar a classe "errada" para o construtor de tipos:

class EvilBar : ICloneable<SomethingTotallyUnrelatedToBar>
{
    public SomethingTotallyUnrelatedToBar Clone()
    {
        // …
    }
}

Membros do tipo Resumo

Como se vê, se você tem membros e subtipos de tipos abstratos, pode se dar bem completamente sem polimorfismo paramétrico e ainda fazer as mesmas coisas. Scala está caminhando nessa direção, sendo a primeira linguagem importante que começou com genéricos e depois tentou removê-los, o que é exatamente o contrário, por exemplo, Java e C #.

Basicamente, no Scala, assim como você pode ter campos, propriedades e métodos como membros, também pode ter tipos. E, assim como os campos, propriedades e métodos podem ser deixados abstratos para serem implementados em uma subclasse posteriormente, os membros do tipo também podem ser deixados abstratos. Vamos voltar às coleções, simples List, que seriam algo assim, se suportadas em C #:

class List
{
    T; // syntax I invented for an abstract type member
    T Get(int index) { /* … */ }
    void Add(T obj) { /* … */ }
}

class IntList : List
{
    T = int;
}
// this is equivalent to saying `List<int>` with generics

eu entendo, que abstração sobre tipos é útil. eu simplesmente não vejo o seu uso na "vida real". Func <> e Tarefa <> e Ação <> são tipos de biblioteca. e obrigado interface IFoo<T> where T : IFoo<T>também me lembrei . essa é obviamente a aplicação da vida real. o exemplo é ótimo. mas, por alguma razão, não me sinto satisfeito. Eu prefiro pensar em algo quando é apropriado e quando não é. as respostas aqui têm alguma contribuição para esse processo, mas ainda me sinto incomodado com tudo isso. é estranho, porque os problemas no nível da linguagem já não me incomodam há tanto tempo.
Jungle_mole

Ótimo exemplo. Eu senti como se estivesse de volta à sala de aula. +1
Chef_Code

1
@Chef_Code: esperança que isso é um elogio :-P
Jörg W Mittag

Sim. Mais tarde, pensei em como isso poderia ser percebido depois de já ter comentado. Então, para confirmar a sinceridade ... Sim, não é mais um elogio.
Chef_Code 26/09/16

14

O ponto principal é: os genéricos são bons para qualquer coisa, exceto coleções, e as restrições de tipo não tornam genéricos tão especializados, como o uso dessa restrição de tipo em vez do parâmetro de tipo genérico dentro da classe?

Não. Você está pensando demais Repository, onde é praticamente o mesmo. Mas não é para isso que servem os genéricos. Eles estão lá para os usuários .

O ponto principal aqui não é que o próprio repositório seja mais genérico. É que os usuários estão mais specialized- ou seja, de que Repository<BusinessObject1>e Repository<BusinessObject2>diferentes tipos, e, além disso, que, se eu tomar um Repository<BusinessObject1>, eu sei que vou receber BusinessObject1de volta para fora da Get.

Você não pode oferecer essa digitação forte a partir de herança simples. Sua classe Repository proposta não faz nada para impedir que as pessoas confundam repositórios para diferentes tipos de objetos de negócios ou garantam a volta do tipo correto de objeto de negócios.


Obrigado, isso faz sentido. Mas o objetivo de usar esse recurso de linguagem altamente elogiado é tão simples quanto ajudar os usuários com o IntelliSense desativado? (Estou exagerando um pouco, mas eu tenho certeza que você começa o ponto)
jungle_mole

@zloidooraque: também o IntelliSense não sabe que tipo de objetos estão armazenados em um repositório. Mas sim, você pode fazer qualquer coisa sem genéricos, se quiser usar elencos.
Gexicide

@gexicide esse é o ponto: não vejo onde preciso usar os modelos se usar uma interface comum. Eu nunca disse "use Object". Também entendo por que usar genéricos ao escrever coleções (princípio DRY). Provavelmente, a minha pergunta inicial deve ter sido algo sobre o uso de genéricos fora do contexto coleções ..
jungle_mole

@zloidooraque: Não tem nada a ver com o meio ambiente. O Intellisense não pode dizer se um IBusinessObjecté um BusinessObject1ou um BusinessObject2. Ele não pode resolver sobrecargas com base no tipo derivado que não conhece. Ele não pode rejeitar o código que passa no tipo errado. Há um milhão de bits da digitação mais forte que o Intellisense não pode fazer absolutamente nada. Um melhor suporte a ferramentas é um benefício interessante, mas nada tem a ver com os principais motivos.
DeadMG

@DeadMG e esse é o meu ponto: o intellisense não pode fazer isso: use genérico, então poderia? isso importa? quando você obtém o objeto por sua interface, por que fazer o downcast? se você fizer isso, é um design ruim, não? e por que e o que é "resolver sobrecargas"? o usuário não deve decidir se deve chamar o método ou não com base no tipo derivado se delegar a chamada do método correto no sistema (qual é o polimorfismo). e isso novamente me leva a uma pergunta: os genéricos são úteis fora dos contêineres? Eu não estou discutindo com você, eu realmente preciso entender isso.
Jungle_mole 13/12/2015

12

"os clientes mais prováveis ​​deste repositório desejam obter e usar objetos através da interface IBusinessObject".

Não, eles não vão.

Vamos considerar que o IBusinessObject possui a seguinte definição:

public interface IBusinessObject
{
  public int Id { get; }
}

Apenas define o ID porque é a única funcionalidade compartilhada entre todos os objetos de negócios. E você tem dois objetos de negócios reais: Pessoa e Endereço (como Pessoas não têm ruas e Endereços não têm nomes, você não pode restringir os dois a uma interface comum com a funcionalidade de ambos. Isso seria um design terrível, violando o Princípio de degradação da interface , o "I" no SOLID )

public class Person : IBusinessObject
{
  public int Id { get; private set; }
  public string Name { get; private set; }
}

public class Address : IBusinessObject
{
  public int Id { get; private set; }
  public string City { get; private set; }
  public string StreetName { get; private set; }
  public int Number { get; private set; }
}

Agora, o que acontece quando você usa a versão genérica do repositório:

public class Repository<T> where T : class, IBusinessObject
{
  T Get(int id)
  void Save(T obj);
  void Delete(T obj);
}

Quando você chama o método Get no repositório genérico, o objeto retornado será fortemente digitado, permitindo acessar todos os membros da classe.

Person p = new Repository<Person>().Get(1);
int id = p.Id;
string name = p.Name;

Address a = new Repository<Address>().Get(1);
int id = a.Id;
string cityName = a.City;
int houseNumber = a.Number;

Por outro lado, quando você usa o Repositório não genérico:

public class Repository
{
  IBusinessOBject Get(int id)
  void Save(IBusinessOBject obj);
  void Delete(IBusinessOBject obj);
}

Você só poderá acessar os membros da interface IBusinessObject:

IBussinesObject p = new Repository().Get(1);
int id = p.Id; //OK
string name = p.Name; //Oooops, you dont have "Name" defined on the IBussinesObject interface.

Portanto, o código anterior não será compilado devido às seguintes linhas:

string name = p.Name;
string cityName = a.City;
int houseNumber = a.Number;

Claro, você pode converter o IBussinesObject para a classe real, mas perderá toda a mágica do tempo de compilação permitida pelos genéricos (levando a InvalidCastExceptions mais adiante), sofrerá com a sobrecarga do elenco desnecessariamente ... E mesmo se você não se preocupe com o tempo de compilação, verificando nem o desempenho (você deveria), a transmissão depois definitivamente não trará nenhum benefício sobre o uso de genéricos em primeiro lugar.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.