Contrairement à ce que d'autres disent, la surcharge par type de retour es possible et es fait par certaines langues modernes. L'objection habituelle est que dans un code comme
int func();
string func();
int main() { func(); }
vous ne pouvez pas dire lequel func()
Ce problème peut être résolu de plusieurs façons :
- Disposer d'une méthode prévisible pour déterminer quelle fonction est appelée dans une telle situation.
- Lorsqu'une telle situation se produit, il s'agit d'une erreur de compilation. Cependant, il faut avoir une syntaxe qui permette au programmeur de désambiguïser, par exemple
int main() { (string)func(); }
.
- Je n'ai pas d'effets secondaires. Si vous n'avez pas d'effets secondaires et que vous n'utilisez jamais la valeur de retour d'une fonction, le compilateur peut éviter d'appeler la fonction en premier lieu.
Deux des langues que je pratique régulièrement ( ab )utiliser la surcharge par type de retour : Perl y Haskell . Laissez-moi vous décrire ce qu'ils font.
Sur Perl il existe une distinction fondamentale entre scalaire y liste (et d'autres, mais nous ferons comme s'il y en avait deux). Chaque fonction intégrée en Perl peut faire des choses différentes en fonction du contexte contexte dans lequel il est appelé. Par exemple, le join
force le contexte de liste (sur la chose jointe) alors que l'opérateur scalar
L'opérateur force le contexte scalaire, donc compare :
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.
Chaque opérateur en Perl fait quelque chose dans un contexte scalaire et quelque chose dans un contexte de liste, et ils peuvent être différents, comme illustré. (Ce n'est pas seulement pour les opérateurs aléatoires comme localtime
. Si vous utilisez un tableau @a
dans un contexte de liste, il renvoie le tableau, tandis que dans un contexte scalaire, il renvoie le nombre d'éléments. Ainsi, par exemple print @a
imprime les éléments, tandis que print 0+@a
imprime la taille). En outre, chaque opérateur peut force un contexte, par exemple l'addition +
force le contexte scalaire. Chaque entrée dans man perlfunc
documente ceci. Par exemple, voici une partie de l'entrée pour glob EXPR
:
Dans un contexte de liste, renvoie une liste (éventuellement vide) d'expansions de noms de fichiers sur la valeur de EXPR
comme l'interpréteur de commandes standard shell Unix /bin/csh
ferait. Dans contexte scalaire, glob itère à travers de telles expansions de nom de fichier, retournant undef lorsque la liste est épuisée.
Maintenant, quelle est la relation entre la liste et le contexte scalaire ? Et bien, man perlfunc
dit
N'oubliez pas la règle importante suivante : Il n'y a pas de règle qui relie le comportement d'une expression dans un à son comportement dans un contexte scalaire ou vice versa. Elle peut faire deux choses totalement différentes. Chaque opérateur et fonction décide quelle type de valeur qu'il serait le plus appropriée de retourner dans un scalaire. Certains opérateurs renvoient la longueur de la liste qui aurait été retournée dans un contexte de liste. Certains opérateurs opérateurs renvoient la première valeur de la la liste. Certains opérateurs renvoient la dernière valeur de la liste. Certains opérateurs opérateurs renvoient un compte des opérations opérations réussies. En général, ils font ce que ce que vous voulez, sauf si vous voulez de la cohérence.
Il ne s'agit donc pas simplement d'avoir une seule fonction, puis de faire une simple conversion à la fin. En fait, j'ai choisi la localtime
exemple pour cette raison.
Ce n'est pas seulement les built-ins qui ont ce comportement. Tout utilisateur peut définir une telle fonction en utilisant wantarray
qui permet de faire la distinction entre les contextes list, scalaire et void. Ainsi, par exemple, vous pouvez décider de ne rien faire si vous êtes appelé dans un contexte void.
Maintenant, vous pouvez vous plaindre que ce n'est pas vrai la surcharge par valeur de retour parce que vous n'avez qu'une seule fonction, qui est informée du contexte dans lequel elle est appelée et qui agit ensuite sur cette information. Cependant, c'est clairement équivalent (et analogue à la façon dont Perl ne permet pas la surcharge habituelle littéralement, mais une fonction peut simplement examiner ses arguments). De plus, cela résout joliment la situation ambiguë mentionnée au début de cette réponse. Perl ne se plaint pas de ne pas savoir quelle méthode appeler ; il l'appelle tout simplement. Tout ce qu'il lui reste à faire est de déterminer dans quel contexte la fonction a été appelée, ce qui est toujours possible :
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"
(Note : il se peut que je dise parfois opérateur Perl alors que je veux dire fonction. Ce n'est pas crucial pour cette discussion).
Haskell adopte l'autre approche, à savoir ne pas avoir d'effets secondaires. Il dispose également d'un système de types fort, ce qui vous permet d'écrire du code comme le suivant :
main = do n <- readLn
print (sqrt n) -- note that this is aligned below the n, if you care to run this
Ce code lit un nombre à virgule flottante à partir de l'entrée standard, et imprime sa racine carrée. Mais qu'est-ce qui est surprenant dans tout cela ? Eh bien, le type de readLn
es readLn :: Read a => IO a
. Ce que cela signifie, c'est que pour tout type qui peut être Read
(formellement, chaque type qui est une instance de la classe Read
classe de type), readLn
peut le lire. Comment Haskell a-t-il su que je voulais lire un nombre à virgule flottante ? Eh bien, le type de sqrt
es sqrt :: Floating a => a -> a
ce qui signifie essentiellement que sqrt
ne peut accepter que des nombres à virgule flottante comme entrées, et donc Haskell a déduit ce que je voulais.
Que se passe-t-il lorsque Haskell ne peut pas déduire ce que je veux ? Eh bien, il y a plusieurs possibilités. Si je n'utilise pas du tout la valeur de retour, Haskell n'appellera tout simplement pas la fonction en premier lieu. Cependant, si je faire utiliser la valeur de retour, alors Haskell se plaindra qu'il ne peut pas déduire le type :
main = do n <- readLn
print n
-- this program results in a compile-time error "Unresolved top-level overloading"
Je peux résoudre l'ambiguïté en spécifiant le type que je veux :
main = do n <- readLn
print (n::Int)
-- this compiles (and does what I want)
Quoi qu'il en soit, ce que toute cette discussion signifie, c'est que la surcharge par valeur de retour est possible et se fait, ce qui répond à une partie de votre question.
L'autre partie de votre question est de savoir pourquoi plus de langues ne le font pas. Je laisse à d'autres le soin d'y répondre. Cependant, quelques commentaires : la raison principale est probablement que la possibilité de confusion est vraiment plus grande ici que dans la surcharge par type d'argument. Vous pouvez également consulter les justifications des différentes langues :
Ada : " Il pourrait sembler que la règle de résolution des surcharges la plus simple consiste à tout utiliser - toutes les informations provenant d'un contexte aussi large que possible - pour résoudre la référence surchargée. Cette règle est peut-être simple, mais elle n'est pas utile. Elle oblige le lecteur humain à parcourir des morceaux de texte arbitrairement grands et à faire des déductions arbitrairement complexes (comme (g) ci-dessus). Nous pensons qu'une meilleure règle est celle qui rend explicite la tâche qu'un lecteur humain ou un compilateur doit effectuer, et qui rend cette tâche aussi naturelle que possible pour le lecteur humain."
C++ (sous-section 7.4.1 de "The C++ Programming Language" de Bjarne Stroustrup) : "Les types de retour ne sont pas pris en compte dans la résolution des surcharges. La raison est de garder la résolution pour un opérateur individuel ou un appel de fonction indépendant du contexte. Pensez-y :
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)
}
Si le type de retour était pris en compte, il ne serait plus possible de regarder un appel de sqrt()
en isolation et déterminer quelle fonction a été appelée." (Notez, à titre de comparaison, qu'en Haskell, il n'y a pas de fonction implicite conversions).
Java ( Spécification du langage Java 9.4.1 ) : "L'une des méthodes héritées doit être return-type-substituable pour chaque autre méthode héritée, sinon une erreur de compilation se produit." (Oui, je sais que cela ne donne pas de justification. Je suis sûr que le raisonnement est donné par Gosling dans "the Java Programming Language". Peut-être que quelqu'un en a une copie ? Je parie que c'est le "principe de moindre surprise" en substance). Cependant, un fait amusant à propos de Java : la JVM permet à surcharge par valeur de retour ! Ceci est utilisé, par exemple, dans Scala et peut être consulté directement par le biais de Java aussi bien en jouant avec les internes.
PS. Pour finir, il est en fait possible de surcharger par valeur de retour en C++ avec une astuce. Témoin :
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
}
2 votes
Duplication possible de Surchargez une fonction C++ en fonction de la valeur de retour.