Dlaczego więcej popularnych języków statycznych nie obsługuje przeciążania funkcji / metod według typów zwracanych? Nie mogę wymyślić nic takiego. Wydaje się nie mniej użyteczne lub uzasadnione niż wspieranie przeciążenia według typu parametru. Dlaczego jest o wiele mniej popularny?
252
Odpowiedzi:
W przeciwieństwie do tego, co mówią inni, przeciążenie według typu zwrotu jest możliwe i jest wykonywane przez niektóre współczesne języki. Zazwyczaj sprzeciw jest taki jak w kodzie
nie możesz powiedzieć, która
func()
jest wywoływana. Można to rozwiązać na kilka sposobów:int main() { (string)func(); }
.Dwa z języków, w których regularnie ( ab ) używam przeciążenia według typu zwrotu: Perl i Haskell . Pozwól mi opisać, co robią.
W Perlu istnieje podstawowa różnica między skalarem a kontekstem listy (i innymi, ale będziemy udawać, że są dwa). Każda wbudowana funkcja w Perlu może robić różne rzeczy w zależności od kontekstu, w którym jest wywoływana. Na przykład
join
operator wymusza kontekst listy (na łączonej rzeczy), podczas gdyscalar
operator wymusza kontekst skalarny, więc porównaj:Każdy operator w Perlu robi coś w kontekście skalarnym i coś w kontekście listy, i mogą być różne, jak pokazano. (Nie dotyczy to tylko losowych operatorów, takich jak
localtime
. Jeśli używasz tablicy@a
w kontekście listy, zwraca tablicę, podczas gdy w kontekście skalarnym zwraca liczbę elementów. Na przykładprint @a
drukuje elementy, podczas gdyprint 0+@a
drukuje rozmiar. ) Ponadto każdy operator może wymusić kontekst, np. Dodanie+
wymusza kontekst skalarny. Każdy wpis wman perlfunc
tym dokumentuje. Na przykład tutaj jest część wpisu dotyczącegoglob EXPR
:Jaki jest związek między listą a kontekstem skalarnym? Cóż,
man perlfunc
mówiwięc nie jest to prosta kwestia posiadania jednej funkcji, a następnie wykonujesz prostą konwersję na końcu. Z tego powodu wybrałem
localtime
przykład.Nie tylko wbudowane mają takie zachowanie. Każdy użytkownik może zdefiniować taką funkcję za pomocą
wantarray
, która pozwala rozróżnić kontekst listowy, skalarny i void. Na przykład możesz zdecydować, że nic nie zrobisz, jeśli zostaniesz wezwany w pustym kontekście.Teraz możesz narzekać, że nie jest to prawdziwe przeciążenie wartością zwracaną, ponieważ masz tylko jedną funkcję, która jest informowana o kontekście, w którym jest wywoływana, a następnie działa na podstawie tych informacji. Jest to jednak wyraźnie równoważne (i analogiczne do tego, w jaki sposób Perl nie pozwala na zwykłe przeciążanie dosłownie, ale funkcja może po prostu zbadać swoje argumenty). Co więcej, dobrze rozwiązuje niejednoznaczną sytuację wspomnianą na początku tej odpowiedzi. Perl nie narzeka, że nie wie, którą metodę wywołać; to po prostu to nazywa. Wystarczy dowiedzieć się, w jakim kontekście została wywołana funkcja, co jest zawsze możliwe:
(Uwaga: czasami mogę powiedzieć operator Perla, gdy mam na myśli funkcję. Nie jest to kluczowe w tej dyskusji).
Haskell przyjmuje inne podejście, mianowicie nie wywoływać skutków ubocznych. Ma również silny system typów, dzięki czemu możesz pisać kod w następujący sposób:
Ten kod odczytuje liczbę zmiennoprzecinkową ze standardowego wejścia i wypisuje pierwiastek kwadratowy. Ale co jest w tym zaskakującego? Cóż, typ
readLn
jestreadLn :: Read a => IO a
. Oznacza to, że dla każdego typu, który może byćRead
(formalnie, każdy typ, który jest instancjąRead
klasy typu),readLn
może go odczytać. Skąd Haskell wiedział, że chcę odczytać liczbę zmiennoprzecinkową? No cóż, typsqrt
jestsqrt :: Floating a => a -> a
, co w gruncie rzeczy oznacza, żesqrt
akceptuje tylko liczby zmiennoprzecinkowe jako dane wejściowe, więc Haskell wywnioskował, czego chciałem.Co się stanie, gdy Haskell nie będzie mógł wywnioskować, czego chcę? Cóż, jest kilka możliwości. Jeśli w ogóle nie użyję wartości zwracanej, Haskell po prostu nie wywoła tej funkcji. Jednak gdybym zrobić użyj wartości zwracanej, a następnie Haskell będzie skarżyć, że nie można ustalić typ:
Mogę rozwiązać niejednoznaczność, określając żądany typ:
Tak czy inaczej, cała ta dyskusja oznacza, że przeciążenie wartością zwracaną jest możliwe i odbywa się, co odpowiada części twojego pytania.
Inną częścią twojego pytania jest to, dlaczego więcej języków tego nie robi. Pozwolę innym odpowiedzieć na to pytanie. Jednak kilka uwag: główną przyczyną jest prawdopodobnie to, że szansa na zamieszanie jest tutaj naprawdę większa niż w przypadku przeciążenia według typu argumentu. Możesz także spojrzeć na uzasadnienia z poszczególnych języków:
Ada : „Może się wydawać, że najprostszą regułą rozwiązywania przeciążenia jest wykorzystanie wszystkiego - wszystkich informacji z jak najszerszego kontekstu - do rozwiązania przeciążonego odwołania. Reguła może być prosta, ale nie jest pomocna. Wymaga od czytelnika skanować dowolnie duże fragmenty tekstu i dokonywać dowolnie złożonych wniosków (takich jak (g) powyżej). Uważamy, że lepsza reguła to taka, która wyraźnie określa zadanie, które musi wykonać czytelnik lub kompilator, i to sprawia, że to zadanie tak naturalne dla ludzkiego czytelnika, jak to możliwe. ”
C ++ (podsekcja 7.4.1 „Język programowania C ++” Bjarne Stroustrupa): „Typy zwrotów nie są uwzględniane w rozwiązywaniu przeciążeń. Powodem jest zachowanie rozdzielczości dla pojedynczego operatora lub wywołania funkcji niezależnie od kontekstu. Rozważ:
Gdyby wzięto pod uwagę typ zwracany, nie byłoby już możliwe oddzielne sprawdzenie wywołania
sqrt()
i określenie, która funkcja została wywołana. ”(Należy zauważyć, dla porównania, że w Haskell nie ma żadnych niejawnych konwersji).Java ( Java Language Specification 9.4.1 ): „Jedna z odziedziczonych metod musi zastępować typem zwracanym dla każdej innej odziedziczonej metody, w przeciwnym razie wystąpi błąd kompilacji”. (Tak, wiem, że to nie daje uzasadnienia. Jestem pewien, że uzasadnienie podane jest przez Goslinga w „Java Programming Language”. Może ktoś ma kopię? Założę się, że to w zasadzie „zasada najmniejszego zaskoczenia”. ) Jednak fajny fakt o Javie: JVM pozwala na przeładowanie przez wartość zwracaną! Jest to używane na przykład w Scali i można uzyskać do niego bezpośredni dostęp również przez Javę , grając z elementami wewnętrznymi.
PS. Na koniec, w rzeczywistości możliwe jest przeciążenie przez zwrócenie wartości w C ++ za pomocą lewy. Świadek:
źródło
Foo
iBar
obsługę dwukierunkową konwersję i zastosowań metody typuFoo
wewnętrznie, ale powraca typuBar
. Jeśli taka metoda zostanie wywołana przez kod, który natychmiast zmusi wynik do wpisaniaFoo
, użycieBar
typu return może działać, ale ta metodaFoo
byłaby lepsza. BTW, chciałbym również zobaczyć środki, za pomocą których ...var someVar = someMethod();
(lub też wyznaczyć, że jego zwrot nie powinien być używany w taki sposób). Na przykład rodzina typów, która implementuje płynny interfejs, możevar thing2 = thing1.WithX(3).WithY(5).WithZ(9);
odnieść korzyści z posiadania wersji modyfikowalnych i niezmiennych, więcWithX(3)
kopiowałabything1
do obiektu zmiennego, mutowała X i zwracała ten zmienny obiekt;WithY(5)
mutuje Y i zwraca ten sam obiekt; podobnie `WithZ (9). Następnie przydział przekształciłby się w niezmienny typ.Jeśli funkcje były przeciążone przez typ zwracany i wystąpiły te dwa przeciążenia
kompilator nie może dowiedzieć się, które z tych dwóch funkcji wywołać po zobaczeniu takiego połączenia
Z tego powodu projektanci języków często nie dopuszczają do przeciążania wartości zwrotu.
Niektóre języki (takie jak MSIL), ale nie pozwalają przeciążenie przez typ zwracany. Oni również napotykają powyższe trudności, ale mają obejścia, dla których będziesz musiał zapoznać się z ich dokumentacją.
źródło
W takim języku, w jaki sposób rozwiązałbyś następujące kwestie:
jeśli
f
miał przeciążeńvoid f(int)
ivoid f(string)
ig
miał przeciążeńint g(int)
istring g(int)
? Potrzebowałbyś pewnego rodzaju disambiguator.Myślę, że sytuacjom, w których możesz tego potrzebować, lepiej byłoby obsłużyć, wybierając nową nazwę dla funkcji.
źródło
Aby ukraść odpowiedź specyficzną dla C ++ z innego bardzo podobnego pytania (dupe?):
Typy zwracanych funkcji nie wchodzą w grę w rozdzielczości przeciążenia tylko dlatego, że Stroustrup (zakładam, że z danych wejściowych innych architektów C ++) chciał, aby rozdzielczość przeciążenia była „niezależna od kontekstu”. Patrz 7.4.1 - „Przeciążenie i typ zwrotu” z „C ++ Programming Language, Third Edition”.
Chcieli, aby opierało się to tylko na tym, jak wywołano przeciążenie - a nie na tym, jak wykorzystano wynik (jeśli w ogóle został wykorzystany). Rzeczywiście, wiele funkcji jest wywoływanych bez użycia wyniku lub wynik byłby użyty jako część większego wyrażenia. Jednym z czynników, który jestem pewien, że wszedł do gry, gdy zdecydowali, że jest to, że jeśli typ zwracany był częścią rozdzielczości, byłoby wiele wywołań przeciążonych funkcji, które musiałyby zostać rozwiązane przy użyciu skomplikowanych reguł lub musiałyby mieć rzut kompilatora błąd polegający na tym, że połączenie było niejednoznaczne.
I, Pan wie, rozdzielczość przeciążenia C ++ jest wystarczająco złożona, ponieważ stoi ...
źródło
W haskell jest to możliwe, nawet jeśli nie ma przeciążenia funkcji. Haskell używa klas typów. W programie można było zobaczyć:
Samo przeciążanie funkcji nie jest tak popularne. Najczęściej używane języki to C ++, być może Java i / lub C #. We wszystkich dynamicznych językach jest to skrót od:
Dlatego nie ma to większego sensu. Większość ludzi nie jest zainteresowana tym, czy język może pomóc upuścić jedną linię w każdym miejscu, w którym go używasz.
Dopasowywanie wzorców jest nieco podobne do przeciążania funkcji i myślę, że czasami działają podobnie. Nie jest to jednak powszechne, ponieważ jest przydatne tylko dla kilku programów i jest trudne do wdrożenia w większości języków.
Widzisz, istnieje nieskończenie wiele innych łatwiejszych do zaimplementowania funkcji do zaimplementowania w języku, w tym:
źródło
Dobre odpowiedzi! Odpowiedź A.Rex jest bardzo szczegółowa i pouczająca. Jak sam zaznacza, C ++ ma rozważyć operatorów typu konwersji dostarczone przez użytkownika podczas kompilacji
lhs = func();
(gdzie func jest naprawdę nazwa struktury) . Moje obejście jest trochę inne - nie lepsze, po prostu inne (chociaż opiera się na tym samym podstawowym pomyśle).Podczas gdy chciałem napisać ...
Skończyło się na rozwiązaniu, które wykorzystuje sparametryzowaną strukturę (z T = typ zwracany):
Zaletą tego rozwiązania jest to, że każdy kod, który zawiera te definicje szablonów, może dodać więcej specjalizacji dla większej liczby typów. W razie potrzeby możesz także wykonywać częściowe specjalizacje struktury. Na przykład, jeśli chcesz specjalnej obsługi typów wskaźników:
Negatywnie nie możesz pisać za
int x = func();
pomocą mojego rozwiązania. Musisz pisaćint x = func<int>();
. Musisz wyraźnie powiedzieć, jaki jest typ zwracany, zamiast prosić kompilator o wyszukanie go, patrząc na operatory konwersji typów. Powiedziałbym, że zarówno „moje” rozwiązanie, jak i A.Rex należą do pareto-optymalnego przodu sposobów rozwiązania tego dylematu C ++ :)źródło
jeśli chcesz przeciążać metody różnymi typami zwracanymi, po prostu dodaj fikcyjny parametr z wartością domyślną, aby umożliwić wykonanie przeciążenia, ale nie zapominaj, że typ parametru powinien być inny, więc logika przeciążania działa dalej to np. na delphi:
użyj tego w ten sposób
źródło
Jak już pokazano - niejednoznaczne wywołania funkcji, która różni się tylko typem zwracanym, wprowadzają niejednoznaczność. Niejednoznaczność powoduje wadliwy kod. Wadliwego kodu należy unikać.
Złożoność wynikająca z próby niejednoznaczności pokazuje, że nie jest to dobry hack. Oprócz ćwiczenia intelektualnego - dlaczego nie zastosować procedur z parametrami odniesienia.
źródło
doing(thisVery(deeplyNested(), andOften(butNotAlways()), notReally()), goodCode());
ta funkcja przeciążania nie jest trudna do zarządzania, jeśli spojrzysz na nią w nieco inny sposób. rozważ następujące,
jeśli język zwróci przeciążenie, pozwoli to na przeładowanie parametrów, ale nie na powielanie. rozwiązałoby to problem:
ponieważ jest tylko jeden f (int wybór) do wyboru.
źródło
W .NET czasami używamy jednego parametru do wskazania pożądanego wyniku ogólnego wyniku, a następnie dokonaliśmy konwersji, aby uzyskać to, czego oczekujemy.
DO#
Może ten przykład też może pomóc:
C ++
źródło
Dla przypomnienia , Octave zezwala na inny wynik w zależności od tego, czy zwracany element jest skalarny w porównaniu do tablicy.
Por. Także rozkład wartości pojedynczej .
źródło
Ten jest nieco inny dla C ++; Nie wiem, czy byłoby to uważane za przeciążenie bezpośrednio przez typ zwrotu. Jest to bardziej specjalizacja szablonowa, która działa w sposób.
util.h
util.inl
util.cpp
W tym przykładzie nie do końca używana jest funkcja przeciążenia funkcji według typu zwracanego, jednak ta nie obiektowa klasa c ++ używa specjalizacji szablonu do symulacji rozdzielczości przeciążenia funkcji według typu zwracanego za pomocą prywatnej metody statycznej.
Każda z
convertToType
funkcji wywołuje szablon funkcji,stringToValue()
a jeśli spojrzysz na szczegóły implementacji lub algorytm tego szablonu funkcji, wywołujegetValue<T>( param, param )
on, a następnie zwraca typT
i zapamiętuje go jakoT*
przekazany dostringToValue()
szablonu funkcji jako jeden z jego parametrów .Inne niż coś takiego; C ++ tak naprawdę nie ma mechanizmu, który ma rozdzielczość przeciążania funkcji według typu zwracanego. Mogą istnieć inne konstrukcje lub mechanizmy, o których nie wiem, które mogą symulować rozdzielczość według typu zwracanego.
źródło
Myślę, że jest to luka w nowoczesnej definicji C ++… dlaczego?
Dlaczego kompilator C ++ nie może zgłosić błędu w przykładzie „3” i zaakceptować kod w przykładzie „1 + 2”?
źródło
Większość języków statycznych obsługuje teraz również generyczne, które rozwiązałyby twój problem. Jak wspomniano wcześniej, bez różnic parametrów nie ma sposobu, aby sprawdzić, który z nich należy wywołać. Więc jeśli chcesz to zrobić, po prostu użyj ogólnych i nazwij to dzień.
źródło