Вопрос

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

Это было полезно?

Решение

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

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

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

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

Два языка я регулярно (аб) используйте перегрузку по типу возвращаемого значения: Перл и Хаскелл.Позвольте мне описать, что они делают.

В Перл, существует фундаментальное различие между скаляр и список контекст (и другие, но мы притворимся, что их два).Каждая встроенная функция 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», когда имею в виду функцию.Это не имеет решающего значения для данного обсуждения.)

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

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++»):«Типы возвращаемых значений не учитываются при разрешении перегрузки.Причина в том, чтобы сохранить разрешение для отдельного оператора или вызова функции контекстно-независимым.Учитывать:

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 9.4.1):«Один из унаследованных методов должен иметь тип возвращаемого значения, заменяемый любым другим унаследованным методом;в противном случае возникает ошибка времени компиляции.» (Да, я знаю, что это не дает обоснования.Я уверен, что обоснование дано Гослингом в «Языке программирования Java».Может у кого есть копия?Могу поспорить, что по сути это «принцип наименьшего неожиданности».) Однако интересный факт о Java:JVM позволяет перегрузка по возвращаемому значению!Это используется, например, в Скала, и к ним можно получить доступ напрямую через Java а также играя с внутренними компонентами.

ПС.И последнее замечание: в 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) do допускает перегрузку по типу возвращаемого значения. Конечно, они тоже сталкиваются с вышеуказанной сложностью, но у них есть обходные пути, для которых вам придется ознакомиться с их документацией.

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

f(g(x))

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

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

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

<Ч>

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

  

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

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

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

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

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
  • Поддержка рефлексивного программирования

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

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

template <typename T> inline T func() { abort(); return T(); }

template <> inline int func()
{ <<special code for int>> }

template <> inline double func()
{ <<special code for double>> }

.. etc, then ..

int x = func(); // ambiguous!
int x = func<int>(); // *also* ambiguous!?  you're just being difficult, g++!

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

template <typename T>
struct func
{
    operator T()
    { abort(); return T(); } 
};

// explicit specializations for supported types
// (any code that includes this header can add more!)

template <> inline
func<int>::operator int()
{ <<special code for int>> }

template <> inline
func<double>::operator double()
{ <<special code for double>> }

.. etc, then ..

int x = func<int>(); // this is OK!
double d = func<double>(); // also OK :)

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

template <typename T>
struct func<T*>
{
    operator T*()
    { <<special handling for T*>> } 
};

Как минус, вы не можете написать int x = func(); с моим решением. Вы должны написать int x = func<int>();. Вы должны явно сказать, что является возвращаемым типом, вместо того, чтобы просить компилятор выяснить это, посмотрев на операторы преобразования типов. Я бы сказал, что & Quot; мой & Quot; решение и A.Rex принадлежат в оптимального по Парето фронт способов решения этой дилеммы 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;

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

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

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 <iostream>

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<Wea*>(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;
}

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

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 <string>
#include <sstream>
#include <algorithm>

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<typename T>
    static bool stringToValue( const std::string& str, T* pVal, unsigned numValues );

    template<typename T>
    static T getValue( const std::string& str, std::size_t& remainder );
};

#include "util.inl"

#endif UTIL_H

util.inl

template<typename T>
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<T>(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<T>(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;
}

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

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

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

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

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 " ??

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

scroll top