在while循环中添加一个break语句如何解决重载歧义问题?

48

考虑以下响应式扩展的代码片段(忽略其实用性):

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

使用NuGet Rx-Main包的Reactive Extensions 2.2.5无法编译。以下错误发生:

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

然而,在while循环中的任何位置添加一个break都可以解决编译错误:

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

即使您不使用响应式扩展,也可以复制该问题(如果您想尝试不使用 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>
{
}

除去响应式扩展的部分,为什么添加break可以帮助C#编译器解决歧义问题?如何用C#规范中的重载解析规则来描述这一点?

我使用的是针对4.5.1版本的Visual Studio 2013 Update 2。


11
自从编译器能够检测到无限循环以来,我就没见过编译器会失败。但是我了解您的意思,编译器通常只会卡在无限循环的代码行上而不是崩溃。 - knittl
1
@Mic1780 不知道你在说什么。C#编译器(实际上,大多数编译器)会愉快地编译一个无限循环,即使是像 while(true) {} 这样简单的循环。 - tnw
2个回答

59

在这里,最简单的做法是将 async 和 lambda 表达式分离出来,因为这样可以强调正在发生的事情。这两种方法都是有效的,且可以编译:

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>。当存在可以匹配两者的lambda表达式时,重载解析的“更好”算法将优先选择返回值的委托,而不是返回void的委托。但是,在您的情况下,两个重载都具有返回值的委托,而async方法返回Task而不是Task<T>的概念等效于不返回值,但这种“优越性”算法中并未考虑。因此,非async版本不会导致二义性错误,即使两个重载都是适用的。

当然值得注意的是,编写一个程序来确定任意代码块是否将完成是一个著名的无解问题。但是,尽管编译器无法正确评估每个片段是否会完成,但它可以在某些简单情况下(如本例)证明代码实际上永远不会完成。因此,存在一些明显永远不会完成的代码编写方式,但编译器将把它们视为可能完成。


1
然而,对于async有一些特殊之处:给定void Foo(Action a); void Foo(Func f);Foo(() => { throw new Exception(); }); }并不是模棱两可的,并且将调用第二个重载:语言规范支持带返回类型的委托。但是,即使async函数似乎没有返回具体值,它们仍然可以具有返回类型,例如Task返回类型。 - user743382
@hvd 对,添加一个段落来概述那个。 - Servy
1
有趣。所以 while(this.PropertyIsAlwaysTrue) {}(或者 MethodReturnsTrue())将会再次解析为一个具有固定返回值的方法(因为编译器无法知道属性将返回什么)。 - knittl
1
@knittl C#编译器一次只会对单个成员块进行语义分析。它永远不会查看多个不同方法/属性的源代码以确定如何编译其中之一。理论上它可以这样做,但实际上这样做变得非常困难,所以决定不值得为了换取你所得到的东西而花费那么多的精力。 - Servy

25

先不考虑async...

使用break后,lambda表达式的结尾是可达的,因此lambda的返回类型必须是void

如果没有使用break,则lambda表达式的结尾是不可达的,因此任何返回类型都是有效的。例如,下面的代码也可以:

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

而这个不是:
Func<string> foo = () => { while(true) { break; } };

没有使用 break 的情况下,Lambda 表达式可以转换为具有单个参数的任何委托类型。加上 break 后,该 Lambda 表达式仅能转换为具有单个参数且返回类型为 void 的委托类型。
再加上 async 部分后,void 就成为了 voidTask ,而先前可以有任何返回类型,现在只能有 voidTask 或任何 T 类型的 Task<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; } };

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接