Vamos primeiro falar sobre polimorfismo paramétrico puro e entrar no polimorfismo limitado posteriormente.
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 Maybe
tipo? 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: None
e 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?
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 value
saí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 BankAccount
em, você tem uma BankAccount
volta, 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:
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 ICloneable
interface? 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
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.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>
eRepository<BusinessObject2>
diferentes tipos, e, além disso, que, se eu tomar umRepository<BusinessObject1>
, eu sei que vou receberBusinessObject1
de volta para fora daGet
.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.
fonte
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 ..IBusinessObject
é umBusinessObject1
ou umBusinessObject2
. 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."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:
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 )
Agora, o que acontece quando você usa a versão genérica do repositório:
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.
Por outro lado, quando você usa o Repositório não genérico:
Você só poderá acessar os membros da interface IBusinessObject:
Portanto, o código anterior não será compilado devido às seguintes linhas:
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.
fonte