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 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?
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:
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
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.