Попри те , що говорять інші, перевантаження повертається типу є можливим і це роблять деякими сучасними мовами. Зазвичай заперечення полягає в тому, що в коді, як
int func();
string func();
int main() { func(); }
ви не можете сказати, що func()
викликається. Це можна вирішити кількома способами:
- Мати передбачуваний метод визначення того, яка функція викликається в такій ситуації.
- Щоразу, коли виникає така ситуація, це помилка часу компіляції. Однак є синтаксис, який дозволяє програмісту роз'єднуватись, наприклад
int main() { (string)func(); }
.
- Не мають побічних ефектів. Якщо у вас немає побічних ефектів і ви ніколи не використовуєте повернене значення функції, компілятор може уникнути будь-якого виклику функції в першу чергу.
Дві мови, якими я регулярно ( ab ) користуюсь перевантаженням за типом повернення: Perl та Haskell . Дозвольте описати, що вони роблять.
У Perl існує принципова відмінність між скалярним та списком контексту (та іншими, але ми будемо робити вигляд, що їх два). Кожна вбудована функція в Perl може робити різні речі залежно від контексту, в якому вона викликана. Наприклад, join
оператор змушує перераховувати контекст (на предмет, що з'єднується), а scalar
оператор примушує скалярний контекст, тому порівняйте:
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.
Кожен оператор в Perl робить щось у скалярному контексті і щось у контексті списку, і вони можуть бути різними, як показано на ілюстрації. (Це не тільки для випадкових операторів, таких як localtime
. Якщо ви використовуєте масив @a
у контексті списку, він повертає масив, а в скалярному контексті - повертає кількість елементів. Так, наприклад, print @a
виводиться з елементів, а print 0+@a
друкується розмір. ) Крім того, кожен оператор може форсувати контекст, наприклад, додавання +
примушує скалярний контекст. Кожен запис у man perlfunc
документах це. Наприклад, ось частина запису для glob EXPR
:
У контексті списку повертає (можливо, порожній) список розширень імен файлів на значення, EXPR
таке як стандартна оболонка Unix /bin/csh
. У скалярному контексті глобус повторюється через такі розширення імен файлів, повертаючи undef, коли список вичерпано.
Тепер, яке співвідношення між списком та скалярним контекстом? Ну, man perlfunc
каже
Запам’ятайте таке важливе правило: Не існує правила, яке б пов'язувало поведінку виразу в контексті списку з його поведінкою у скалярному контексті чи навпаки. Це може зробити дві абсолютно різні речі. Кожен оператор і функція вирішує, яке саме значення було б найбільш доцільним повернути в скалярному контексті. Деякі оператори повертають довжину списку, який був би повернутий у контексті списку. Деякі оператори повертають перше значення у списку. Деякі оператори повертають останнє значення у списку. Деякі оператори повертають кількість успішних операцій. Взагалі вони роблять те, що ви хочете, якщо ви не хочете послідовності.
тому справа не в тому, щоб мати єдину функцію, і тоді ви виконаєте просте перетворення в кінці. Насправді я вибрав localtime
приклад саме з цієї причини.
Таку поведінку мають не лише вбудовані модулі. Будь-який користувач може визначити таку функцію за допомогою wantarray
, що дозволяє розрізняти список, скалярний та недійсний контекст. Так, наприклад, ви можете вирішити нічого не робити, якщо вас викликають у недійсному контексті.
Тепер ви можете поскаржитися, що це неправда перевантаження за значенням повернення, оскільки у вас є лише одна функція, яка відповідає контексту, в який вона викликана, а потім діє на цю інформацію. Однак це очевидно еквівалентно (і аналогічно тому, як Perl не дозволяє звичайно перевантажувати буквально буквально, але функція може просто вивчити свої аргументи). Більше того, це добре вирішує неоднозначну ситуацію, про яку було сказано на початку цієї відповіді. Perl не скаржиться, що не знає, до якого методу дзвонити; це просто називає. Все, що потрібно зробити, це зрозуміти, в якому контексті викликалася функція, яка завжди можлива:
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"
(Примітка. Іноді я можу сказати, що оператор Perl, коли я маю на увазі функцію. Це не важливо для цієї дискусії.)
Haskell застосовує інший підхід, а саме - не мати побічних ефектів. Він також має сильну систему типу, і тому ви можете писати код на зразок наступного:
main = do n <- readLn
print (sqrt n) -- note that this is aligned below the n, if you care to run this
Цей код зчитує число з плаваючою комою зі стандартного вводу та друкує його квадратний корінь. Але що дивного в цьому? Ну, тип readLn
є readLn :: Read a => IO a
. Це означає, що для будь-якого типу, який може бути Read
(формально, кожен тип, який є екземпляром Read
класу типів), readLn
може його прочитати. Звідки Haskell знав, що я хочу прочитати число з плаваючою комою? Ну, тип sqrt
є sqrt :: Floating a => a -> a
, що по суті означає, що sqrt
можна приймати лише цифри з плаваючою комою як вхідні дані, і тому Haskell зробив висновок про те, що я хотів.
Що станеться, коли Haskell не може зробити те, що я хочу? Ну, є кілька можливостей. Якщо я взагалі не використовую повернене значення, Haskell просто не буде викликати функцію в першу чергу. Однак, якщо я зробити використовувати значення, що повертається, то Haskell буде скаржитися , що він не може визначити тип:
main = do n <- readLn
print n
-- this program results in a compile-time error "Unresolved top-level overloading"
Я можу вирішити двозначність, вказавши потрібний тип:
main = do n <- readLn
print (n::Int)
-- this compiles (and does what I want)
У будь-якому випадку, що означає вся ця дискусія, це те, що перевантаження зворотним значенням можливе і робиться, що відповідає частині вашого запитання.
Інша частина вашого питання полягає в тому, чому більшість мов цього не роблять. Я дозволю іншим відповісти на це. Однак кілька коментарів: головна причина, ймовірно, полягає в тому, що можливості для плутанини тут справді більше, ніж при перевантаженні за типом аргументу. Ви також можете переглянути обґрунтування з окремих мов:
Ада : "Може здатися, що найпростіше правило розв'язання перевантаження - використовувати все - всю інформацію з якомога ширшого контексту - для вирішення перевантаженої посилання. Це правило може бути простим, але не корисним. Це вимагає від читача людини сканувати довільно великі фрагменти тексту та робити довільно складні умовиводи (наприклад, (g) вище). Ми вважаємо, що кращим правилом є те, що робить явним завданням, який повинен виконувати читач людини чи компілятор, і це робить це завдання якомога природніше для людського читача ».
C ++ (підрозділ 7.4.1 розділу "Мова програмування на C ++" Bjarne Stroustrup): "Типи повернення не враховуються в роздільній здатності перевантаження. Причиною є збереження роздільної здатності для окремого оператора або функції виклику функції незалежно від контексту".
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)
}
Якщо врахувати тип повернення, більше не можна було б дивитись на виклик sqrt()
ізоляції та визначати, яка функція викликалася. "(Зауважте, для порівняння, що в Haskell немає неявних перетворень.)
Java ( специфікація мови Java 9.4.1 ): "Один із успадкованих методів повинен бути замінним типом повернення для всіх інших успадкованих методів, інакше виникає помилка часу компіляції." (Так, я знаю, що це не дає обгрунтування. Я впевнений, що обгрунтування дано Гослінгом у "Мові програмування Java". Можливо, у когось є копія? Я думаю, що це "принцип найменшого здивування" по суті. ) Однак, цікавий факт про Java: JVM дозволяє перевантажувати повернене значення! Це використовується, наприклад, у Scala , і до нього можна дістатись безпосередньо через Java, також граючи з внутрішніми.
PS. Як остаточне зауваження, насправді можна перевантажувати повернене значення в C ++ хитрістю. Свідок:
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
}