Ответ 1
Это весело :) Есть несколько аспектов. Для начала упростите его очень значительно, удалив Rx и фактическое разрешение перегрузки с рисунка. Разрешение перегрузки обрабатывается в самом конце ответа.
Анонимная функция для делегирования конверсий и доступности
Разница здесь заключается в достижении конечной точки лямбда-выражения. Если это так, то это выражение лямбда ничего не возвращает, а выражение лямбда может быть преобразовано только в Func<Task>
. Если конечная точка лямбда-выражения недоступна, то она может быть преобразована в любой Func<Task<T>>
.
Форма выражения while
имеет значение из-за этой части спецификации С#. (Это из стандарта ECMA С# 5, другие версии могут иметь несколько другую формулировку для той же концепции.)
Конечная точка оператора
while
доступна, если выполняется хотя бы одно из следующих утверждений:
- Оператор
while
содержит допустимый оператор break, который завершает оператор while.- Оператор
while
доступен и булевское выражение не имеет постоянного значенияtrue
.
Когда у вас есть цикл while (true)
без инструкций break
, ни одна из них не является истиной, поэтому конечная точка оператора while
(и, следовательно, выражение лямбда в вашем случае) недоступна.
Здесь приведен короткий, но полный пример без участия Rx:
using System;
using System.Threading.Tasks;
public class Test
{
static void Main()
{
// Valid
Func<Task> t1 = async () => { while(true); };
// Valid: end of lambda is unreachable, so it fine to say
// it'll return an int when it gets to that end point.
Func<Task<int>> t2 = async () => { while(true); };
// Valid
Func<Task> t3 = async () => { while(false); };
// Invalid
Func<Task<int>> t4 = async () => { while(false); };
}
}
Мы можем упростить еще больше, удалив async из уравнения. Если у нас есть синхронное без параметрического лямбда-выражения без операторов return, которое всегда конвертируется в Action
, но оно также конвертируется в Func<T>
для любого T
если конец лямбда-выражения недоступен. Незначительное изменение приведенного выше кода:
using System;
public class Test
{
static void Main()
{
// Valid
Action t1 = () => { while(true); };
// Valid: end of lambda is unreachable, so it fine to say
// it'll return an int when it gets to that end point.
Func<int> t2 = () => { while(true); };
// Valid
Action t3 = () => { while(false); };
// Invalid
Func<int> t4 = () => { while(false); };
}
}
Мы можем рассмотреть это несколько иначе, удалив делегатов и лямбда-выражения из микса. Рассмотрим следующие методы:
void Method1()
{
while (true);
}
// Valid: end point is unreachable
int Method2()
{
while (true);
}
void Method3()
{
while (false);
}
// Invalid: end point is reachable
int Method4()
{
while (false);
}
Хотя метод ошибки для метода Method4
"не все пути кода возвращают значение", то, как это обнаружено, "конец метода доступен". Теперь представьте, что эти тела методов являются лямбда-выражениями, пытающимися удовлетворить делегат с той же сигнатурой, что и подпись метода, и мы вернемся ко второму примеру...
Развлечения с разрешением перегрузки
Как отметил Панайотис Канавос, первоначальная ошибка в отношении разрешения перегрузки не воспроизводится в Visual Studio 2017. Итак, что происходит? Опять же, нам не нужен Rx, чтобы проверить это. Но мы можем видеть очень странное поведение. Учти это:
using System;
using System.Threading.Tasks;
class Program
{
static void Foo(Func<Task> func) => Console.WriteLine("Foo1");
static void Foo(Func<Task<int>> func) => Console.WriteLine("Foo2");
static void Bar(Action action) => Console.WriteLine("Bar1");
static void Bar(Func<int> action) => Console.WriteLine("Bar2");
static void Main(string[] args)
{
Foo(async () => { while (true); });
Bar(() => { while (true) ; });
}
}
Это вызывает предупреждение (нет ожидающих операторов), но оно компилируется с помощью компилятора С# 7. Результат удивил меня:
Foo1
Bar2
Таким образом, разрешение для Foo
определяет, что преобразование в Func<Task>
лучше, чем преобразование в Func<Task<int>>
, тогда как разрешение для Bar
определяет, что преобразование в Func<int>
лучше, чем преобразование в Action
. Все преобразования справедливы - если вы закомментировать Foo1
и Bar2
методы, он все еще компилируется, но дает выход Foo2
, Bar1
.
С компилятором С# 5 вызов Foo
неоднозначен, Bar
вызов Bar
разрешает Bar2
, как и с компилятором С# 7.
С еще большим количеством исследований синхронная форма указана в 12.6.4.4 спецификации ECMA С# 5:
C1 является лучшим преобразованием, чем C2, если выполняется хотя бы одно из следующих условий:
- ...
- E - анонимная функция, T1 либо тип делегата D1, либо тип дерева выражений. Выражение, T2 - либо тип делегата D2, либо выражение типа выражения Expression и одно из следующих утверждений:
- D1 - лучшая цель преобразования, чем D2 (для нас это не имеет значения)
- D1 и D2 имеют одинаковые списки параметров, и выполняется одно из следующих действий:
- D1 имеет тип возврата Y1, а D2 имеет тип возвращаемого значения Y2, для D-типа возвращаемого типа X существует для E в контексте этого списка параметров (§12.6.3.13), а преобразование из X в Y1 лучше, чем преобразование из От X до Y2
- E является асинхронным, D1 имеет тип возврата
Task<Y1>
, а D2 имеет тип возвращаемого значенияTask<Y2>
, тип предполагаемого возвратаTask<X>
существует для E в контексте этого списка параметров (§12.6.3.13), и преобразование из X в Y1 лучше, чем преобразование из X в Y2- D1 имеет возвращаемый тип Y, а D2 недействителен
Таким образом, это имеет смысл для не-асинхронного случая - и также имеет смысл, как компилятор С# 5 не в состоянии разрешить двусмысленность, потому что эти правила не нарушают связь.
У нас пока нет полной спецификации С# 6 или С# 7, но есть проект. Его правила разрешения перегрузки выражаются несколько иначе, и изменение может быть где-то там.
Если он собирается что-либо скомпилировать, я ожидаю, что перегрузка Foo
принимающая Func<Task<int>>
будет выбрана над перегрузкой, принимающей Func<Task>
потому что это более конкретный тип. (Там ссылочное преобразование из Func<Task<int>>
в Func<Task>
, но не наоборот).
Обратите внимание, что выводимый тип возвращаемого выражения лямбда будет просто Func<Task>
в спецификациях С# 5 и Draft С# 6.
В конечном счете, разрешение перегрузки и вывод типа являются действительно трудными битами спецификации. Этот ответ объясняет, почему цикл while(true)
имеет значение (потому что без него перегрузка, принимающая func, возвращающая Task<T>
даже не применима), но я дошел до конца, что я могу решить выбор компилятора С# 7.