Вопреки тому, что говорят другие, перегрузка по типу возврата возможна , а - в некоторых современных языках. Обычное возражение заключается в том, что в коде типа
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
, что позволяет различать списочный, скалярный и пустой контекст. Так, например, вы можете решить ничего не делать, если вас вызывают в пустом контексте.
Теперь вы можете жаловаться, что это не true перегрузка возвращаемым значением, потому что у вас есть только одна функция, которая сообщает контекст, в котором она вызывается, и затем воздействует на эту информацию. Однако это явно эквивалентно (и аналогично тому, как 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
равен sqrt :: Floating a => a -> a
, что по сути означает, что sqrt
может принимать только числа с плавающей запятой в качестве входных данных, и поэтому Хаскелл сделал вывод, что я хотел.
Что происходит, когда Хаскелл не может понять, чего я хочу? Ну, есть несколько возможностей. Если я вообще не использую возвращаемое значение, Haskell просто не будет вызывать функцию. Однако, если я do использую возвращаемое значение, то 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)
В любом случае, все это обсуждение означает, что перегрузка возвращаемым значением возможна и выполняется, что отвечает на часть вашего вопроса.
Другая часть вашего вопроса - почему больше языков не делают этого. Я позволю другим ответить на это. Однако несколько комментариев: основная причина, вероятно, заключается в том, что вероятность путаницы здесь действительно больше, чем при перегрузке по типу аргумента. Вы также можете посмотреть на обоснования на отдельных языках:
Ada : «Может показаться, что простейшим правилом разрешения перегрузки является использование всего - всей информации из максимально широкого контекста - для разрешения перегруженной ссылки. Это правило может быть простым, но оно не помогает. Требуется, чтобы читатель-человек сканировал произвольно большие фрагменты текста и делал произвольно сложные выводы (такие как (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
}