Подтвердить что ты не робот

"Двухуровневый" аргумент аргумента метода с делегатом

Рассмотрим следующий пример:

class Test
{
    public void Fun<T>(Func<T, T> f)
    {
    }

    public string Fun2(string test)
    {
        return ""; 
    }

    public Test()
    {
        Fun<string>(Fun2);
    }
}

Это хорошо компилируется.

Интересно, почему я не могу удалить аргумент <string> generic? Я получаю сообщение об ошибке, из-за которого он не может быть выведен из использования.

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

Я хотел бы объяснить это поведение.

Изменить ответ на вопрос Джона Ханны:

Тогда почему это работает?

class Test
{
    public void Fun<T1, T2>(T1 a, Func<T1, T2> f)
    {
    }

    public string Fun2(int test)
    {
        return test.ToString(); 
    }

    public Test()
    {
        Fun(0, Fun2);
    }
}

Здесь я связываю только один параметр с T1 a, но T2 кажется сложным.

4b9b3361

Ответ 1

Он не может вывести тип, потому что тип здесь не определен.

Fun2 не является Func<string, string>, но может быть назначено Func<string, string>.

Итак, если вы используете:

public Test()
{
  Func<string, string> del = Fun2;
  Fun(del);
}

Или:

public Test()
{
  Fun((Func<string, string>)Fun2);
}

Затем вы явно создаете Func<string, string> из Fun2, и общий тип-вывод работает соответственно.

И наоборот, когда вы делаете:

public Test()
{
  Fun<string>(Fun2);
}

Тогда набор перегрузок Fun<string> содержит только один, который принимает Func<string, string>, и компилятор может сделать вывод, что вы хотите использовать Fun2 как таковой.

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

(Стоит учесть, что в .NET 1.0 не только были делегаты не общие, поэтому вам пришлось бы определять delgate string MyDelegate(string test) -but, но также было необходимо создать объект с помощью конструктора Fun(new MyDelegate(Fun2)). Синтаксис изменился использовать делегатов проще несколькими способами, но неявное использование Fun2 as Func<string, string> по-прежнему является конструкцией объекта-делегата за кадром).

Тогда почему это работает?

class Test
{
    public void Fun<T1, T2>(T1 a, Func<T1, T2> f)
    {
    }

    public string Fun2(int test)
    {
        return test.ToString(); 
    }

    public Test()
    {
        Fun(0, Fun2);
    }
}

Потому что тогда он может вывести, чтобы:

  • T1 - int.
  • Fun2 присваивается Func<int, T2> для некоторого T2.
  • Fun2 может быть назначен Func<int, T2>, если T2 - string. Поэтому T2 является строкой.

В частности, возвращаемый тип Func может быть выведен из функции после того, как у вас есть типы аргументов. Это так же хорошо (и стоит усилий со стороны компилятора), потому что это важно в Linq Select. Это вызывает связанный случай, тот факт, что с помощью всего x.Select(i => i.ToString()) нам не хватает информации, чтобы узнать, к чему прикасается лямбда. Как только мы узнаем, что x IEnumerable<T> или IQueryable<T>, мы знаем, что у нас либо есть Func<T, ?>, либо Expression<Func<T, ?>>, а остальное можно сделать здесь.

Здесь также стоит отметить, что вывод возвращаемого типа не зависит от двусмысленности, которая выводит другие типы. Подумайте, были ли у нас оба Fun2 (тот, который принимает string и тот, который принимает int) в том же классе. Это допустимая перегрузка С#, но делает вывод о типе Func<T, string>, который Fun2 можно отличить до невозможности; оба действительны.

Однако, в то время как .NET разрешает перегрузку по типу возврата, С# этого не делает. Таким образом, никакая действительная программа С# не может быть неоднозначной для возвращаемого типа Func<T, TResult>, созданного методом (или лямбда) после определения типа T. Эта относительная легкость, в сочетании с большой полезностью, делает ее компилятором для нас.

Ответ 2

Вы хотите, чтобы компилятор выводил string из Fun2, что слишком сильно связано с компилятором С#. Это связано с тем, что он видит Fun2 как группу методов, а не делегат, если ссылается на Test.

Если вы измените код для передачи в параметре, который требуется Fun2 и вызовите его из Fun, тогда необходимость исчезнет, ​​так как теперь у вас есть параметр string, позволяющий выводить тип:

class Test
{
    public void Fun<T>(Func<T, T> f, T x)
    {
        f(x);
    }

    public string Fun2(string test)
    {
        return test;
    }

    public Test()
    {
        Fun(Fun2, "");
    }
}

Чтобы ответить на вашу отредактированную версию вопроса, предоставив тип T1, который вы предоставили компилятору - с помощью Fun(0, Fun2); вызова - дополнительная информация. Теперь он знает, что ему нужен метод из группы методов Fun2, который имеет параметр T1, в данном случае int. Это сужает его до одного метода, и поэтому он может сделать вывод о том, какой из них использовать.