Перегрузка функций по типу возврата?

Почему не более распространенные статически типизированные языки поддерживают перегрузку функции / метода по типу возврата? Я не могу думать об этом. Это кажется не менее полезным или разумным, чем поддержка перегрузки по типу параметра. Почему он так популярен?

Вопреки тому, что говорят другие, перегрузка по типу возврата возможна и выполняется некоторыми современными языками. Обычное возражение состоит в том, что в коде вроде

int func(); string func(); int main() { func(); } 

вы не можете определить, какую func() вызывает func() . Это можно решить несколькими способами:

  1. Имейте предсказуемый метод, чтобы определить, какая функция вызывается в такой ситуации.
  2. Всякий раз, когда возникает такая ситуация, это ошибка времени компиляции. Однако иметь синтаксис, который позволяет программисту устранить неоднозначность, например int main() { (string)func(); } int main() { (string)func(); } .
  3. Не имеют побочных эффектов. Если у вас нет побочных эффектов, и вы никогда не используете возвращаемое значение функции, тогда компилятор может избежать вызова функции в первую очередь.

Два языка, на которых я регулярно ( 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 [email protected] печатает размер.) Кроме того, каждый оператор может принудительно использовать контекст, например, добавление + заставляет скалярный контекст. Каждая запись в man perlfunc документирует это. Например, вот часть записи для glob EXPR :

В контексте списка возвращается (возможно, пустой) список расширений имени файла в значении EXPR например, стандартный Unix shell /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 (формально, каждый тип, являющийся экземпляром classа типа Read ), readLn может его прочитать. Как Хаскелл знал, что я хотел прочитать номер с плавающей запятой? Ну, тип sqrt - 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) 

Во всяком случае, что означает вся эта дискуссия, является то, что перегрузка по возвращаемому значению возможна и выполняется, что отвечает на часть вашего вопроса.

Другая часть вашего вопроса заключается в том, почему другие языки этого не делают. Я позволю другим ответить на это. Однако несколько замечаний: основная причина, вероятно, в том, что возможность беспорядка здесь действительно больше, чем при перегрузке по типу аргумента. Вы также можете взглянуть на обоснования с отдельных языков:

