Как добавление разрыва в цикле 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.
Ответы
Ответ 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; } };