Вопреки тому , что говорят другие, перегрузка возвращаемого типа является возможным и это делают некоторыми современными языками. Обычное возражение заключается в том, что в коде, как
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
. В скалярном контексте glob повторяет такие расширения имени файла, возвращая 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
может его прочитать. Как Хаскелл узнал, что я хочу прочитать число с плавающей запятой? Ну, тип sqrt
is sqrt :: Floating a => a -> a
, что по сути означает, что он sqrt
может принимать только числа с плавающей запятой в качестве входных данных, и поэтому 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 ++» Бьярна Страуструпа): «Типы возвращаемых данных не учитываются при разрешении перегрузки. Причина заключается в том, чтобы сохранять разрешение для отдельного оператора или вызова функции независимым от контекста.
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
}