Ada : «Может показаться, что самым простым правилом разрешения перегрузки является использование всего - вся информация из максимально широкого контекста - для устранения перегруженной ссылки. Это правило может быть простым, но это не полезно. сканировать произвольно большие fragmentы текста и делать произвольно сложные выводы (например, (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 } 

Если функции были перегружены возвращаемым типом, и у вас были эти две перегрузки

 int func(); string func(); 

компилятор не может понять, какая из этих двух функций вызывает вызов такого типа

 void main() { func(); } 

По этой причине разработчики языка часто запрещают перегрузку возвращаемых значений.

Однако некоторые языки (например, MSIL) допускают перегрузку по типу возврата. Конечно, они тоже сталкиваются с вышеуказанной сложностью, но у них есть обходные пути, для которых вам придется проконсультироваться с их документацией.

На таком языке, как бы вы решили следующее:

 f(g(x)) 

если f имеет перегрузки void f(int) и void f(string) а g имеет перегрузки int g(int) и string g(int) ? Вам понадобится какой-то разноплановый вызов.

Я думаю, что ситуации, в которых вам может понадобиться, было бы лучше, если бы вы выбрали новое имя для этой функции.

Чтобы украсть специальный ответ C ++ из другого очень похожего вопроса (dupe?):


Типы возвращаемых функций не вступают в игру при разрешении перегрузки просто потому, что Stroustrup (я предполагаю, что с использованием других архитекторов C ++) хотел, чтобы разрешение перегрузки было «независимым от контекста». См. 7.4.1 – «Тип перегрузки и возврата» из «Язык программирования C ++, третье издание».

Причина заключается в том, чтобы поддерживать разрешение для отдельного оператора или вызова функции контекстно-зависимым.

Они хотели, чтобы он основывался только на том, как называлась перегрузка, а не как результат был использован (если он вообще использовался). Действительно, многие функции вызываются без использования результата или результат будет использоваться как часть большего выражения. Один из факторов, который, я уверен, вступил в игру, когда они решили, что если бы тип возврата был частью резолюции, было бы много вызовов перегруженных функций, которые нужно было бы разрешить с помощью сложных правил или потребовалось бы, чтобы компилятор ошибка в том, что вызов был неоднозначным.

И, бог знает, разрешение перегрузки C ++ достаточно сложно, поскольку оно стоит …

В haskell это возможно, даже если у него нет функции перегрузки. Haskell использует classы типов. В программе вы можете увидеть:

 class Example a where example :: Integer -> a instance Example Integer where -- example is now implemented for Integer example :: Integer -> Integer example i = i * 10 

Перегрузка функции сама по себе не так популярна. В основном языки, которые я видел с ним, это C ++, возможно, java и / или C #. На всех динамических языках это сокращение для:

 define example:i ↑i type route: Integer = [↑i & 0xff] String = [↑i upper] def example(i): if isinstance(i, int): return i & 0xff elif isinstance(i, str): return i.upper() 

Поэтому в этом нет особого смысла. Большинство людей не интересуется, может ли язык помочь вам сбросить одну строку за каждый раз, когда вы ее используете.

Совпадение шаблонов несколько похоже на перегрузку функций, и я думаю, что иногда они работают аналогично. Это не часто, потому что оно полезно только для нескольких программ и сложно реализовать на большинстве языков.

Вы видите, что существует множество других более простых в реализации функций для реализации на языке, в том числе:

  • Динамическая типизация
  • Внутренняя поддержка списков, словарей и строк юникода
  • Оптимизации (JIT, ввод типов, компиляция)
  • Интегрированные инструменты развертывания
  • Поддержка библиотек
  • Общественная поддержка и встречи
  • Богатые стандартные библиотеки
  • Хороший синтаксис
  • Прочитать цикл печати eval
  • Поддержка рефлексивного программирования

Хорошие ответы! Ответ A.Rex, в частности, очень подробный и поучительный. Как он указывает, C ++ рассматривает пользовательские операторы преобразования типов при компиляции lhs = func(); (где func – это действительно имя структуры) . Мое обходное решение немного другое – не лучше, просто другое (хотя оно основано на одной и той же базовой идее).

В то время как я хотел написать …

 template  inline T func() { abort(); return T(); } template <> inline int func() { <> } template <> inline double func() { <> } .. etc, then .. int x = func(); // ambiguous! int x = func(); // *also* ambiguous!? you're just being difficult, g++! 

Я закончил с решением, которое использует параметризованную структуру (с T = возвращаемым типом):

 template  struct func { operator T() { abort(); return T(); } }; // explicit specializations for supported types // (any code that includes this header can add more!) template <> inline func::operator int() { <> } template <> inline func::operator double() { <> } .. etc, then .. int x = func(); // this is OK! double d = func(); // also OK :) 

Преимущество этого решения состоит в том, что любой код, который включает в себя эти определения шаблонов, может добавить больше специальностей для большего количества типов. Также вы можете сделать частичную специализацию структуры по мере необходимости. Например, если вам нужна специальная обработка для типов указателей:

 template  struct func { operator T*() { <> } }; 

Как отрицательный, вы не можете написать int x = func(); с моим решением. Вы должны написать int x = func(); , Вы должны явно указать, что такое тип возврата, вместо того, чтобы просить компилятор рассказать об этом, посмотрев на операторы преобразования типов. Я бы сказал, что «мое» решение и A.Rex оба принадлежат к оптимальному по парето вариантам решения этой дилеммы C ++ 🙂

Как уже было показано, неоднозначные вызовы функции, которая отличается только обратным типом, вводит двусмысленность. Неоднозначность вызывает дефектный код. Неисправный код следует избегать.

Сложность, вызванная попыткой двусмысленности, показывает, что это не хороший взлом. Помимо интеллектуальных упражнений – почему бы не использовать процедуры со ссылочными параметрами.

 procedure(reference string){}; procedure(reference int){}; string blah; procedure(blah) 

эта функция перегрузки не сложна для управления, если вы посмотрите на нее несколько иначе. рассмотрим следующее:

 public Integer | String f(int choice){ if(choice==1){ return new string(); }else{ return new Integer(); }} 

если язык возвратит перегрузку, это приведет к перегрузке параметров, но не к дублированиям. это решило бы проблему:

 main (){ f(x) } 

потому что есть только один выбор f (int).

В .NET иногда мы используем один параметр, чтобы указать желаемый результат из общего результата, а затем сделали преобразование, чтобы получить то, что мы ожидаем.

C #

 public enum FooReturnType{ IntType, StringType, WeaType } class Wea { public override string ToString() { return "Wea class"; } } public static object Foo(FooReturnType type){ object result = null; if (type == FooReturnType.IntType) { /*Int related actions*/ result = 1; } else if (type == FooReturnType.StringType) { /*String related actions*/ result = "Some important text"; } else if (type == FooReturnType.WeaType) { /*Wea related actions*/ result = new Wea(); } return result; } static void Main(string[] args) { Console.WriteLine("Expecting Int from Foo: " + Foo(FooReturnType.IntType)); Console.WriteLine("Expecting String from Foo: " + Foo(FooReturnType.StringType)); Console.WriteLine("Expecting Wea from Foo: " + Foo(FooReturnType.WeaType)); Console.Read(); } 

Возможно, этот пример тоже может помочь:

C ++

  #include  enum class FooReturnType{ //Only C++11 IntType, StringType, WeaType }_FooReturnType; class Wea{ public: const char* ToString(){ return "Wea class"; } }; void* Foo(FooReturnType type){ void* result = 0; if (type == FooReturnType::IntType) //Only C++11 { /*Int related actions*/ result = (void*)1; } else if (type == FooReturnType::StringType) //Only C++11 { /*String related actions*/ result = (void*)"Some important text"; } else if (type == FooReturnType::WeaType) //Only C++11 { /*Wea related actions*/ result = (void*)new Wea(); } return result; } int main(int argc, char* argv[]) { int intReturn = (int)Foo(FooReturnType::IntType); const char* stringReturn = (const char*)Foo(FooReturnType::StringType); Wea *someWea = static_cast(Foo(FooReturnType::WeaType)); std::cout << "Expecting Int from Foo: " << intReturn << std::endl; std::cout << "Expecting String from Foo: " << stringReturn << std::endl; std::cout << "Expecting Wea from Foo: " << someWea->ToString() << std::endl; delete someWea; // Don't leak oil! return 0; } 

Для записи Octave допускает разные результаты в соответствии с возвращаемым элементом, являющимся скалярным и массивным.

 x = min ([1, 3, 0, 2, 0]) ⇒ x = 0 [x, ix] = min ([1, 3, 0, 2, 0]) ⇒ x = 0 ix = 3 (item index) 

Cf также разложение сингулярных значений .

Это немного отличается для C ++; Я не знаю, будет ли это считаться перегрузкой по типу возврата напрямую. Это скорее типовая специализация, которая действует в манере.

util.h

 #ifndef UTIL_H #define UTIL_H #include  #include  #include  class util { public: static int convertToInt( const std::string& str ); static unsigned convertToUnsigned( const std::string& str ); static float convertToFloat( const std::string& str ); static double convertToDouble( const std::string& str ); private: util(); util( const util& c ); util& operator=( const util& c ); template static bool stringToValue( const std::string& str, T* pVal, unsigned numValues ); template static T getValue( const std::string& str, std::size_t& remainder ); }; #include "util.inl" #endif UTIL_H 

util.inl

 template static bool util::stringToValue( const std::string& str, T* pValue, unsigned numValues ) { int numCommas = std::count(str.begin(), str.end(), ','); if (numCommas != numValues - 1) { return false; } std::size_t remainder; pValue[0] = getValue(str, remainder); if (numValues == 1) { if (str.size() != remainder) { return false; } } else { std::size_t offset = remainder; if (str.at(offset) != ',') { return false; } unsigned lastIdx = numValues - 1; for (unsigned u = 1; u < numValues; ++u) { pValue[u] = getValue(str.substr(++offset), remainder); offset += remainder; if ((u < lastIdx && str.at(offset) != ',') || (u == lastIdx && offset != str.size())) { return false; } } } return true; } 

util.cpp

 #include "util.h" template<> int util::getValue( const std::string& str, std::size_t& remainder ) { return std::stoi( str, &remainder ); } template<> unsigned util::getValue( const std::string& str, std::size_t& remainder ) { return std::stoul( str, &remainder ); } template<> float util::getValue( const std::string& str, std::size_t& remainder ) { return std::stof( str, &remainder ); } template<> double util::getValue( const std::string& str, std::size_t& remainder ) { return std::stod( str, &remainder ); } int util::convertToInt( const std::string& str ) { int i = 0; if ( !stringToValue( str, &i, 1 ) ) { std::ostringstream strStream; strStream << __FUNCTION__ << " Bad conversion of [" << str << "] to int"; throw strStream.str(); } return i; } unsigned util::convertToUnsigned( const std::string& str ) { unsigned u = 0; if ( !stringToValue( str, &u, 1 ) ) { std::ostringstream strStream; strStream << __FUNCTION__ << " Bad conversion of [" << str << "] to unsigned"; throw strStream.str(); } return u; } float util::convertToFloat(const std::string& str) { float f = 0; if (!stringToValue(str, &f, 1)) { std::ostringstream strStream; strStream << __FUNCTION__ << " Bad conversion of [" << str << "] to float"; throw strStream.str(); } return f; } double util::convertToDouble(const std::string& str) { float d = 0; if (!stringToValue(str, &d, 1)) { std::ostringstream strStream; strStream << __FUNCTION__ << " Bad conversion of [" << str << "] to double"; throw strStream.str(); } return d; } 

Этот пример не точно использует разрешение функции перегрузки по типу возвращаемого типа, однако этот class объектов c ++ использует специализированную специализацию для имитации разрешения перегрузки функции с помощью типа возврата с помощью частного статического метода.

Каждая из функций convertToType вызывает шаблон функции stringToValue() и если вы посмотрите на детали реализации или алгоритм этого шаблона функции, он вызывает getValue( param, param ) и возвращает тип T и сохраняет его в T* который передается в шаблон функции stringToValue() как один из его параметров.

Помимо чего-то подобного; В C ++ нет механизма для перегрузки функций с помощью возвращаемого типа. Могут быть другие конструкции или механизмы, о которых я не знаю, которые могли бы имитировать разрешение по типу возврата.

если вы хотите перегрузить методы с разными типами возвращаемых данных, просто добавьте параметр фиктивного значения со значением по умолчанию, чтобы разрешить выполнение перегрузки, но не забывайте, что тип параметра должен быть другим, поэтому логика перегрузки работает следующим образом: например, на delphi:

 type myclass = class public function Funct1(dummy: string = EmptyStr): String; overload; function Funct1(dummy: Integer = -1): Integer; overload; end; 

используйте его так

 procedure tester; var yourobject : myclass; iValue: integer; sValue: string; begin yourobject:= myclass.create; iValue:= yourobject.Funct1(); //this will call the func with integer result sValue:= yourobject.Funct1(); //this will call the func with string result end; 

Большинство статических языков также поддерживают дженерики, которые помогут решить вашу проблему. Как указывалось ранее, без параметра diff, нет способа узнать, какой из них нужно вызвать. Поэтому, если вы хотите это сделать, просто используйте дженерики и назовите это днем.

Я думаю, что это GAP в современном определении на C ++ … почему?

 int func(); double func(); // example 1. → defined int i = func(); // example 2. → defined double d = func(); // example 3. → NOT defined. error void main() { func(); } 

Почему компилятор C ++ не может вывести ошибку в примере «3» и принять код в примере «1 + 2»?

  • C # static member "inheritance" - почему это вообще существует?
  • Ограничение дженериков с помощью ключевого слова 'super'
  • Почему шаблон функции не может быть частично специализированным?
  • Нумерация нулевого месяца
  • Почему последняя часть имени объекта Objective-C принимает аргумент (когда имеется более одной части)?
  • Почему ваш тип данных оператора switch не может быть длинным, Java?
  • Почему в C # не разрешены параметры const?
  • Interesting Posts

    Как я могу конвертировать документ OpenOffice в PDF из командной строки Linux?

    Как дублировать элемент при использовании сортировки jquery?

    Каковы наилучшие методы структурирования большого приложения Meteor со многими файлами шаблонов HTML?

    понимание базовой рекурсии

    Как щелкнуть / выбрать строку таблицы в сценарии apple

    Чтение Xml с помощью XmlReader в C #

    Outlook 2007 переустановить и импортировать из резервной копии, теперь у меня есть дубликаты всего

    Пример примера streamparse wordcount

    Перенести настройки пользователя Firefox в другую папку на одном компьютере

    Есть ли способ бесконечно приостановить stream?

    Является ли время заменой Safely Remove Hardware?

    Почему мой Macbook становится очень жарким при использовании Boot Camp?

    Пароль байпаса BIOS, установленный неисправной прошивкой Toshiba на ноутбуке Satellite A55-S1065?

    Как вызвать метод асинхронизации с геттера или сеттера?

    Может ли функтор компоновщика работать?

    Давайте будем гением компьютера.