Вложенные генераторы: почему компилятор не может вывести аргументы типа в этом случае?

Я играл с проектом хобби, когда сталкивался с ошибкой типа-вывода, которую я не понимал. Я упростил его на следующий тривиальный пример.

У меня есть следующие классы и функции:

class Foo { }
class Bar { }
class Baz { }

static T2 F<T1, T2>(Func<T1, T2> f) { return default(T2); }
static T3 G<T1, T2, T3>(Func<T1, Func<T2, T3>> f) { return default(T3); }

Теперь рассмотрим следующие примеры:

// 1. F with explicit type arguments - Fine
F<Foo, Bar>(x => new Bar());

// 2. F with implicit type arguments - Also fine, compiler infers <Foo, Bar>
F((Foo x) => new Bar());

// 3. G with explicit type arguments - Still fine...
G<Foo, Bar, Baz>(x => y => new Baz());

// 4. G with implicit type arguments - Bang!
// Compiler error: Type arguments cannot be inferred from usage
G((Foo x) => (Bar y) => new Baz());

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

ВОПРОС: Почему компилятор не может указать <Foo, Bar, Baz> в этом случае?

ОБНОВЛЕНИЕ: Я обнаружил, что простое перенос второго лямбда в функцию идентификации заставит компилятор правильно выводить все типы:

static Func<T1, T2> I<T1, T2>(Func<T1, T2> f) { return f; }

// Infers G<Foo, Bar, Baz> and I<Bar, Baz>
G((Foo x) => I((Bar y) => new Baz()));

Почему он может делать все отдельные шаги совершенно, но не весь вывод сразу? Есть ли какая-то тонкость в том порядке, в котором компилятор анализирует неявные типы лямбда и неявные общие типы?

Ответы

Ответ 1

Поскольку алгоритм, описанный в спецификации С#, в этом случае не преуспевает. Давайте посмотрим на спецификацию, чтобы понять, почему это так.

Описание алгоритма длинное и сложное, поэтому я сильно сократил это.

Соответствующие типы, упомянутые в алгоритме, имеют для вас следующие значения:

  • Eᵢ= анонимный lambda (Foo x) => (Bar y) => new Baz()
  • Tᵢ= тип параметра (Func<T1, Func<T2, T3>>)
  • Xᵢ= три общих типа параметров (T1, T2, T3)

Во-первых, theres первая фаза, которая в вашем случае делает только одно:

7.5.2.1 Первая фаза

Для каждого из аргументов метода Eᵢ (в вашем случае, только один, лямбда):

  • Если Eᵢ является анонимной функцией [это], вывод явного типа параметра (§7.5.2.7) производится от Eᵢ до Tᵢ
  • В противном случае [не относится]
  • В противном случае [не относится]
  • В противном случае для этого аргумента не делается никаких указаний.

Я пропустил подробности вывода явного типа параметра здесь; достаточно сказать, что для вызова G((Foo x) => (Bar y) => new Baz()) он указывает на то, что T1= Foo.

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

7.5.2.2 Вторая фаза

Вторая фаза выполняется следующим образом:

  • [...]
  • В противном случае для всех аргументов Eᵢ с соответствующим типом параметра Tᵢ, где выходные типы (§7.5.2.4) содержат переменные незафиксированного типа Xj, но входные типы (§7.5.2.3) не имеют выхода (§7.5.2.6) производится от Eᵢ до Tᵢ. Затем повторяется вторая фаза.

[Переведено и применено к вашему делу, это означает:

  • В противном случае, если тип возврата делегата (т.е. Func<T2,T3>) содержит еще неопределенную переменную типа (она делает), но ее типы параметров (т.е. T1) не (они этого не делают, мы уже знаем, что T1= Foo), выводится вывод выходного типа (§7.5.2.6).]

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

7.5.2.6 Выводы выходного типа

Вывод выходного типа производится из выражения E в тип T следующим образом:

  • Если E является анонимной функцией [с указанным типом возврата U (§7.5.2.12) и T является типом типа делегата или дерева выражений с типом возврата Tb, то нижняя (§7.5.2.9) производится от U до Tb.
  • В противном случае, [rest snipped]

"Предполагаемый тип возврата" U - это анонимный lambda (Bar y) => new Baz() и Tb is Func<T2,T3>. Вывод нижней границы.

Я не думаю, что мне нужно процитировать весь алгоритм нижнего предела вывода (его длинный); достаточно сказать, что он не упоминает анонимные функции. Он заботится о взаимоотношениях наследования, реализации интерфейса, ковариации массивов, интерфейсе и делегировании co/contravariance,... но не lambdas. Следовательно, применяется его последняя маркерная точка:

  • В противном случае никаких выводов не делается.

Затем мы возвращаемся ко второй фазе, которая отказывается, потому что не было сделано никаких указаний для T2 и T3.

Мораль истории: алгоритм вывода типа не рекурсивен с lambdas. Он может только выводить типы из параметра и возвращать типы внешней лямбды, а не лямбда внутри. Только нижний вывод является рекурсивным (так что он может принимать вложенные общие конструкции, такие как List<Tuple<List<T1>, T2>> в отдельности), но ни вывода выходных типов (§7.5.2.6), ни явные выражения типа параметра (§7.5.2.7) не являются рекурсивными и никогда не применяются к внутренним лямбдам.

Добавление

Когда вы добавляете вызов этой функции идентификации I:

  • G((Foo x) => I((Bar y) => new Baz()));

тогда вывод типа сначала применяется к вызову I, что приводит к тому, что тип возврата I s выводится как Func<Bar, Baz>. Тогда "предполагаемый тип возврата" U внешней лямбда является типом делегирования Func<Bar, Baz> и Tb равен Func<T2, T3>. Таким образом, вывод с нижней границей будет успешным, поскольку он будет сталкиваться с двумя явными типами делегатов (Func<Bar, Baz> и Func<T2, T3>), но без анонимных функций /lambdas. Вот почему функция идентификации делает ее успешной.

Ответ 2

Лямбда не может быть выведена, что она возвращает тип, поскольку она не назначена и не может быть определена компилятором. Проверьте ссылку о том, как типы возврата lambdas определяются компилятором. Если бы вы having:

Func<Bar, Baz> f = (Bar y) => new Baz();
G((Foo x) => f);

тогда компилятор мог бы вычислить возвращаемый тип лямбда на основании того, для чего он назначен, но поскольку теперь он не назначен ни на что, компилятор пытается определить, какой тип возврата для (Bar y) => new Baz(); будет.

Ответ 3

Для компилятора функция лямбда отличается от Func, т.е. использование лямбда-функции для Func подразумевает преобразование типа. Компилятор не выполняет преобразования "вложенных" типов при специализировании дженериков. Это, однако, потребуется в вашем примере:

Тип (Foo x) => (Bar y) => new Baz () равен lambda (Foo, lambda (Bar, Baz)), но требуется Func (T1, Func (T2, T3)), т.е. два преобразования, которые являются вложенными.