Универсальные методы в .NET не могут иметь свои возвращаемые типы. Зачем? - PullRequest
48 голосов
/ 08 июля 2010

Дано:

static TDest Gimme<TSource,TDest>(TSource source) 
{ 
    return default(TDest); 
}

Почему я не могу сделать:

string dest = Gimme(5);

без получения ошибки компилятора:

error CS0411: The type arguments for method 'Whatever.Gimme<TSource,TDest>(TSource)' cannot be inferred from the usage. Try specifying the type arguments explicitly.

5 можно вывести как int, но есть ограничение, при котором компилятор не может / не может разрешить возвращаемый тип как string. Я читал в нескольких местах, что это специально , но никакого реального объяснения нет. Я где-то читал, что это может измениться в C # 4, но это не так.

Кто-нибудь знает, почему возвращаемые типы не могут быть выведены из обобщенных методов? Это один из тех вопросов, ответ на который настолько очевиден, что смотрит вам в лицо? Я надеюсь, что нет!

Ответы [ 7 ]

86 голосов
/ 08 июля 2010

Общий принцип здесь заключается в том, что информация о типах передается только «в одну сторону»: от внутри до вне выражения.Пример, который вы приводите, предельно прост.Предположим, что мы хотим иметь поток информации о типе «в обе стороны» при выполнении вывода типа для метода R G<A, R>(A a), и рассмотрим некоторые из сумасшедших сценариев, которые создают:

N(G(5))

Предположим, существует десять различных перегрузок Nкаждый с различным типом аргумента.Должны ли мы сделать десять различных выводов для R?Если бы мы это сделали, должны ли мы каким-то образом выбрать «лучший» вариант?

double x = b ? G(5) : 123;

Каким должен быть выводной тип G?Int, потому что другая половина условного выражения является int?Или двойной, потому что в конечном итоге эта вещь будет назначена двойной?Теперь, возможно, вы начинаете видеть, как это происходит;если вы собираетесь сказать, что вы рассуждаете снаружи внутрь как далеко вы идете ?На этом пути может быть много шагов.Посмотрите, что произойдет, когда мы начнем объединять их:

N(b ? G(5) : 123)

Что нам теперь делать?У нас есть десять перегрузок N на выбор.Мы говорим, что R это int?Это может быть int или любой тип, в который int неявно конвертируется.Но из этих типов, какие из них неявно преобразуются в тип аргумента N?Пишем ли мы себе небольшую прологическую программу и попросим движок пролога решить, каковы все возможные типы возврата, которыми может быть R, чтобы удовлетворить каждую из возможных перегрузок на N, и затем каким-то образом выбрать лучший?

(Я не шучу; есть языки, которые по сути делают пишут небольшую прологическую программу, а затем используют логический движок, чтобы выяснить, что это за типы. Например, F #,делает намного более сложный вывод типов, чем C #. Система типов Haskell на самом деле Turing Complete: вы можете кодировать произвольно сложные проблемы в системе типов и просить компилятор их решать. Как мы увидим позже, то же самое верно и для разрешения перегрузки.в C # - вы не можете кодировать проблему остановки в системе типов C #, как в Haskell, но вы можете кодировать проблемы NP-HARD в проблемы с разрешением перегрузки.)

Это все еще очень простое выражение.Предположим, у вас было что-то вроде

N(N(b ? G(5) * G("hello") : 123));

Теперь мы должны решить эту проблему несколько раз для G, и, возможно, также для N, и мы должны решить их в комбинации .У нас есть пять проблем с разрешением перегрузки, и все из них, если честно, должны учитывать как свои аргументы, так и тип контекста.Если есть десять возможностей для N, то потенциально есть сотни вариантов для N (N (...)) и тысяча для N (N (N (...))), и очень быстро вы получите решениепроблемы, которые легко имели миллиарды возможных комбинаций и делали компилятор очень медленным.

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

Обратите внимание, чтоинформация о типах для лямбд движется в обоих направлениях!Если вы скажете N(x=>x.Length), то, конечно же, мы рассмотрим все возможные перегрузки N, которые имеют аргументы типа функции или выражения и опробуем все возможные типы для x.И, конечно же, есть ситуации, в которых вы можете легко заставить компилятор испытать миллиарды возможных комбинаций , чтобы найти уникальную комбинацию, которая работает.Правила вывода типов, которые делают это возможным для универсальных методов, чрезвычайно сложны и даже нервируют Джона Скита. Эта функция делает разрешение перегрузки NP-HARD .

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

7 голосов
/ 08 июля 2010

Вы должны сделать:

string dest = Gimme<int, string>(5);

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

System.String - плохой пример, потому что это запечатанный класс, но говорят, что это не так. Как мог компилятор знать, что вам не нужен один из его подклассов, если вы не указали тип в вызове?

Возьмите этот пример:

System.Windows.Forms.Control dest = Gimme(5);

Как бы компилятор узнал, какой элемент управления на самом деле сделать? Вам нужно указать это так:

System.Windows.Forms.Control dest = Gimme<int, System.Windows.Forms.Button>(5);
5 голосов
/ 08 июля 2010

Вызов Gimme(5) игнорирование возвращаемого значения является юридическим утверждением, как компилятор узнает, какой тип возвращать?

1 голос
/ 08 июля 2010

Наверное, это было дизайнерское решение.Я также считаю это полезным при программировании на Java.

В отличие от Java, C #, похоже, эволюционирует в сторону функционального языка программирования, и вы можете получить вывод типа наоборот, поэтому вы можете иметь:

var dest = Gimme<int, string>(5);

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

0 голосов
/ 02 августа 2018
public class ReturnString : IReq<string>
{
}

public class ReturnInt : IReq<int>
{
}

public interface IReq<T>
{
}

public class Handler
{
    public T MakeRequest<T>(IReq<T> requestObject)
    {
        return default(T);
    }
}

var handler = new Handler();
string stringResponse = handler.MakeRequest(new ReturnString());
int intResponse = handler.MakeRequest(new ReturnInt());
0 голосов
/ 12 марта 2018

Я использую эту технику, когда мне нужно сделать что-то подобное:

static void Gimme<T>(out T myVariable)
{
    myVariable = default(T);
}

и использовать его так:

Gimme(out int myVariable);
Print(myVariable); //myVariable is already declared and usable.

Обратите внимание, что встроенное объявление переменных out доступно, так какC # 7.0

0 голосов
/ 08 июля 2010

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

...