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.