Im Gegensatz zu dem, was andere sagen, Überlastung durch Rückgabetyp ist möglich und wird von einigen modernen Sprachen gemacht. Der übliche Einwand ist, dass in Code wie
int func();
string func();
int main() { func(); }
Sie können nicht sagen, welche func()
aufgerufen wird. Dies kann auf verschiedene Arten gelöst werden:
- Haben Sie eine vorhersehbare Methode, um zu bestimmen, welche Funktion in einer solchen Situation aufgerufen wird.
- Immer wenn eine solche Situation auftritt, handelt es sich um einen Fehler bei der Kompilierung. Haben Sie jedoch eine Syntax, die es dem Programmierer ermöglicht, zu unterscheiden, z
int main() { (string)func(); }
.
- Keine Nebenwirkungen haben. Wenn Sie keine Nebenwirkungen haben und niemals den Rückgabewert einer Funktion verwenden, kann der Compiler vermeiden, die Funktion überhaupt aufzurufen.
Zwei der Sprachen, in denen ich regelmäßig ( ab ) Überladung nach Rückgabetyp verwende: Perl und Haskell . Lassen Sie mich beschreiben, was sie tun.
In Perl gibt es einen grundlegenden Unterschied zwischen skalaren und Liste Kontext (und anderen, aber wir werden es zwei vorgeben ist). Jede in Perl integrierte Funktion kann je nach Kontext unterschiedliche Funktionen ausführen . Beispielsweise join
erzwingt der Operator den Listenkontext (für das zu verbindende Objekt), während der scalar
Operator den skalaren Kontext erzwingt. Vergleichen Sie also:
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.
Jeder Operator in Perl führt etwas im skalaren Kontext und etwas im Listenkontext aus, und sie können, wie dargestellt, unterschiedlich sein. (Dies gilt nicht nur für zufällige Operatoren wie localtime
. Wenn Sie ein Array @a
im Listenkontext verwenden, wird das Array zurückgegeben, während im skalaren Kontext die Anzahl der Elemente zurückgegeben wird. So werden beispielsweise print @a
die Elemente ausgedruckt, während print 0+@a
die Größe gedruckt wird. ) Darüber hinaus kann jeder Operator einen Kontext erzwingen , z. B. Addition +
erzwingt skalaren Kontext. Jeder Eintrag in man perlfunc
dokumentiert dies. Hier ist zum Beispiel ein Teil des Eintrags für glob EXPR
:
Gibt im Listenkontext eine (möglicherweise leere) Liste von Dateinamenerweiterungen für den Wert von zurück, EXPR
wie dies die Standard-Unix-Shell /bin/csh
tun würde. Im skalaren Kontext durchläuft glob solche Dateinamenerweiterungen und gibt undef zurück, wenn die Liste erschöpft ist.
Wie ist nun die Beziehung zwischen Liste und skalarem Kontext? Nun, man perlfunc
sagt
Beachten Sie die folgende wichtige Regel: Es gibt keine Regel, die das Verhalten eines Ausdrucks im Listenkontext mit seinem Verhalten im skalaren Kontext in Beziehung setzt oder umgekehrt. Es könnte zwei völlig verschiedene Dinge tun. Jeder Operator und jede Funktion entscheidet, welche Art von Wert im skalaren Kontext am besten zurückgegeben werden soll. Einige Operatoren geben die Länge der Liste zurück, die im Listenkontext zurückgegeben worden wäre. Einige Operatoren geben den ersten Wert in der Liste zurück. Einige Operatoren geben den letzten Wert in der Liste zurück. Einige Bediener geben eine Anzahl erfolgreicher Vorgänge zurück. Im Allgemeinen tun sie, was Sie wollen, es sei denn, Sie wollen Konsistenz.
Es ist also nicht einfach, eine einzige Funktion zu haben, und am Ende führen Sie eine einfache Konvertierung durch. Tatsächlich habe ich das localtime
Beispiel aus diesem Grund gewählt.
Es sind nicht nur die integrierten Funktionen, die dieses Verhalten aufweisen. Jeder Benutzer kann eine solche Funktion mit definieren wantarray
, mit der Sie zwischen Listen-, Skalar- und Leerkontext unterscheiden können. So können Sie beispielsweise entscheiden, nichts zu tun, wenn Sie im ungültigen Kontext aufgerufen werden.
Jetzt können Sie sich beschweren, dass dies keine echte Überladung durch den Rückgabewert ist, da Sie nur eine Funktion haben, der der Kontext mitgeteilt wird, in dem sie aufgerufen wird, und dann auf diese Informationen reagiert. Dies ist jedoch eindeutig gleichwertig (und analog dazu, dass Perl die übliche Überladung nicht buchstäblich zulässt, sondern eine Funktion nur ihre Argumente untersuchen kann). Darüber hinaus wird die zu Beginn dieser Antwort erwähnte zweideutige Situation gut gelöst. Perl beschwert sich nicht darüber, dass es nicht weiß, welche Methode es aufrufen soll. es nennt es einfach. Alles, was es tun muss, ist herauszufinden, in welchem Kontext die Funktion aufgerufen wurde, was immer möglich ist:
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"
(Hinweis: Ich kann manchmal Perl-Operator sagen, wenn ich Funktion meine. Dies ist für diese Diskussion nicht entscheidend.)
Haskell verfolgt den anderen Ansatz, nämlich keine Nebenwirkungen zu haben. Es hat auch ein starkes Typsystem, so dass Sie Code wie folgt schreiben können:
main = do n <- readLn
print (sqrt n) -- note that this is aligned below the n, if you care to run this
Dieser Code liest eine Gleitkommazahl aus der Standardeingabe und druckt ihre Quadratwurzel. Aber was ist daran überraschend? Nun, die Art von readLn
ist readLn :: Read a => IO a
. Dies bedeutet, dass jeder Typ, der sein kann Read
(formal jeder Typ, der eine Instanz der Read
Typklasse ist), readLn
ihn lesen kann. Woher wusste Haskell, dass ich eine Gleitkommazahl lesen wollte? Nun, die Art von sqrt
ist sqrt :: Floating a => a -> a
, was im Wesentlichen bedeutet, dass sqrt
nur Gleitkommazahlen als Eingaben akzeptiert werden können, und so folgerte Haskell, was ich wollte.
Was passiert, wenn Haskell nicht ableiten kann, was ich will? Nun, es gibt ein paar Möglichkeiten. Wenn ich den Rückgabewert überhaupt nicht verwende, ruft Haskell die Funktion einfach gar nicht erst auf. Wenn ich jedoch den Rückgabewert verwende, beschwert sich Haskell, dass der Typ nicht abgeleitet werden kann:
main = do n <- readLn
print n
-- this program results in a compile-time error "Unresolved top-level overloading"
Ich kann die Mehrdeutigkeit beheben, indem ich den gewünschten Typ spezifiziere:
main = do n <- readLn
print (n::Int)
-- this compiles (and does what I want)
Wie auch immer, diese ganze Diskussion bedeutet, dass eine Überladung durch den Rückgabewert möglich ist und durchgeführt wird, was einen Teil Ihrer Frage beantwortet.
Der andere Teil Ihrer Frage ist, warum mehr Sprachen dies nicht tun. Ich werde andere das beantworten lassen. Ein paar Anmerkungen: Der Hauptgrund ist wahrscheinlich, dass die Verwechslungsgefahr hier wirklich größer ist als bei der Überladung nach Argumenttyp. Sie können auch Rationales aus einzelnen Sprachen betrachten:
Ada : "Es scheint, dass die einfachste Überlastungsauflösungsregel darin besteht, alles - alle Informationen aus einem möglichst breiten Kontext - zu verwenden, um die überladene Referenz aufzulösen. Diese Regel mag einfach sein, ist aber nicht hilfreich. Sie erfordert den menschlichen Leser Wir glauben, dass eine bessere Regel eine ist, die die Aufgabe, die ein menschlicher Leser oder ein Compiler ausführen muss, explizit macht, und die diese Aufgabe macht, um beliebig große Textstücke zu scannen und willkürlich komplexe Schlussfolgerungen zu ziehen (wie (g) oben) so natürlich wie möglich für den menschlichen Leser. "
C ++ (Unterabschnitt 7.4.1 von Bjarne Stroustrups "The C ++ Programming Language"): "Rückgabetypen werden bei der Überlastungsauflösung nicht berücksichtigt. Der Grund besteht darin, die Auflösung für einen einzelnen Operator oder Funktionsaufruf kontextunabhängig zu halten. Beachten Sie:
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)
}
Wenn der Rückgabetyp berücksichtigt würde, wäre es nicht mehr möglich, einen Aufruf von sqrt()
isoliert zu betrachten und festzustellen, welche Funktion aufgerufen wurde. "(Beachten Sie zum Vergleich, dass es in Haskell keine impliziten Konvertierungen gibt.)
Java ( Java Language Specification 9.4.1 ): "Eine der geerbten Methoden muss für jede andere geerbte Methode durch einen Rückgabetyp ersetzt werden können. Andernfalls tritt ein Fehler bei der Kompilierung auf." (Ja, ich weiß, dass dies keine Begründung gibt. Ich bin sicher, dass die Begründung von Gosling in "der Java-Programmiersprache" gegeben wird. Vielleicht hat jemand eine Kopie? Ich wette, es ist im Wesentlichen das "Prinzip der geringsten Überraschung". ) Eine lustige Tatsache über Java: Die JVM ermöglicht das Überladen durch den Rückgabewert! Dies wird beispielsweise in Scala verwendet und kann auch direkt über Java aufgerufen werden, indem mit Interna herumgespielt wird.
PS. Abschließend ist es tatsächlich möglich, mit einem Trick den Rückgabewert in C ++ zu überladen. Zeuge:
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
}