Porozmawiajmy najpierw o czystym parametrycznym polimorfizmie, a później zajmiemy się ograniczonym polimorfizmem.
Co oznacza polimorfizm parametryczny? Oznacza to, że konstruktor typu, a raczej typ jest parametryzowany przez typ. Ponieważ typ jest przekazywany jako parametr, nie można z góry wiedzieć, co to może być. Na tej podstawie nie można przyjmować żadnych założeń. Jeśli nie wiesz, co to może być, to jaki jest pożytek? Co możesz z tym zrobić?
Możesz na przykład przechowywać i odzyskiwać. Tak już wspomniałeś: kolekcje. Aby przechowywać element na liście lub tablicy, nie muszę nic wiedzieć o elemencie. Lista lub tablica mogą być całkowicie nieświadome typu.
Ale co z tym Maybe
typem? Jeśli go nie znasz, Maybe
jest to typ, który może mieć wartość, a może nie. Gdzie byś go użył? Na przykład przy pobieraniu elementu ze słownika: fakt, że elementu nie ma w słowniku, nie jest wyjątkową sytuacją, więc naprawdę nie powinieneś rzucać wyjątku, jeśli elementu nie ma. Zamiast tego zwracasz instancję podtypu Maybe<T>
, który ma dokładnie dwa podtypy: None
i Some<T>
. int.Parse
jest kolejnym kandydatem na coś, co naprawdę powinno zwrócić Maybe<int>
zamiast rzucać wyjątek lub cały int.TryParse(out bla)
taniec.
Teraz możesz argumentować, że Maybe
to trochę jak lista, która może mieć tylko zero lub jeden element. I tak jakby kolekcja.
Co z tego Task<T>
? Jest to typ, który obiecuje zwrócić wartość w pewnym momencie w przyszłości, ale niekoniecznie ma teraz wartość.
A co powiesz na Func<T, …>
? Jak przedstawiłbyś koncepcję funkcji z jednego typu na inny, jeśli nie możesz abstrakcji nad typami?
Lub bardziej ogólnie: biorąc pod uwagę, że abstrakcja i ponowne użycie są dwoma podstawowymi operacjami inżynierii oprogramowania, dlaczego nie chcesz mieć możliwości abstrakcji na typach?
Porozmawiajmy teraz o ograniczonym polimorfizmie. Ograniczony polimorfizm występuje w zasadzie tam, gdzie spotykają się polimorfizm parametryczny i polimorfizm podtypu: zamiast konstruktora typu całkowicie nieświadomego parametru parametru, możesz powiązać (lub ograniczyć) typ, aby był podtypem określonego typu.
Wróćmy do kolekcji. Weź hashtable. Powiedzieliśmy powyżej, że lista nie musi nic wiedzieć o swoich elementach. No cóż, tablica skrótów robi: musi wiedzieć, że potrafi je haszować. (Uwaga: w języku C # wszystkie obiekty są haszowalne, podobnie jak wszystkie obiekty można porównać pod względem równości. Nie jest to jednak prawdą we wszystkich językach i czasami jest uważane za błąd projektowy nawet w języku C #).
Zatem chcesz ograniczyć parametr typu dla typu klucza w tablicy mieszającej, aby był instancją IHashable
:
class HashTable<K, V> where K : IHashable
{
Maybe<V> Get(K key);
bool Add(K key, V value);
}
Wyobraź sobie, że zamiast tego masz to:
class HashTable
{
object Get(IHashable key);
bool Add(IHashable key, object value);
}
Co byś zrobił z tym, value
że się stamtąd wydostałeś? Nie możesz nic z tym zrobić, wiesz tylko, że to obiekt. A jeśli wykonasz iterację, wszystko, co zyskujesz, to para czegoś, o czym wiesz, że jest IHashable
(co niewiele ci pomaga, ponieważ ma tylko jedną właściwość Hash
) i coś, o czym wiesz, że jest object
(co pomaga ci jeszcze mniej).
Lub coś na podstawie twojego przykładu:
class Repository<T> where T : ISerializable
{
T Get(int id);
void Save(T obj);
void Delete(T obj);
}
Element musi być możliwy do serializacji, ponieważ będzie przechowywany na dysku. Ale co, jeśli masz to zamiast tego:
class Repository
{
ISerializable Get(int id);
void Save(ISerializable obj);
void Delete(ISerializable obj);
}
Z ogólnym przypadku, jeśli umieścić BankAccount
w, masz BankAccount
plecy, z metody i właściwości, takie jak Owner
, AccountNumber
, Balance
, Deposit
, Withdraw
, itd. Coś można pracować. A teraz druga sprawa? Włożysz BankAccount
ale można dostać z powrotem Serializable
, który ma tylko jedną właściwość: AsString
. Co zamierzasz z tym zrobić?
Istnieje również kilka ciekawych sztuczek, które można zrobić z ograniczonym polimorfizmem:
Kwantyfikacja ograniczona przez F polega zasadniczo na tym, że zmienna typu pojawia się ponownie w ograniczeniu. Może to być przydatne w niektórych okolicznościach. Np. Jak piszesz ICloneable
interfejs? Jak napisać metodę, w której typ zwracany jest typem klasy implementującej? W języku z funkcją MyType to proste:
interface ICloneable
{
public this Clone(); // syntax I invented for a MyType feature
}
W języku z ograniczonym polimorfizmem możesz zamiast tego zrobić coś takiego:
interface ICloneable<T> where T : ICloneable<T>
{
public T Clone();
}
class Foo : ICloneable<Foo>
{
public Foo Clone()
{
// …
}
}
Zauważ, że nie jest to tak bezpieczne jak wersja MyType, ponieważ nic nie stoi na przeszkodzie, aby ktoś po prostu przekazał „niewłaściwą” klasę konstruktorowi typów:
class EvilBar : ICloneable<SomethingTotallyUnrelatedToBar>
{
public SomethingTotallyUnrelatedToBar Clone()
{
// …
}
}
Członkowie typu abstrakcyjnego
Jak się okazuje, jeśli masz elementy typu abstrakcyjnego i subtypy, możesz faktycznie przejść całkowicie bez parametrycznego polimorfizmu i nadal robić te same rzeczy. Scala zmierza w tym kierunku, będąc pierwszym głównym językiem, który zaczął od generycznych, a następnie próbował je usunąć, co jest dokładnie na odwrót od np. Java i C #.
Zasadniczo w Scali, podobnie jak możesz mieć pola i właściwości oraz metody jako członków, możesz także mieć typy. I podobnie jak pola, właściwości i metody mogą pozostać abstrakcyjne, aby można je było później zaimplementować w podklasie, elementy typu mogą być również abstrakcyjne. Wróćmy do kolekcji, prostej List
, która wyglądałaby mniej więcej tak, gdyby była obsługiwana w 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>
. to oczywiście aplikacja z prawdziwego życia. przykład jest świetny. ale z jakiegoś powodu nie czuję się usatysfakcjonowany. wolę rozmyślać, kiedy jest to właściwe, a kiedy nie. odpowiedzi tutaj wnoszą pewien wkład w ten proces, ale nadal czuję, że jestem do tego nieprzygotowany. to dziwne, ponieważ problemy na poziomie językowym nie przeszkadzają mi już tak długo.