На самом деле невозможно использовать перегрузку возвращаемого типа?

Я сделал небольшую DLL в MSIL двумя способами:

float AddNumbers(int, int)
int AddNumbers(int, int)

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

float f = ILasm1.MainClass.AddNumbers(1, 2);

Ошибка:

Вызов неоднозначен между следующими методами или свойствами: 'ILasm1.MainClass.AddNumbers(int, int)' и 'ILasm1.MainClass.AddNumbers(int, int)'

Действительно ли С# не способен различать разные типы возвращаемых данных? Я знаю, что не могу программировать методы, которые имеют только разные типы возвращаемых данных, но я всегда предполагал, что он будет знать, как с ним справиться.

Ответ 1

Как и все остальные, ни один С# не поддерживает это. Фактически, причина, по которой IL поддерживает это, заключается в том, что вы должны быть явно о типах возвращаемых данных, точно так же, как и параметры. Например, в IL вы скажете

ldarg.0
ldarg.1
call int AddNumbers(int, int)

IL действительно не имеет понятия перегрузки метода: float AddNumbers(int, int) не имеет отношения к int AddNumbers(int, int), что касается IL. Вы должны заранее сказать компилятору IL, и он никогда не пытается сделать вывод о намерениях (например, языки более высокого уровня, такие как С# do).

Обратите внимание, что большинство языков .NET и С# делают одно исключение для перегрузки типа возвращаемого типа: операторы преобразования. Так

public static explicit operator B(A a);
public static explicit operator C(A a);

скомпилированы в

public static B op_Explicit(A a);
public static C op_Explicit(A a);

Поскольку это такой конкретный краевой случай, который должен поддерживаться для примитивных типов (например, int → bool) и конверсий ссылочного типа (в противном случае вы получите очень педантичный язык), это обрабатывается, но не как случай перегрузки метода.

Ответ 2

ECMA-334 С# Раздел 8.7.3

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

Можно использовать общий метод:

T AddNumbers<T>(int a, int b)
{
   if (typeof(T) == typeof(int) || typeof(T) == typeof(float))
   {
      return (T)Convert.ChangeType(a + b, typeof(T));
   }

   throw new NotSupportedException();
}

Ответ 3

Да, это действительно невозможно в С#, я знаю, что С++ тоже этого не допускает, это связано с тем, как интерпретируется оператор:

double x = AddNumbers(1, 2);

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

Компилятор не может определить, какая версия наиболее подходит. Используя какое-то произвольное правило, просто будет сложно найти ошибки.

Это связано с этим простым утверждением:

double y = 5 / 2;  // y = 2.0

Ответ 4

Проблема в том, что существует автоматическое преобразование из int в float, поэтому оно действительно не знает, что вы намеревались. Вы намеревались вызвать метод, который принимает два ints и возвращает int, а затем преобразовать его в float или вы намеревались вызвать метод, который принимает два int и возвращает float? Лучше иметь ошибку компилятора, чем делать неправильный выбор и не сообщать об этом до тех пор, пока ваше приложение не сломается.

Ответ 5

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

Ковариантные типы возвращаемых данных обсуждались в течение многих лет, они частично разрешены на Java, у С++ есть специальный случай, а дизайнеры С# добавили некоторые незначительные изменяется на 4.0.

Для вашей игрушечной проблемы существуют простые типичные решения. Например, ваш float AddNumbers(int, int), вероятно, всегда будет таким же, как (float) AddNumbers(int, int), и в этом случае нет необходимости в второй функции. Обычно этот случай обрабатывается с помощью дженериков: <T> AddNumbers(<T> n1, <T> n2), поэтому вы получите float AddNumbers(float, float) и int AddNumbers(int, int).

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

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

Об этом уже говорилось здесь.

Случай в MSIL/С# в http://blogs.msdn.com/abhinaba/archive/2005/10/07/478221.aspx является особенным, поскольку они являются явными операторами преобразования.

Ответ 6

Нет, нет способа сделать это. На самом деле, кроме С++ с шаблонами, язык не поддерживает его. Это просто слишком опасно. И снова, что, если вы пишете

var a = AddNumbers(1, 1);

какой тип a должен быть?

Или, если вы называете это

 double a = AddNumbers(1, 1);

или даже

 AddNumbers(1, 1);

какую версию он должен назвать?

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

class Program
{
    static int parse(int a) { return a; }
    static float parse(float a) { return a; }


    static void Main(string[] args)
    {
        double a = parse(1.0);
    }
}

Если вы попытаетесь скомпилировать его, компилятор даст вам сообщение об ошибке

error C2668: 'parse' : ambiguous call to overloaded function
could be 'float parse(float)'

потому что 1.0 имеет тип double, и компилятор действительно не знает, какой тип выбрать между int и float, поэтому он просит вас дать ему подсказку. Поэтому вы можете просто перейти к преобразованию аргумента перед вызовом функции.

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

Ответ 7

Как насчет передачи возвращаемого типа в качестве параметра с помощью указателей?

void AddNumbers(int a, int b, float *ret){
  *ret = (float)(a + b);
}

void AddNumbers(int a, int b, int *ret){
  *ret = (int)(a + b);
}

Вызов: это будет примерно так:

int a;
float b;
AddNumbers(1, 2, &a);
AddNumbers(1, 2, &b);