【问题标题】:How does adding a break in a while loop resolve overload ambiguity?在 while 循环中添加中断如何解决重载歧义?
【发布时间】:2014-09-17 19:12:46
【问题描述】:

考虑一下这个 Reactive Extensions sn-p(忽略它的实用性):

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

这不能与 Reactive Extensions 2.2.5 一起编译(使用 NuGet Rx-Main 包)。它失败了:

错误 1 ​​以下方法或属性之间的调用不明确:'System.Reactive.Linq.Observable.Create(System.Func,System.Threading.Tasks.Task>)' 和 'System.Reactive.Linq.Observable.Create(System.Func,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>
{
}

忽略其中的 Reactive Extensions 部分,为什么添加 break 有助于 C# 编译器解决歧义?这如何用 C# 规范中的重载决议规则来描述?

我正在使用面向 4.5.1 的 Visual Studio 2013 Update 2。

【问题讨论】:

  • @Mic1780:什么时候编译器在检测到无限循环时会失败?以前从未见过……
  • @Mic1780 不知道你在说什么。 C# 编译器(实际上是大多数编译器)会愉快地编译一个无限循环,甚至像 while(true) {} 这样简单

标签: c#


【解决方案1】:

这里最简单的方法是拉出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&lt;T&gt;,它们都总是返回something。当存在可以匹配的 lambda 时,重载解决方案的“更好”算法将更喜欢返回值的委托而不是 void 委托,但是在您的情况下,两个重载都有返回值的委托,事实上对于 @返回 Task 而不是 Task&lt;T&gt; 的 987654331@ 方法在概念上等同于不返回值,并未包含在该更好算法中。因此,非异步等效项不会导致歧义错误,即使两个重载都适用。

当然值得注意的是,编写程序来确定任意代码块是否会完成是一个众所周知的无法解决的问题,但是,虽然编译器无法正确评估每个 sn-p 是否会完成,但它可以证明,在某些简单的情况下,例如这个,代码实际上永远不会完成。因此,有一些编写代码的方法显然(对你和我而言)永远不会完成,但编译器会将其视为可能完成。

【讨论】:

  • 虽然async 有一些特定的东西:给定void Foo(Action a); void Foo(Func f);Foo(() =&gt; { throw new Exception(); }); }不是 模棱两可的,将调用第二个重载:语言规范偏爱具有返回类型。但是async 函数可以有一个返回类型,即使它们实际上似乎没有返回一个具体的值:它仍然可以有一个Task 返回类型。
  • @hvd 对,在一段中添加了该内容。
  • 有趣。所以while(this.PropertyIsAlwaysTrue) {}(或MethodReturnsTrue())将再次解析为具有固定返回值的方法(因为编译器无法知道该属性将返回什么)。
  • @knittl C# 编译器一次只对单个成员块进行语义分析。它永远不会查看多个不同方法/属性的来源以确定如何编译其中一个。从理论上讲,它可能只是这样做会变得非常困难,非常快,所以我们决定不值得为你所得到的付出付出这么大的努力。
【解决方案2】:

离开 async 开始...

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

如果没有中断,lambda 表达式的结尾将无法到达,因此 any 返回类型将是有效的。例如,这很好:

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

而这不是:

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

因此,如果没有break,lambda 表达式将可以转换为具有单个参数的任何委托类型。对于break,lambda 表达式可转换为具有单个参数和返回类型为void 的委托类型。

添加async 部分,void 变为voidTask,而voidTaskTask&lt;T&gt; 用于任何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; } };

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-08-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-12-20
    • 2019-02-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多