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
int func();
string func();
int main() { func(); }
nie możesz powiedzieć, która func()
jest wywoływana. Można to rozwiązać na kilka sposobów:
- Mieć przewidywalną metodę określania, która funkcja jest wywoływana w takiej sytuacji.
- Ilekroć taka sytuacja ma miejsce, jest to błąd czasu kompilacji. Jednak mają składnię, która pozwala programiście ujednoznacznić, np
int main() { (string)func(); }
.
- Nie ma skutków ubocznych. Jeśli nie masz efektów ubocznych i nigdy nie używasz zwracanej wartości funkcji, kompilator może w ogóle uniknąć wywołania funkcji w pierwszej kolejności.
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 gdy scalar
operator wymusza kontekst skalarny, więc porównaj:
print join " ", localtime(); # printed "58 11 2 14 0 109 3 13 0" for me right now
print scalar localtime(); # printed "Wed Jan 14 02:12:44 2009" for me right now.
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ład print @a
drukuje elementy, podczas gdy print 0+@a
drukuje rozmiar. ) Ponadto każdy operator może wymusić kontekst, np. Dodanie +
wymusza kontekst skalarny. Każdy wpis w man perlfunc
tym dokumentuje. Na przykład tutaj jest część wpisu dotyczącego glob EXPR
:
W kontekście listy zwraca (ewentualnie pustą) listę rozszerzeń nazw plików na wartość, jaką zrobiłaby EXPR
standardowa powłoka uniksowa /bin/csh
. W kontekście skalarnym glob iteruje przez takie rozszerzenia nazw plików, zwracając undef po wyczerpaniu listy.
Jaki jest związek między listą a kontekstem skalarnym? Cóż, man perlfunc
mówi
Zapamiętaj następującą ważną zasadę: Nie ma reguły, która wiązałaby zachowanie wyrażenia w kontekście listy z jego zachowaniem w kontekście skalarnym lub odwrotnie. Może to zrobić dwie zupełnie różne rzeczy. Każdy operator i funkcja decyduje, jaki rodzaj wartości najlepiej byłoby zwrócić w kontekście skalarnym. Niektórzy operatorzy zwracają długość listy, która zostałaby zwrócona w kontekście listy. Niektórzy operatorzy zwracają pierwszą wartość z listy. Niektórzy operatorzy zwracają ostatnią wartość z listy. Niektórzy operatorzy zwracają liczbę udanych operacji. Ogólnie rzecz biorąc, robią to, co chcesz, chyba że chcesz spójności.
wię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:
sub func {
if( not defined wantarray ) {
print "void\n";
} elsif( wantarray ) {
print "list\n";
} else {
print "scalar\n";
}
}
func(); # prints "void"
() = func(); # prints "list"
0+func(); # prints "scalar"
(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:
main = do n <- readLn
print (sqrt n) -- note that this is aligned below the n, if you care to run this
Ten kod odczytuje liczbę zmiennoprzecinkową ze standardowego wejścia i wypisuje pierwiastek kwadratowy. Ale co jest w tym zaskakującego? Cóż, typ readLn
jest readLn :: 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óż, typ sqrt
jest sqrt :: Floating a => a -> a
, co w gruncie rzeczy oznacza, że sqrt
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:
main = do n <- readLn
print n
-- this program results in a compile-time error "Unresolved top-level overloading"
Mogę rozwiązać niejednoznaczność, określając żądany typ:
main = do n <- readLn
print (n::Int)
-- this compiles (and does what I want)
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ż:
float sqrt(float);
double sqrt(double);
void f(double da, float fla)
{
float fl = sqrt(da); // call sqrt(double)
double d = sqrt(da); // call sqrt(double)
fl = sqrt(fla); // call sqrt(float)
d = sqrt(fla); // call sqrt(float)
}
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:
struct func {
operator string() { return "1";}
operator int() { return 2; }
};
int main( ) {
int x = func(); // calls int version
string y = func(); // calls string version
double d = func(); // calls int version
cout << func() << endl; // calls int version
func(); // calls neither
}