Contrariamente a quanto dicono gli altri, il sovraccarico per tipo di ritorno è possibile ed è fatto da alcune lingue moderne. La solita obiezione è quella in codice come
int func();
string func();
int main() { func(); }
non puoi dire quale func()
viene chiamato. Questo può essere risolto in alcuni modi:
- Avere un metodo prevedibile per determinare quale funzione viene chiamata in tale situazione.
- Ogni volta che si verifica una situazione del genere, si tratta di un errore di compilazione. Tuttavia, avere una sintassi che consenta al programmatore di chiarire le ambiguità, ad es
int main() { (string)func(); }
.
- Non avere effetti collaterali. Se non si hanno effetti collaterali e non si utilizza mai il valore restituito di una funzione, il compilatore può evitare di chiamare la funzione in primo luogo.
Due delle lingue che uso regolarmente ( ab ) usano il sovraccarico per tipo di ritorno: Perl e Haskell . Lasciami descrivere cosa fanno.
In Perl , esiste una distinzione fondamentale tra contesto scalare e elenco (e altri, ma faremo finta che ce ne siano due). Ogni funzione integrata in Perl può fare cose diverse a seconda del contesto in cui viene chiamata. Ad esempio, l' join
operatore forza il contesto dell'elenco (sulla cosa da unire) mentre l' scalar
operatore forza il contesto scalare, quindi confronta:
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.
Ogni operatore in Perl fa qualcosa in un contesto scalare e qualcosa nel contesto di un elenco, e possono essere diversi, come illustrato. (Questo non è solo per operatori casuali come localtime
. Se si utilizza un array @a
nel contesto dell'elenco, restituisce l'array, mentre nel contesto scalare, restituisce il numero di elementi. Quindi, ad esempio, print @a
stampa gli elementi, mentre print 0+@a
stampa le dimensioni. ) Inoltre, ogni operatore può forzare un contesto, ad esempio +
forze addizionali per un contesto scalare. Ogni voce nei man perlfunc
documenti questo. Ad esempio, qui fa parte della voce relativa a glob EXPR
:
Nel contesto dell'elenco, restituisce un elenco (possibilmente vuoto) di espansioni di nomi di file sul valore di EXPR
una shell Unix standard /bin/csh
. In un contesto scalare, glob scorre attraverso tali espansioni di nomi di file, restituendo undef quando l'elenco è esaurito.
Ora, qual è la relazione tra elenco e contesto scalare? Bene, man perlfunc
dice
Ricorda la seguente regola importante: Non esiste una regola che collega il comportamento di un'espressione nel contesto di elenco al suo comportamento nel contesto scalare o viceversa. Potrebbe fare due cose totalmente diverse. Ogni operatore e funzione decide quale tipo di valore sarebbe più appropriato restituire in un contesto scalare. Alcuni operatori restituiscono la lunghezza dell'elenco che sarebbe stato restituito nel contesto dell'elenco. Alcuni operatori restituiscono il primo valore nell'elenco. Alcuni operatori restituiscono l'ultimo valore nell'elenco. Alcuni operatori restituiscono un numero di operazioni riuscite. In generale, fanno quello che vuoi, a meno che tu non voglia coerenza.
quindi non è una semplice questione di avere un'unica funzione, e alla fine fai una semplice conversione. In effetti, ho scelto l' localtime
esempio per quel motivo.
Non sono solo i built-in che hanno questo comportamento. Qualsiasi utente può definire tale funzione utilizzando wantarray
, che consente di distinguere tra elenco, scalare e contesto vuoto. Quindi, ad esempio, puoi decidere di non fare nulla se vieni chiamato in un contesto vuoto.
Ora, potresti lamentarti del fatto che questo non è un vero sovraccarico per valore restituito perché hai solo una funzione, a cui viene detto il contesto in cui viene chiamato e quindi agisce su tali informazioni. Tuttavia, questo è chiaramente equivalente (e analogo a come Perl non consenta letteralmente un sovraccarico normale, ma una funzione può semplicemente esaminare i suoi argomenti). Inoltre, risolve piacevolmente l'ambigua situazione menzionata all'inizio di questa risposta. Perl non si lamenta di non sapere quale metodo chiamare; lo chiama e basta. Tutto quello che deve fare è capire in quale contesto è stata chiamata la funzione, che è sempre possibile:
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"
(Nota: a volte posso dire operatore Perl quando intendo funzione. Questo non è cruciale per questa discussione.)
Haskell adotta l'altro approccio, vale a dire di non avere effetti collaterali. Ha anche un sistema di tipo forte, quindi puoi scrivere codice come il seguente:
main = do n <- readLn
print (sqrt n) -- note that this is aligned below the n, if you care to run this
Questo codice legge un numero in virgola mobile dall'input standard e stampa la sua radice quadrata. Ma cosa sorprende di questo? Bene, il tipo di readLn
è readLn :: Read a => IO a
. Ciò significa che per qualsiasi tipo che può essere Read
(formalmente, ogni tipo che è un'istanza della Read
classe type), readLn
può leggerlo. Come faceva Haskell a sapere che volevo leggere un numero in virgola mobile? Bene, il tipo di sqrt
è sqrt :: Floating a => a -> a
, che essenzialmente significa che sqrt
può accettare solo numeri in virgola mobile come input, e quindi Haskell ha dedotto ciò che volevo.
Cosa succede quando Haskell non può dedurre ciò che voglio? Bene, ci sono alcune possibilità. Se non uso affatto il valore restituito, Haskell semplicemente non chiamerà la funzione in primo luogo. Tuttavia, se mi faccio usare il valore di ritorno, allora Haskell si lamenta che non si può dedurre il tipo:
main = do n <- readLn
print n
-- this program results in a compile-time error "Unresolved top-level overloading"
Posso risolvere l'ambiguità specificando il tipo che desidero:
main = do n <- readLn
print (n::Int)
-- this compiles (and does what I want)
Comunque, ciò che significa tutta questa discussione è che il sovraccarico per valore di ritorno è possibile ed è fatto, il che risponde a una parte della tua domanda.
L'altra parte della tua domanda è perché più lingue non lo fanno. Lascerò che gli altri rispondano. Tuttavia, alcuni commenti: la ragione principale è probabilmente che l'opportunità di confusione è davvero maggiore qui che nel sovraccarico per tipo di argomento. Puoi anche guardare i razionali dalle singole lingue:
Ada : "Potrebbe sembrare che la regola di risoluzione del sovraccarico più semplice sia utilizzare tutto - tutte le informazioni da un contesto il più ampio possibile - per risolvere il riferimento sovraccarico. Questa regola può essere semplice, ma non è utile. Richiede il lettore umano per scansionare pezzi di testo arbitrariamente grandi e fare inferenze arbitrariamente complesse (come (g) sopra). Crediamo che una regola migliore sia quella che rende esplicito il compito che un lettore umano o un compilatore deve svolgere e che lo rende il più naturale possibile per il lettore umano ".
C ++ (sottosezione 7.4.1 di "Il linguaggio di programmazione C ++" di Bjarne Stroustrup): "I tipi di restituzione non sono considerati nella risoluzione di sovraccarico. Il motivo è mantenere la risoluzione per un singolo operatore o chiamata di funzione indipendente dal contesto. Considerare:
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)
}
Se si prendesse in considerazione il tipo di ritorno, non sarebbe più possibile esaminare una chiamata sqrt()
in isolamento e determinare quale funzione è stata chiamata. "(Nota, per confronto, che in Haskell non ci sono impliciti conversioni ).
Java ( Java Language Specification 9.4.1 ): "Uno dei metodi ereditati deve essere sostituibile con tipo restituito per ogni altro metodo ereditato, altrimenti si verifica un errore di compilazione." (Sì, lo so che questo non dà una logica. Sono sicuro che la logica è data da Gosling nel "linguaggio di programmazione Java". Forse qualcuno ne ha una copia? Scommetto che in sostanza è il "principio della minima sorpresa". ) Tuttavia, fatto divertente su Java: la JVM consente il sovraccarico in base al valore restituito! Questo è usato, ad esempio, in Scala , ed è possibile accedervi direttamente tramite Java anche giocando con gli interni.
PS. Come nota finale, è effettivamente possibile sovraccaricare il valore restituito in C ++ con un trucco. Testimone:
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
}