Jaki jest powód używania interfejsu w porównaniu z typem z ograniczeniami ogólnymi

15

W językach zorientowanych obiektowo, które obsługują ogólne parametry typu (znane również jako szablony klas i polimorfizm parametryczny, chociaż oczywiście każda nazwa ma inne konotacje), często możliwe jest określenie ograniczenia typu dla parametru typu, tak aby można było zejść z innego rodzaju. Na przykład jest to składnia w języku C #:

//for classes:
class ExampleClass<T> where T : I1 {

}
//for methods:
S ExampleMethod<S>(S value) where S : I2 {
        ...
}

Jakie są powody używania rzeczywistych typów interfejsów w porównaniu z typami ograniczonymi przez te interfejsy? Na przykład, jakie są przyczyny podpisania metody I2 ExampleMethod(I2 value)?

GregRos
źródło
4
szablony klas (C ++) są czymś zupełnie innym i znacznie potężniejszym niż nędzne leki generyczne. Mimo że języki posiadające generyczne pożyczyły dla nich składnię szablonów.
Deduplicator
Metody interfejsu to wywołania pośrednie, natomiast metody typu mogą być wywołaniami bezpośrednimi. To ostatnie może być szybsze niż poprzednie, aw przypadku refparametrów typu wartości może faktycznie zmodyfikować typ wartości.
user541686,
@Deduplicator: Biorąc pod uwagę, że generyczne są starsze niż szablony, nie widzę, jak generyczne mogły pożyczyć cokolwiek z szablonów, składniowych lub innych.
Jörg W Mittag
3
@ JörgWMittag: Podejrzewam, że przez „języki obiektowe, które obsługują generyczne”, Deduplicator mógł zrozumieć „Java i C #” zamiast „ML i Ada”. Zatem wpływ C ++ na to pierwsze jest wyraźny, mimo że nie wszystkie języki mają generyczny lub parametryczny polimorfizm zapożyczony z C ++.
Steve Jessop,
2
@SteveJessop: ML, Ada, Eiffel, Haskell wcześniej szablony C ++, Scala, F #, OCaml przyszły i żaden z nich nie podziela składni C ++. (Co ciekawe, nawet D, który mocno pożycza od C ++, zwłaszcza szablonów, nie ma wspólnej składni C ++.) „Java i C #” to raczej wąski pogląd na „języki posiadające generyczne”.
Jörg W Mittag

Odpowiedzi:

21

Korzystanie z wersji parametrycznej daje

  1. Więcej informacji dla użytkowników funkcji
  2. Ogranicza liczbę programów, które możesz napisać (bezpłatne sprawdzanie błędów)

Jako przypadkowy przykład, załóżmy, że mamy metodę, która oblicza pierwiastki równania kwadratowego

int solve(int a, int b, int c) {
  // My 7th grade math teacher is laughing somewhere
}

A potem chcesz, aby działał na innych rodzajach, takich jak rzeczy poza int. Możesz napisać coś takiego

Num solve(Num a, Num b, Num c){
  ...
}

Problem polega na tym, że nie mówi to, co chcesz. To mówi

Daj mi 3 dowolne rzeczy, które są liczbami (niekoniecznie w ten sam sposób), a dam ci jakąś liczbę

Nie możemy zrobić coś jak int sol = solve(a, b, c)gdyby a, bi cto intdlatego, że nie wiemy, że metoda będzie zwracaćint w końcu! Prowadzi to do niezręcznego tańca z upuszczaniem i modlitwą, jeśli chcemy zastosować rozwiązanie w szerszym znaczeniu.

Wewnątrz funkcji ktoś mógłby podać nam liczbę zmiennoprzecinkową, bigint i stopnie, a my musielibyśmy je dodać i pomnożyć razem. Chcielibyśmy to statycznie odrzucić, ponieważ operacje między tymi 3 klasami będą bełkotliwe. Stopnie są mod 360, więc tak nie będziea.plus(b) = b.plus(a) i podobne podobieństwa się pojawią.

Jeśli zastosujemy polimorfizm parametryczny z podtypami, możemy wykluczyć to wszystko, ponieważ nasz typ faktycznie mówi, co mamy na myśli

<T : Num> T solve(T a, T b, T c)

Lub słowami „Jeśli podasz mi jakiś typ, który jest liczbą, mogę rozwiązać równania z tymi współczynnikami”.

To pojawia się również w wielu innych miejscach. Innym dobrym źródłem przykładów są funkcje, które streszczenie nad jakimś pojemniku, Ala reverse, sort, map, itd.

