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

Как добавление разрыва в цикле while разрешает проблему перегрузки неоднозначно?

Рассмотрим этот фрагмент Reactive Extensions (игнорируйте его практичность):

return Observable.Create<string>(async observable =>
{
    while (true)
    {
    }
});

Это не компилируется с помощью Reactive Extensions 2.2.5 (с использованием пакета NuGet Rx-Main). Он не работает:

Ошибка 1 Вызов неоднозначен между следующими методами или свойствами: 'System.Reactive.Linq.Observable.Create <string> (System.Func < System.IObserver <string> , System.Threading.Tasks.Task < System.Action > ) 'и' System.Reactive.Linq.Observable.Create <string> (System.Func < System.IObserver <string> , System.Threading.Tasks.Task > ) '

Однако добавление break в любом месте цикла while исправляет ошибку компиляции:

return Observable.Create<string>(async observable =>
{
    while (true)
    {
        break;
    }
});

Проблема может быть воспроизведена без Reactive Extensions вообще (проще, если вы хотите попробовать ее без использования Rx):

class Program
{
    static void Main(string[] args)
    {
        Observable.Create<string>(async blah =>
        {
            while (true)
            {
                Console.WriteLine("foo.");
                break; //Remove this and the compiler will break
            }
        });
    }
}

public class Observable
{
    public static IObservable<TResult> Create<TResult>(Func<IObserver<TResult>, Task> subscribeAsync)
    {
        throw new Exception("Impl not important.");
    }

    public static IObservable<TResult> Create<TResult>(Func<IObserver<TResult>, Task<Action>> subscribeAsync)
    {
        throw new Exception("Impl not important.");
    }
}

public interface IObserver<T>
{
}

Игнорирование части Reactive Extensions, почему добавление break помогает компилятору С# разрешить неоднозначность? Как это можно описать с правилами разрешения перегрузки по спецификации С#?

Я использую Visual Studio 2013 Update 2 для таргетинга 4.5.1.

4b9b3361

Ответ 1

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

public static void Foo()
{
    while (true) { }
}
public static Action Foo()
{
    while (true) { }
}

Однако для этих двух методов:

public static void Foo()
{
    while (true) { break; }
}
public static Action Foo()
{
    while (true) { break; }
}

Первый компилятор, а второй - нет. Он имеет путь к коду, который не возвращает допустимое значение.

Фактически, while(true){} (вместе с throw new Exception();) является интересным утверждением в том, что он является действительным телом метода с любым возвращаемым типом.

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

Конечно, чтобы вернуть async обратно в игру, это действительно актуально в одном случае. Для методов async они оба всегда возвращают что-то, будь то Task или Task<T>. Алгоритмы "блеска" для разрешения перегрузки будут предпочтительнее делегатов, которые возвращают значение над делегатами void, когда есть лямбда, которая может соответствовать, но в вашем случае две перегрузки имеют делегаты, которые возвращают значение, тот факт, что для async, возвращающие Task вместо Task<T>, является концептуальным эквивалентом не возвращающего значения, не включается в этот алгоритм выживания. Из-за этого неасинхронный эквивалент не приведет к ошибке двусмысленности, даже если применимы обе перегрузки.

Конечно, стоит отметить, что писать программу, чтобы определить, будет ли когда-либо закончен произвольный блок кода, является, однако, неразрешимой проблемой, хотя компилятор не может правильно оценить, будет ли каждый отдельный фрагмент завершен, он может доказать, в определенных простых случаях, таких как этот, что код на самом деле никогда не будет завершен. Из-за этого существуют способы написания кода, который явно (для вас и для меня) никогда не завершится, но что компилятор будет рассматривать как возможно завершение.

Ответ 2

Оставив async из этого, чтобы начать с...

С разрывом конец лямбда-выражения достижим, поэтому возвращаемый тип лямбда должен быть void.

Без разрыва конец выражения лямбда недостижим, поэтому любой возвращаемый тип будет действителен. Например, это нормально:

Func<string> foo = () => { while(true); };

тогда как это не так:

Func<string> foo = () => { while(true) { break; } };

Таким образом, без break, выражение лямбда будет преобразовано в любой тип делегата с единственным параметром. С помощью break выражение лямбда преобразуется только в тип делегата с единственным параметром и типом возврата void.

Добавьте часть async и void станет void или Task, vs void, Task или Task<T> для любого T, где ранее вы могли бы иметь любой тип возврата. Например:

// Valid
Func<Task<string>> foo = async () => { while(true); };
// Invalid (it doesn't actually return a string)
Func<Task<string>> foo = async () => { while(true) { break; } };
// Valid
Func<Task> foo = async () => { while(true) { break; } };
// Valid
Action foo = async () => { while(true) { break; } };