Проблемы с перегрузкой метода С# в Visual Studio 2013

Наличие этих трех методов в библиотеке Rx.NET

public static IObservable<TResult> Create<TResult>(Func<IObserver<TResult>, CancellationToken, Task> subscribeAsync) {...}
public static IObservable<TResult> Create<TResult>(Func<IObserver<TResult>, CancellationToken, Task<IDisposable>> subscribeAsync) {...}
public static IObservable<TResult> Create<TResult>(Func<IObserver<TResult>, CancellationToken, Task<Action>> subscribeAsync) {...}

Я пишу следующий пример кода в MSVS 2013:

var sequence =
  Observable.Create<int>( async ( observer, token ) =>
                          {
                            while ( true )
                            {
                              token.ThrowIfCancellationRequested();
                              await Task.Delay( 100, token );
                              observer.OnNext( 0 );
                            }
                          } );

Это не скомпилируется из-за неоднозначных перегрузок. Точный вывод компилятора:

Error    1    The call is ambiguous between the following methods or properties: 
'System.Reactive.Linq.Observable.Create<int>(System.Func<System.IObserver<int>,System.Threading.CancellationToken,System.Threading.Tasks.Task<System.Action>>)' 
and 
'System.Reactive.Linq.Observable.Create<int>(System.Func<System.IObserver<int>,System.Threading.CancellationToken,System.Threading.Tasks.Task>)'

Однако, как только я заменяю while( true ) с while( false ) или var condition = true; while( condition)... var condition = true; while( condition)...

var sequence =
  Observable.Create<int>( async ( observer, token ) =>
                          {                            
                            while ( false ) // It the only difference
                            {
                              token.ThrowIfCancellationRequested();
                              await Task.Delay( 100, token );
                              observer.OnNext( 0 );
                            }
                          } );

ошибка исчезает, и вызов метода разрешает следующее:

public static IObservable<TResult> Create<TResult>(Func<IObserver<TResult>, CancellationToken, Task> subscribeAsync) {...}

То, что там происходит?

Ответ 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.

Ответ 2

В дополнение к ответу от @Daisy Shipton я хотел бы добавить, что такое же поведение можно наблюдать и в следующем случае:

var sequence = Observable.Create<int>(
    async (observer, token) =>
    {
        throw new NotImplementedException();
    });

в основном по той же причине - компилятор видит, что функция лямбда никогда не возвращается, так что любой тип возврата будет соответствовать, что в свою очередь заставляет лямбда соответствовать любой из Observable.Create перегрузок.

И, наконец, пример простого решения: вы можете наложить лямбду на нужный тип подписи, чтобы намекнуть компилятору, который перегружает Rx.

var sequence =
    Observable.Create<int>(
        (Func<IObserver<int>, CancellationToken, Task>)(async (observer, token) =>
        {
            throw new NotImplementedException();
        })
      );