Warum unterstützen nicht mehr statisch typisierte Mainstream-Sprachen das Überladen von Funktionen / Methoden nach Rückgabetyp? Mir fällt nichts ein. Es scheint nicht weniger nützlich oder vernünftig zu sein, als die Überladung nach Parametertyp zu unterstützen. Wie kommt es, dass es so viel weniger beliebt ist?
252
Antworten:
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
Sie können nicht sagen, welche
func()
aufgerufen wird. Dies kann auf verschiedene Arten gelöst werden:int main() { (string)func(); }
.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 derscalar
Operator den skalaren Kontext erzwingt. Vergleichen Sie also: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 beispielsweiseprint @a
die Elemente ausgedruckt, währendprint 0+@a
die Größe gedruckt wird. ) Darüber hinaus kann jeder Operator einen Kontext erzwingen , z. B. Addition+
erzwingt skalaren Kontext. Jeder Eintrag inman perlfunc
dokumentiert dies. Hier ist zum Beispiel ein Teil des Eintrags fürglob EXPR
:Wie ist nun die Beziehung zwischen Liste und skalarem Kontext? Nun,
man perlfunc
sagtEs 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:
(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:
Dieser Code liest eine Gleitkommazahl aus der Standardeingabe und druckt ihre Quadratwurzel. Aber was ist daran überraschend? Nun, die Art von
readLn
istreadLn :: Read a => IO a
. Dies bedeutet, dass jeder Typ, der sein kannRead
(formal jeder Typ, der eine Instanz derRead
Typklasse ist),readLn
ihn lesen kann. Woher wusste Haskell, dass ich eine Gleitkommazahl lesen wollte? Nun, die Art vonsqrt
istsqrt :: Floating a => a -> a
, was im Wesentlichen bedeutet, dasssqrt
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:
Ich kann die Mehrdeutigkeit beheben, indem ich den gewünschten Typ spezifiziere:
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:
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:
quelle
Foo
undBar
unterstützen die bidirektionale Konvertierung, und eine Methode verwendet den TypFoo
intern, gibt jedoch den Typ zurückBar
. Wenn eine solche Methode von einem Code aufgerufen wird, der das Ergebnis sofort zum Typ zwingt, funktioniert möglicherweise dieFoo
Verwendung desBar
Rückgabetyps, aber derFoo
eine wäre besser. Übrigens, ich würde auch gerne ein Mittel sehen, mit dem ...var someVar = someMethod();
(oder festlegen, dass seine Rückgabe nicht auf diese Weise verwendet werden soll). Beispielsweise könnte eine Typenfamilie, die eine Fluent-Schnittstelle implementiert, von veränderlichen und unveränderlichen Versionen profitieren. Sievar thing2 = thing1.WithX(3).WithY(5).WithZ(9);
müsste also in ein veränderliches ObjektWithX(3)
kopierenthing1
, X mutieren und dieses veränderbare Objekt zurückgeben.WithY(5)
würde Y mutieren und dasselbe Objekt zurückgeben; ebenfalls `WithZ (9). Dann würde die Zuordnung in einen unveränderlichen Typ konvertiert.Wenn Funktionen durch den Rückgabetyp überladen wurden und Sie diese beiden Überladungen hatten
Der Compiler kann auf keinen Fall herausfinden, welche dieser beiden Funktionen er aufrufen soll, wenn er einen solchen Aufruf sieht
Aus diesem Grund verbieten Sprachdesigner häufig das Überladen von Rückgabewerten.
Einige Sprachen (wie MSIL), aber sie erlauben durch Rückgabetyp zu überlasten. Natürlich sind auch sie mit der oben genannten Schwierigkeit konfrontiert, aber es gibt Problemumgehungen, für die Sie ihre Dokumentation konsultieren müssen.
quelle
Wie würden Sie in einer solchen Sprache Folgendes lösen:
wenn
f
hatte Überlastungenvoid f(int)
undvoid f(string)
undg
hatte Überlastungenint g(int)
undstring g(int)
? Sie würden eine Art Disambiguator brauchen.Ich denke, die Situationen, in denen Sie dies benötigen könnten, wären besser zu bedienen, wenn Sie einen neuen Namen für die Funktion wählen.
quelle
So stehlen Sie eine C ++ - spezifische Antwort aus einer anderen sehr ähnlichen Frage (Dupe?):
Funktionsrückgabetypen spielen bei der Überlastungsauflösung keine Rolle, nur weil Stroustrup (ich nehme an, dass andere C ++ - Architekten Eingaben gemacht haben) wollte, dass die Überlastungsauflösung "kontextunabhängig" ist. Siehe 7.4.1 - "Überladen und Rückgabetyp" aus der "C ++ - Programmiersprache, dritte Ausgabe".
Sie wollten, dass es nur darauf basiert, wie die Überladung aufgerufen wurde - nicht darauf, wie das Ergebnis verwendet wurde (wenn es überhaupt verwendet wurde). In der Tat werden viele Funktionen aufgerufen, ohne das Ergebnis zu verwenden, oder das Ergebnis würde als Teil eines größeren Ausdrucks verwendet. Ein Faktor, von dem ich sicher bin, dass er ins Spiel kam, als sie entschieden, dass wenn der Rückgabetyp Teil der Auflösung wäre, es viele Aufrufe überladener Funktionen geben würde, die mit komplexen Regeln aufgelöst werden müssten oder den Compiler auslösen müssten Ein Fehler, dass der Aufruf nicht eindeutig war.
Und, Gott weiß, die C ++ - Überlastungsauflösung ist derzeit komplex genug ...
quelle
In haskell ist dies möglich, obwohl es keine Funktionsüberladung gibt. Haskell verwendet Typklassen. In einem Programm konnte man sehen:
Funktionsüberladung selbst ist nicht so beliebt. Die meisten Sprachen, die ich damit gesehen habe, sind C ++, vielleicht Java und / oder C #. In allen dynamischen Sprachen ist es eine Abkürzung für:
Deshalb macht es nicht viel Sinn. Die meisten Menschen sind nicht daran interessiert, ob die Sprache Ihnen dabei helfen kann, eine einzelne Zeile zu löschen, wo immer Sie sie verwenden.
Pattern Matching ähnelt in gewisser Weise der Funktionsüberladung, und ich denke, manchmal funktioniert es ähnlich. Dies ist jedoch nicht üblich, da es nur für wenige Programme nützlich und in den meisten Sprachen schwierig zu implementieren ist.
Sie sehen, es gibt unendlich viele andere, besser zu implementierende Funktionen, die in die Sprache implementiert werden können, darunter:
quelle
Gute Antworten! Insbesondere die Antwort von A.Rex ist sehr detailliert und lehrreich. Er weist darauf hin, C ++ nicht vom Benutzer bereitgestellte Typumwandlung Betreiber berücksichtigt bei der Kompilierung
lhs = func();
(wo func wirklich der Name einer Struktur ist) . Meine Problemumgehung ist etwas anders - nicht besser, nur anders (obwohl sie auf derselben Grundidee basiert).Während ich schreiben wollte ...
Am Ende hatte ich eine Lösung, die eine parametrisierte Struktur verwendet (mit T = dem Rückgabetyp):
Ein Vorteil dieser Lösung besteht darin, dass jeder Code, der diese Vorlagendefinitionen enthält, mehr Spezialisierungen für mehr Typen hinzufügen kann. Sie können bei Bedarf auch Teilspezialisierungen der Struktur vornehmen. Wenn Sie beispielsweise eine spezielle Behandlung für Zeigertypen wünschen:
Negativ kann man
int x = func();
mit meiner Lösung nicht schreiben . Du musst schreibenint x = func<int>();
. Sie müssen explizit angeben, um welchen Rückgabetyp es sich handelt, anstatt den Compiler zu bitten, ihn anhand der Typkonvertierungsoperatoren zu ermitteln. Ich würde sagen, dass "meine" Lösung und die von A.Rex beide zu einer paretooptimalen Front gehören , um dieses C ++ - Dilemma anzugehen :)quelle
Wenn Sie Methoden mit unterschiedlichen Rückgabetypen überladen möchten, fügen Sie einfach einen Dummy-Parameter mit dem Standardwert hinzu , um die Ausführung der Überladung zu ermöglichen. Vergessen Sie jedoch nicht, dass der Parametertyp unterschiedlich sein sollte, damit die Überladungslogik als Nächstes funktioniert.
benutze es so
quelle
Wie bereits gezeigt, führen mehrdeutige Aufrufe einer Funktion, die sich nur durch den Rückgabetyp unterscheidet, zu Mehrdeutigkeiten. Mehrdeutigkeit führt zu fehlerhaftem Code. Fehlerhafter Code muss vermieden werden.
Die Komplexität, die durch den Versuch der Mehrdeutigkeit verursacht wird, zeigt, dass dies kein guter Hack ist. Abgesehen von einer intellektuellen Übung - warum nicht Verfahren mit Referenzparametern verwenden?
quelle
doing(thisVery(deeplyNested(), andOften(butNotAlways()), notReally()), goodCode());
Diese Überladungsfunktion ist nicht schwer zu verwalten, wenn Sie sie etwas anders betrachten. Folgendes berücksichtigen,
Wenn eine Sprache eine Überladung zurückgeben würde, würde dies eine Überladung der Parameter ermöglichen, jedoch keine Duplikationen. Dies würde das Problem lösen von:
weil es nur ein f (int choice) zur Auswahl gibt.
quelle
In .NET verwenden wir manchmal einen Parameter, um die gewünschte Ausgabe eines generischen Ergebnisses anzugeben, und führen dann eine Konvertierung durch, um das zu erhalten, was wir erwarten.
C #
Vielleicht könnte auch dieses Beispiel helfen:
C ++
quelle
Für die Aufzeichnung erlaubt Octave unterschiedliche Ergebnisse, je nachdem, ob das Rückgabeelement skalar oder Array ist.
Vgl. Auch Singular Value Decomposition .
quelle
Dieser ist für C ++ etwas anders; Ich weiß nicht, ob es als direkte Überladung durch den Rückgabetyp angesehen werden würde. Es ist eher eine Vorlagenspezialisierung, die sich wie folgt verhält.
util.h
util.inl
util.cpp
In diesem Beispiel wird die Auflösung von Funktionsüberladungen nach Rückgabetyp nicht genau verwendet. Diese C ++ - Nichtobjektklasse verwendet jedoch eine Vorlagenspezialisierung, um die Auflösung von Funktionsüberlastungen nach Rückgabetyp mit einer privaten statischen Methode zu simulieren.
Jede der
convertToType
Funktionen ruft die Funktionsvorlage auf.stringToValue()
Wenn Sie sich die Implementierungsdetails oder den Algorithmus dieser Funktionsvorlage ansehen, ruft sie aufgetValue<T>( param, param )
und gibt einen Typ zurückT
und speichert ihn in einemT*
, derstringToValue()
als einer seiner Parameter an die Funktionsvorlage übergeben wird .Anders als so etwas; C ++ verfügt nicht wirklich über einen Mechanismus, mit dem die Auflösung von Funktionen nach Rückgabetyp überladen werden kann. Möglicherweise gibt es andere Konstrukte oder Mechanismen, von denen ich nicht weiß, dass sie die Auflösung nach Rückgabetyp simulieren könnten.
quelle
Ich denke, dies ist eine Lücke in der modernen C ++ - Definition… warum?
Warum kann ein C ++ - Compiler in Beispiel "3" keinen Fehler auslösen und den Code in Beispiel "1 + 2" akzeptieren?
quelle
Die meisten statischen Sprachen unterstützen jetzt auch Generika, die Ihr Problem lösen würden. Wie bereits erwähnt, gibt es ohne Parameterunterschiede keine Möglichkeit zu wissen, welche aufgerufen werden sollen. Wenn Sie dies tun möchten, verwenden Sie einfach Generika und nennen Sie es einen Tag.
quelle