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.Nie. Za dużo myślisz o tym
Repository
, gdzie jest prawie tak samo. Ale nie do tego służą generycy. Są tam dla użytkowników .Kluczową kwestią tutaj nie jest to, że samo Repozytorium jest bardziej ogólne. Chodzi o to, że użytkownicy są bardziej wyspecjalizowani, tj. To
Repository<BusinessObject1>
iRepository<BusinessObject2>
są różnego rodzaju, a ponadto, że jeśli wezmęRepository<BusinessObject1>
, wiem , że z niegoBusinessObject1
wrócęGet
.Nie możesz zaoferować tego silnego pisania na podstawie zwykłego dziedziczenia. Proponowana klasa repozytorium nie robi nic, aby zapobiec pomyleniu repozytoriów dla różnych rodzajów obiektów biznesowych lub zagwarantowaniu poprawnego rodzaju zwracanych obiektów biznesowych.
źródło
Object
”. Rozumiem także, dlaczego przy tworzeniu kolekcji używamy rodzajów ogólnych (zasada DRY). Prawdopodobnie moim początkowym pytaniem powinno być coś na temat używania leków generycznych poza kontekstem kolekcji.IBusinessObject
jestBusinessObject1
aBusinessObject2
. Nie może rozwiązać problemu przeciążenia na podstawie typu pochodnego, którego nie zna. Nie może odrzucić kodu, który przechodzi w niewłaściwym typie. Istnieje milion bitów silniejszego pisania, które Intellisense nie może zrobić absolutnie nic. Lepsza obsługa narzędzi to dobra zaleta, ale tak naprawdę nie ma nic wspólnego z podstawowymi powodami.„najprawdopodobniej klienci tego repozytorium będą chcieli pobierać obiekty i korzystać z nich za pośrednictwem interfejsu IBusinessObject”.
Nie zrobią tego.
Rozważmy, że IBusinessObject ma następującą definicję:
Po prostu definiuje identyfikator, ponieważ jest to jedyna wspólna funkcjonalność między wszystkimi obiektami biznesowymi. I masz dwa rzeczywiste obiekty biznesowe: Osoba i Adres (ponieważ Osoby nie mają ulic i Adresów nie mają nazw, nie możesz powiązać ich obu z interfejsem komunikacyjnym o funkcjonalności obu. Byłby to straszny projekt, naruszający Zasada degradacji interfejsu , „I” w SOLID )
Co się stanie, gdy użyjesz ogólnej wersji repozytorium:
Po wywołaniu metody Get w ogólnym repozytorium zwrócony obiekt zostanie silnie wpisany, umożliwiając dostęp do wszystkich członków klasy.
Z drugiej strony, gdy korzystasz z nietypowego repozytorium:
Dostęp do członków będzie można uzyskać tylko z interfejsu IBusinessObject:
Tak więc poprzedni kod nie zostanie skompilowany z powodu następujących wierszy:
Jasne, możesz rzucić IBussinesObject na rzeczywistą klasę, ale stracisz całą magię czasu kompilacji, na którą pozwalają leki generyczne (co prowadzi do wyjątków InvalidCastExceptions w dół drogi), niepotrzebnie ucierpi z powodu rzutowania ... A nawet jeśli tego nie zrobisz dbaj o czas kompilacji, nie sprawdzając żadnej wydajności (powinieneś), casting po zdecydowanie nie da ci żadnej przewagi nad używaniem generics na pierwszym miejscu.
źródło