Parliamo prima del polimorfismo parametrico puro e in seguito entriamo nel polimorfismo limitato.
Cosa significa polimorfismo parametrico? Bene, significa che un tipo, o meglio un costruttore di tipi, è parametrizzato da un tipo. Poiché il tipo viene passato come parametro, non è possibile sapere in anticipo quale potrebbe essere. Non è possibile formulare ipotesi basate su di esso. Ora, se non sai cosa potrebbe essere, a che serve? Cosa puoi farci?
Bene, potresti archiviarlo e recuperarlo, per esempio. È il caso che hai già menzionato: le raccolte. Per archiviare un elemento in un elenco o un array, non ho bisogno di sapere nulla sull'elemento. L'elenco o l'array può essere completamente ignaro del tipo.
Ma per quanto riguarda il Maybe
tipo? Se non lo conosci, Maybe
è un tipo che forse ha un valore e forse no. Dove lo useresti? Bene, ad esempio, quando si estrae un elemento da un dizionario: il fatto che un elemento potrebbe non essere nel dizionario non è una situazione eccezionale, quindi non si dovrebbe davvero fare un'eccezione se l'elemento non è presente. Invece, si restituisce un'istanza di un sottotipo di Maybe<T>
, che ha esattamente due sottotipi: None
e Some<T>
. int.Parse
è un altro candidato di qualcosa che dovrebbe davvero restituire un Maybe<int>
invece di lanciare un'eccezione o l'intera int.TryParse(out bla)
danza.
Ora, potresti sostenere che Maybe
è un po 'come una lista che può contenere solo zero o uno elementi. E quindi una specie di collezione.
E allora Task<T>
? È un tipo che promette di restituire un valore ad un certo punto in futuro, ma non ha necessariamente un valore in questo momento.
O che dire Func<T, …>
? Come rappresenteresti il concetto di una funzione da un tipo a un altro se non riesci ad astrarre sui tipi?
O, più in generale: considerando che l'astrazione e il riutilizzo sono le due operazioni fondamentali dell'ingegneria del software, perché non dovresti voler astrarre sui tipi?
Quindi, parliamo ora del polimorfismo limitato. Il polimorfismo limitato è fondamentalmente il punto in cui il polimorfismo parametrico e il polimorfismo del sottotipo si incontrano: invece di un costruttore di tipi che è completamente ignaro del suo parametro di tipo, puoi legare (o vincolare) il tipo a un sottotipo di un tipo specificato.
Torniamo alle collezioni. Prendi una hashtable. Abbiamo detto sopra che un elenco non ha bisogno di sapere nulla dei suoi elementi. Bene, una hashtable lo fa: deve sapere che può hash. (Nota: in C #, tutti gli oggetti sono hash, proprio come tutti gli oggetti possono essere confrontati per uguaglianza. Questo non è vero per tutte le lingue, tuttavia, ed è talvolta considerato un errore di progettazione anche in C #.)
Pertanto, si desidera vincolare il parametro type per il tipo chiave nella tabella hash in modo che sia un'istanza di IHashable
:
class HashTable<K, V> where K : IHashable
{
Maybe<V> Get(K key);
bool Add(K key, V value);
}
Immagina se invece avessi questo:
class HashTable
{
object Get(IHashable key);
bool Add(IHashable key, object value);
}
Cosa faresti con un value
che esci di lì? Non puoi farci niente, sai solo che è un oggetto. E se iteri su di esso, tutto ciò che ottieni è una coppia di qualcosa che sai essere IHashable
(che non ti aiuta molto perché ha solo una proprietà Hash
) e qualcosa che conosci è un object
(che ti aiuta anche meno).
O qualcosa basato sul tuo esempio:
class Repository<T> where T : ISerializable
{
T Get(int id);
void Save(T obj);
void Delete(T obj);
}
L'articolo deve essere serializzabile perché verrà archiviato sul disco. Ma cosa succede se hai questo invece:
class Repository
{
ISerializable Get(int id);
void Save(ISerializable obj);
void Delete(ISerializable obj);
}
Con il caso generico, se si mette un BankAccount
in, si ottiene un BankAccount
indietro, con metodi e immobili come Owner
, AccountNumber
, Balance
, Deposit
, Withdraw
, ecc Qualcosa che si può lavorare. Ora l'altro caso? Hai messo in una BankAccount
ma si ottiene indietro un Serializable
, che ha solo una proprietà: AsString
. Che cosa hai intenzione di fare con quello?
Ci sono anche alcuni trucchi che puoi fare con il polimorfismo limitato:
La quantificazione limitata da F è fondamentalmente il punto in cui la variabile type appare nuovamente nel vincolo. Questo può essere utile in alcune circostanze. Ad esempio, come si scrive ICloneable
un'interfaccia? Come si scrive un metodo in cui il tipo restituito è il tipo della classe di implementazione? In una lingua con una funzionalità MyType , è facile:
interface ICloneable
{
public this Clone(); // syntax I invented for a MyType feature
}
In una lingua con polimorfismo limitato, puoi invece fare qualcosa del genere:
interface ICloneable<T> where T : ICloneable<T>
{
public T Clone();
}
class Foo : ICloneable<Foo>
{
public Foo Clone()
{
// …
}
}
Si noti che questo non è sicuro come la versione MyType, perché non c'è nulla che impedisce a qualcuno di passare semplicemente la classe "sbagliata" al costruttore del tipo:
class EvilBar : ICloneable<SomethingTotallyUnrelatedToBar>
{
public SomethingTotallyUnrelatedToBar Clone()
{
// …
}
}
Membri di tipo astratto
A quanto pare, se si hanno membri e sottotipi di tipo astratti, si può effettivamente cavarsela completamente senza polimorfismo parametrico e fare comunque le stesse cose. Scala sta andando in questa direzione, essendo il primo linguaggio principale che è iniziato con i generici e poi ha cercato di rimuoverli, che è esattamente il contrario da Java e C #.
Fondamentalmente, in Scala, proprio come puoi avere campi, proprietà e metodi come membri, puoi anche avere tipi. E proprio come campi e proprietà e metodi possono essere lasciati astratti per essere implementati in una sottoclasse in un secondo momento, anche i membri del tipo possono essere lasciati astratti. Torniamo alle raccolte, una semplice List
, che sarebbe simile a questa, se fosse supportata in 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>
. questa è ovviamente l'applicazione della vita reale. l'esempio è fantastico. ma per qualche motivo non mi sento soddisfatto. preferisco concentrarmi su quando è appropriato e quando non lo è. le risposte qui danno un contributo a questo processo, ma mi sento ancora incerto su tutto questo. è strano perché i problemi a livello di lingua non mi disturbano da così tanto tempo.