Почему более распространенные статически типизированные языки не поддерживают перегрузку функций / методов по типу возвращаемого значения? Я не могу думать ни о чем, что делает. Это кажется не менее полезным или разумным, чем поддержка перегрузки по типу параметра. Почему это так менее популярно?
252
Ответы:
Вопреки тому , что говорят другие, перегрузка возвращаемого типа является возможным и это делают некоторыми современными языками. Обычное возражение заключается в том, что в коде, как
Вы не можете сказать, что
func()
называется. Это может быть решено несколькими способами:int main() { (string)func(); }
.Два языка, которые я регулярно ( ab ) использую перегрузку по типу возвращаемого значения: Perl и Haskell . Позвольте мне описать, что они делают.
В Perl есть фундаментальное различие между скалярным и списочным контекстом (и другими, но мы будем делать вид, что их два). Каждая встроенная функция в Perl может делать разные вещи в зависимости от контекста, в котором она вызывается. Например,
join
оператор форсирует контекст списка (для соединяемой вещи), аscalar
оператор форсирует скалярный контекст, поэтому сравните:Каждый оператор в Perl делает что-то в скалярном контексте и что-то в контексте списка, и они могут быть разными, как показано на рисунке. (Это не только для случайных операторов, таких как
localtime
. Если вы используете массив@a
в контексте списка, он возвращает массив, в то время как в скалярном контексте он возвращает количество элементов. Так, например,print @a
распечатывает элементы, аprint 0+@a
печатает размер. Кроме того, каждый оператор может форсировать контекст, например, сложение+
вызывает скалярный контекст. Каждая запись вman perlfunc
документах это. Например, вот часть записи дляglob EXPR
:Теперь, какова связь между списком и скалярным контекстом? Ну
man perlfunc
говориттак что это не просто вопрос наличия единственной функции, а затем вы делаете простое преобразование в конце. На самом деле, я выбрал
localtime
пример по этой причине.Это не только встроенные модули, которые имеют такое поведение. Любой пользователь может определить такую функцию с помощью
wantarray
, которая позволяет различать списочный, скалярный и пустой контекст. Так, например, вы можете решить ничего не делать, если вас вызывают в пустом контексте.Теперь вы можете жаловаться, что это не настоящая перегрузка возвращаемым значением, потому что у вас есть только одна функция, которая сообщает контекст, в котором она вызывается, и затем воздействует на эту информацию. Однако это явно эквивалентно (и аналогично тому, как Perl не допускает обычной перегрузки буквально, но функция может просто проверить свои аргументы). Кроме того, это хорошо решает неоднозначную ситуацию, упомянутую в начале этого ответа. Perl не жалуется, что не знает, какой метод вызывать; это просто называет это. Все, что нужно сделать, это выяснить, в каком контексте была вызвана функция, что всегда возможно:
(Примечание. Иногда я имею в виду оператор Perl, когда имею в виду функцию. Это не принципиально для этого обсуждения.)
Haskell использует другой подход, а именно, чтобы не иметь побочных эффектов. Он также имеет строгую систему типов, и поэтому вы можете написать код, подобный следующему:
Этот код читает число с плавающей точкой из стандартного ввода и печатает его квадратный корень. Но что удивительного в этом? Ну типа
readLn
естьreadLn :: Read a => IO a
. Это означает, что любой тип, который может бытьRead
(формально, каждый тип, который является экземпляромRead
класса типов),readLn
может его прочитать. Как Хаскелл узнал, что я хочу прочитать число с плавающей запятой? Ну, типsqrt
issqrt :: Floating a => a -> a
, что по сути означает, что онsqrt
может принимать только числа с плавающей запятой в качестве входных данных, и поэтому Haskell сделал вывод, что я хотел.Что происходит, когда Хаскелл не может понять, чего я хочу? Ну, есть несколько возможностей. Если я вообще не использую возвращаемое значение, Haskell просто не будет вызывать функцию. Однако, если я сделать использовать возвращаемое значение, то Haskell будет жаловаться , что он не может определить тип:
Я могу устранить неоднозначность, указав тип, который я хочу:
В любом случае, все это обсуждение означает, что перегрузка возвращаемым значением возможна и выполняется, что отвечает на часть вашего вопроса.
Другая часть вашего вопроса - почему больше языков не делают этого. Я позволю другим ответить на это. Однако несколько комментариев: основная причина, вероятно, заключается в том, что вероятность путаницы здесь действительно больше, чем при перегрузке по типу аргумента. Вы также можете посмотреть на обоснования из отдельных языков:
Ада : «Может показаться, что самое простое правило разрешения перегрузки - это использовать все - всю информацию из максимально широкого контекста - для разрешения перегруженной ссылки. Это правило может быть простым, но оно не полезно. Оно требует читателя-человека. сканировать произвольно большие фрагменты текста и делать произвольно сложные выводы (такие как (g) выше). Мы считаем, что лучшим правилом является то, которое делает явным задачу, которую должен выполнять читатель или компилятор, и которая делает эту задачу настолько естественным для человека, насколько это возможно. "
C ++ (подраздел 7.4.1 «Языка программирования C ++» Бьярна Страуструпа): «Типы возвращаемых данных не учитываются при разрешении перегрузки. Причина заключается в том, чтобы сохранять разрешение для отдельного оператора или вызова функции независимым от контекста.
Если бы возвращаемый тип был принят во внимание, было бы больше невозможно смотреть на отдельный вызов
sqrt()
и определять, какая функция была вызвана. "(Для сравнения, обратите внимание, что в Haskell нет неявных преобразований.)Java ( спецификация языка Java 9.4.1 ): «Один из унаследованных методов должен быть заменяемым типом возврата для любого другого унаследованного метода, иначе произойдет ошибка времени компиляции». (Да, я знаю, что это не дает обоснования. Я уверен, что обоснование дается Гослингом в «Языке программирования Java». Может быть, у кого-то есть копия? ) Однако забавный факт о Java: JVM допускает перегрузку возвращаемым значением! Это используется, например, в Scala , и к нему можно получить доступ напрямую через Java, а также поиграть с внутренностями.
PS. И последнее замечание: на самом деле можно перегрузить возвращаемым значением в C ++ с помощью хитрости. Свидетель:
источник
Foo
иBar
поддержка двунаправленного преобразования и способ применения типаFoo
внутренне , но тип возвращаетсяBar
. Если такой метод вызывается кодом, который немедленно приведет результат к типуFoo
, использованиеBar
возвращаемого типа может сработать, ноFoo
тот будет лучше. Кстати, я также хотел бы увидеть средства, с помощью которых ...var someVar = someMethod();
(или указать, что его возвращение не должно использоваться таким образом). Например, семейство типов, которое реализует интерфейс Fluent, может выиграть от наличия изменяемых и неизменяемых версий, поэтомуvar thing2 = thing1.WithX(3).WithY(5).WithZ(9);
имело быWithX(3)
копиюthing1
в изменяемый объект, изменяющее X и возвращающее этот изменяемый объект;WithY(5)
мутирует Y и возвращает тот же объект; аналогично `WithZ (9). Тогда назначение будет преобразовано в неизменный тип.Если функции были перегружены типом возвращаемого значения, и у вас были эти две перегрузки
компилятор не может выяснить, какую из этих двух функций вызывать, увидев такой вызов
По этой причине разработчики языка часто запрещают перегрузку возвращаемых значений.
Однако некоторые языки (например, MSIL) действительно позволяют перегружать возвращаемым типом. Конечно, они тоже сталкиваются с вышеуказанной сложностью, но у них есть обходные пути, для которых вам придется ознакомиться с их документацией.
источник
На таком языке, как бы вы решили следующее:
если
f
у перегрузокvoid f(int)
иvoid f(string)
иg
имели перегрузкиint g(int)
иstring g(int)
? Вам понадобится какой-то двусмысленный.Я думаю, что ситуации, в которых вам это может понадобиться, будут лучше подходить, если выбрать новое имя для функции.
источник
Чтобы украсть специфический для C ++ ответ на другой очень похожий вопрос (дурак?):
Типы возвращаемых функций не вступают в игру в разрешении перегрузки просто потому, что Страуструп (я полагаю, с помощью других архитекторов C ++) хотел, чтобы разрешение перегрузки было «независимым от контекста». См. 7.4.1 - «Перегрузка и тип возврата» в «Языке программирования C ++, третье издание».
Они хотели, чтобы он основывался только на том, как вызывается перегрузка, а не на том, как был использован результат (если он вообще использовался). Действительно, многие функции вызываются без использования результата, иначе результат будет использоваться как часть большего выражения. Я уверен, что один из факторов вступил в игру, когда они решили, что если бы возвращаемый тип был частью разрешения, было бы много обращений к перегруженным функциям, которые нужно было бы разрешить с помощью сложных правил или пришлось бы бросать компилятор. ошибка в том, что звонок был неоднозначным.
И, Господь знает, разрешение перегрузки в C ++ достаточно сложно, как оно есть ...
источник
В haskell это возможно, даже если у него нет перегрузки функций. Haskell использует классы типов. В программе вы могли видеть:
Функция перегрузки сама по себе не так популярна. Большинство языков, которые я видел, это C ++, возможно, Java и / или C #. Во всех динамических языках это сокращение для:
Поэтому в этом нет особого смысла. Большинству людей не интересно, может ли язык помочь вам оставить одну строчку за то место, где вы его используете.
Сопоставление с шаблоном в некоторой степени похоже на перегрузку функций, и я думаю, иногда они работают аналогично. Это не часто встречается, потому что это полезно только для немногих программ и сложно реализовать на большинстве языков.
Вы видите, что существует бесконечно много других, более простых в реализации, функций для реализации на языке, включая:
источник
Хорошие ответы! В частности, ответ А.Рекса очень подробный и поучительный. Как он указывает, C ++ делает рассмотреть предоставленные пользователем операторы преобразования типов при компиляции
lhs = func();
(где FUNC действительно имя структуры) . Мой обходной путь немного другой - не лучше, просто другой (хотя он основан на той же основной идее).В то время как я хотел написать ...
Я закончил с решением, которое использует параметризованную структуру (с T = тип возвращаемого значения):
Преимущество этого решения заключается в том, что любой код, который включает эти определения шаблонов, может добавить больше специализаций для большего количества типов. Также вы можете делать частичные специализации структуры по мере необходимости. Например, если вы хотите специальную обработку для типов указателей:
Как минус, вы не можете написать
int x = func();
с моим решением. Вы должны написатьint x = func<int>();
. Вы должны явно сказать, что является возвращаемым типом, вместо того, чтобы просить компилятор выяснить это, посмотрев на операторы преобразования типов. Я бы сказал, что «моё» решение и решение A.Rex принадлежат к оптимальному по парето способу решения этой дилеммы C ++ :)источник
если вы хотите перегрузить методы с различными типами возвращаемых данных, просто добавьте фиктивный параметр со значением по умолчанию, чтобы разрешить выполнение перегрузки, но не забывайте, что тип параметра должен отличаться, поэтому следующая логика перегрузки работает, например, на delphi:
используйте это так
источник
Как уже было показано, неоднозначные вызовы функции, которая отличается только типом возврата, привносят неоднозначность. Неоднозначность вызывает дефектный код. Неисправный код следует избегать.
Сложность, вызванная попыткой двусмысленности, показывает, что это не хороший взлом. Помимо интеллектуальных упражнений - почему бы не использовать процедуры с эталонными параметрами.
источник
doing(thisVery(deeplyNested(), andOften(butNotAlways()), notReally()), goodCode());
с этой функцией перегрузки нетрудно управлять, если посмотреть на нее немного по-другому. учтите следующее,
если язык возвращает перегрузку, это допускает перегрузку параметров, но не дублирование. это решило бы проблему:
потому что есть только один f (int choice) на выбор.
источник
В .NET иногда мы используем один параметр, чтобы указать желаемый результат общего результата, а затем выполняем преобразование, чтобы получить то, что мы ожидаем.
C #
Может быть, этот пример тоже может помочь:
C ++
источник
Для записи, Octave допускает другой результат в зависимости от возвращаемого элемента, который является скалярным или массивом.
См. Также Сингулярное Разложение Значения .
источник
Этот немного отличается для C ++; Я не знаю, будет ли это рассматриваться как перегрузка по типу возврата напрямую. Это скорее шаблонная специализация, которая действует как.
util.h
util.inl
util.cpp
В этом примере не совсем используется разрешение перегрузки функций по типу возвращаемого значения, однако этот необъектный класс c ++ использует специализацию шаблона для имитации разрешения перегрузки функции по типу возвращаемого значения с помощью частного статического метода.
Каждая из
convertToType
функций вызывает шаблон функции,stringToValue()
и если вы посмотрите на детали реализации или алгоритм этого шаблона функции, она вызываетgetValue<T>( param, param )
и возвращает типT
и сохраняет егоT*
в классе, который передается вstringToValue()
шаблон функции в качестве одного из ее параметров. ,Помимо чего-то вроде этого; В C ++ на самом деле нет механизма для разрешения перегрузки функций по типу возвращаемого значения. Могут быть другие конструкции или механизмы, о которых я не знаю, которые могут имитировать разрешение по типу возвращаемого значения.
источник
Я думаю, что это GAP в современном определении C ++ ... почему?
Почему компилятор C ++ не может выдать ошибку в примере "3" и принять код в примере "1 + 2" ??
источник
Большинство статических языков также теперь поддерживают дженерики, которые решат вашу проблему. Как указывалось ранее, без параметров diff нет способа узнать, какой из них вызывать. Так что, если вы хотите сделать это, просто используйте дженерики и называйте это днем.
источник