Visual Studio 2013中C#方法重载解析问题

31

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 sequence =
  Observable.Create<int>( async ( observer, token ) =>
                          {                            
                            while ( false ) // It's the only difference
                            {
                              token.ThrowIfCancellationRequested();
                              await Task.Delay( 100, token );
                              observer.OnNext( 0 );
                            }
                          } );

错误消失,方法调用解析为this:

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

那里发生了什么?


2
当代码中出现 while(true) 时,编译器面对的是一个它知道永远不会返回的方法。我认为这会干扰编译器试图确定该方法返回类型的部分。 - Damien_The_Unbeliever
编译器太聪明了。由于您有一个始终为false的while循环,编译器不会给出错误。错误消息1显示开放和关闭角括号数量不相等。 - jdweng
@jdweng:这里的特殊情况不是“一个始终为假的while循环”。对于问题,这等同于“条件有时为假,有时为真的while循环”。而是“一个始终为真且没有任何break语句的while循环”,这才是特殊情况,使得while循环的结束不可达。 - Jon Skeet
@PanagiotisKanavos:但是VS2015是C# 6。你怎么试着用C# 5和Roslyn呢?我仍然认为这里没有实际的编译器错误-只有不同的重载解析规则。我仍在调查中。 - Jon Skeet
@DaisyShipton,你可以在VS 2017的“生成”选项卡中切换语言。我安装了15.7.3版本,它还修复了关于方法组的额外分辨率问题。在这种情况下,正确的重载显然是接受一个操作的那个。如果编译器混淆了,那显然是一个bug。 - Panagiotis Kanavos
显示剩余5条评论
2个回答

32
这是一个有趣的问题 :) 它有多个方面。首先,让我们通过从图片中删除Rx和实际重载分辨率来大大简化它。重载分辨率在答案的最后处理。
匿名函数转换为委托,并且可达性
区别在于lambda表达式的末端是否可达。如果是,则lambda表达式不返回任何内容,并且lambda表达式只能转换为Func。如果lambda表达式的末端不可达,则可以将其转换为任何Func>。
while语句的形式有所不同,因为C#规范的这一部分。 (这是来自ECMA C# 5标准的;其他版本可能对相同概念的措辞略有不同。)
当您使用没有break语句的while(true)循环时,两个项目都不成立,因此while语句的终点(因此在您的情况下lambda表达式)不可达。
以下是一个没有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's 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); };
    }
}

我们可以进一步简化,从方程式中移除异步操作。如果我们有一个同步无参数的lambda表达式且没有返回语句,那么它总是可以转换为Action,但如果lambda表达式的末尾是不可达到的,则也可以转换为任何TFunc<T>。以上代码稍作改变:
using System;

public class Test
{
    static void Main()
    {
        // Valid
        Action t1 = () => { while(true); };

        // Valid: end of lambda is unreachable, so it's 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); };
    }
}

如果我们从混合中删除委托和Lambda表达式,我们可以以稍微不同的方式来看待它。请考虑以下方法:

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的错误方法是“不是所有代码路径都返回值”,但检测到这种情况的方式是“方法的结尾可达”。现在想象一下,这些方法体是lambda表达式,试图满足与方法签名相同的委托,我们回到了第二个例子...
重载解析的有趣玩法
正如Panagiotis Kanavos所指出的那样,在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) ; });
    }
}

这会发出一个警告(没有await运算符),但它可以在C# 7编译器中编译。输出结果让我感到惊讶:

Foo1
Bar2

因此,Foo的解析是确定转换为Func<Task>比转换为Func<Task<int>>更好,而Bar的解析是确定转换为Func<int>比转换为Action更好。所有转换都是有效的-如果您注释掉Foo1Bar2方法,则仍会编译,但会输出Foo2Bar1

使用C# 5编译器,Foo调用由于Bar调用解析为Bar2而不明确。

通过更多的研究,同步形式在ECMA C# 5规范的12.6.4.4中指定:

如果满足以下至少一项条件,则C1比C2更好的转换:

  • ...
  • E是匿名函数,T1是委托类型D1或表达式树类型Expression之一,T2是委托类型D2或表达式树类型Expression之一,并且满足以下情况之一:
    • D1是比D2更好的转换目标(对我们不相关)
    • D1和D2具有相同的参数列表,并且满足以下情况之一:
    • D1具有返回类型Y1,D2具有返回类型Y2,在该参数列表的上下文中存在E的推断返回类型X(§12.6.3.13),并且从X到Y1的转换优于从X到Y2的转换
    • E是异步的,D1具有返回类型Task<Y1>,D2具有返回类型Task<Y2>,在该参数列表的上下文中存在E的推断返回类型Task<X>(§12.6.3.13),并且从X到Y1的转换优于从X到Y2的转换
    • D1具有返回类型Y,D2返回void

因此,这对于非异步情况是有意义的-对于C# 5编译器无法解决歧义的方式也是有意义的,因为这些规则不会打破平局。

我们还没有完整的C# 6或C# 7规范,但有草案可用。它的重载解析规则表达方式略有不同,而更改可能在其中某个地方。

如果需要编译,我会预期选择接受 Func<Task<int>>Foo 重载,而不是接受 Func<Task> 的重载 - 因为它是更具体的类型。(从 Func<Task<int>>Func<Task> 存在引用转换,但反之则不然。)
请注意,在 lambda 表达式中的推断返回类型在 C# 5 和草案 C# 6 规范中都只是 Func<Task>
最终,重载决议和类型推断是规范中非常困难的部分。这个答案解释了为什么 while(true) 循环会产生影响(因为没有它,接受返回 Task<T> 的函数重载甚至都不可用),但我已经到达了我能够理解 C# 7 编译器做出的选择的极限。

我无法在Visual Studio 2017中重现这个问题。这是预-Roslyn编译器的一个副产品,而不是语言本身的问题。 - Panagiotis Kanavos
1
@PanagiotisKanavos:Roslyn并不总是完全遵循规范,因此仅凭这一观察并不能确定哪个编译器“有问题”,或者规范是否需要调整。(我甚至没有触及规范现在已经永久过时的事实,因为编译器的发展速度比它能够被调整的速度更快...) - Jeroen Mostert
所以,对于Foo来说,决定将其转换为Func<Task<int>>比转换为Func<Task>更好,而对于Bar来说,决定将其转换为Action比转换为Func<int>更好。虽然我认为应该是这样(没有手头的ide进行测试),但您的代码/输出显示完全相反...我是错过了什么还是只是混淆了。Foo1是Func<Task>版本... - René Vogt
@RenéVogt:你没有错过任何东西——我在输入时感到困惑。我会修复它。 - Jon Skeet

5

除了 @Daisy Shipton 的回答之外,我还想补充一下,在以下情况下也可以观察到相同的行为:

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

基本上是因为同样的原因 - 编译器发现 lambda 函数永远不会返回,因此任何返回类型都会匹配,这反过来又使 lambda 匹配 Observable.Create 的任何重载。

最后,一个简单解决方案的例子:您可以将 lambda 强制转换为所需的签名类型,以提示编译器选择哪个 Rx 重载。

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

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