Daniel Gratzer
źródło
8
Podsumowując, wersja ogólna gwarantuje, że wszystkie trzy dane wejściowe (i dane wyjściowe) będą tego samego typu .
MathematicalOrchid
Jest to jednak niewystarczające, gdy nie kontrolujesz danego typu (a zatem nie możesz dodać do niego interfejsu). Dla maksymalnej ogólności musisz zaakceptować interfejs sparametryzowany przez typ argumentu (np. Num<int>) Jako dodatkowy argument. Zawsze możesz zaimplementować interfejs dla dowolnego typu poprzez delegację. Właśnie takie są klasy typu Haskell, z wyjątkiem o wiele bardziej żmudnego użytkowania, ponieważ trzeba jawnie ominąć interfejs.
Doval
16

Jakie są powody używania rzeczywistych typów interfejsów w porównaniu z typami ograniczonymi przez te interfejsy?

Ponieważ tego potrzebujesz ...

IFoo Fn(IFoo x);
T Fn<T>(T x) where T: IFoo;

to dwa zdecydowanie różne podpisy. Pierwszy bierze dowolny typ implementujący interfejs, a jedyną gwarancją jest to, że zwracana wartość spełnia interfejs.

Drugi przyjmuje dowolny typ implementujący interfejs i gwarantuje, że zwróci co najmniej ten typ ponownie (zamiast czegoś, co spełnia mniej restrykcyjny interfejs).

Czasami potrzebujesz słabszej gwarancji. Czasami chcesz silniejszego.

Telastyn
źródło
Czy możesz podać przykład zastosowania słabszej wersji gwarancji?
GregRos
4
@GregRos - Na przykład w jakimś kodzie parsera, który napisałem. Mam funkcję, Orktóra pobiera dwa Parserobiekty (abstrakcyjna klasa bazowa, ale zasada obowiązuje) i zwraca nową Parser(ale z innym typem). Użytkownik końcowy nie powinien wiedzieć ani przejmować się, jaki jest typ betonu.
Telastyn
W C # wyobrażam sobie, że zwrócenie T innego niż ten, który został przekazany, jest prawie niemożliwe (bez bólu odbicia) bez nowego ograniczenia, a także uczynienie twojej silnej gwarancji samą w sobie bezużyteczną.
NtscCobalt
1
@NtscCobalt: Jest to bardziej przydatne, gdy łączysz programowanie parametryczne i ogólne. Np. To, co LINQ robi cały czas (akceptuje IEnumerable<T>, zwraca inną, IEnumerable<T>która jest np. W rzeczywistości OrderedEnumerable<T>)
Ben Voigt
2

Użycie ograniczonych danych ogólnych dla parametrów metody może pozwolić metodzie na bardzo zwrotny typ w zależności od tego, co przekazano. W .NET mogą mieć także dodatkowe zalety. Pomiędzy nimi:

  1. Metodę, która przyjmuje ograniczoną wartość ogólną jako parametr reflub out, można przekazać zmienną spełniającą ograniczenie; z drugiej strony, metoda ogólna z parametrem typu interfejsu byłaby ograniczona do akceptowania zmiennych dokładnie tego typu interfejsu.

  2. Metoda z parametrem typu ogólnego T może akceptować ogólne zbiory T. Metoda, która akceptuje an, IList<T> where T:IAnimalbędzie w stanie zaakceptować List<SiameseCat>, ale metoda, która chciała an IList<Animal>, nie będzie w stanie tego zrobić.

  3. Ograniczenie może czasami określać interfejs pod względem rodzaju ogólnego, np where T:IComparable<T>.

  4. Struktura, która implementuje interfejs, może być zachowana jako typ wartości, gdy zostanie przekazana do metody przyjmującej ograniczony parametr ogólny, ale musi zostać umieszczona w ramce, gdy zostanie przekazana jako typ interfejsu. Może to mieć ogromny wpływ na szybkość.

  5. Ogólny parametr może mieć wiele ograniczeń, podczas gdy nie ma innego sposobu na określenie parametru „jakiegoś typu, który implementuje zarówno IFoo, jak i IBar”. Czasami może to być obosieczny miecz, ponieważ kod, który otrzymał parametr typu, IFoobędzie miał trudności z przekazaniem go do takiej metody, która spodziewałaby się podwójnego ograniczenia, nawet jeśli dana instancja spełniłaby wszystkie ograniczenia.

Jeśli w konkretnej sytuacji nie byłoby żadnej korzyści z używania standardowego, po prostu zaakceptuj parametr typu interfejsu. Użycie generycznego zmusi system typów i JITtera do wykonania dodatkowej pracy, więc jeśli nie ma żadnych korzyści, nie należy tego robić. Z drugiej strony bardzo często stosuje się co najmniej jedną z powyższych zalet.

supercat
źródło