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

Moo*_*ooh 31 c# rx.net

在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) {...}
Run Code Online (Sandbox Code Playgroud)

我在MSVS 2013中编写以下示例代码:

var sequence =
  Observable.Create<int>( async ( observer, token ) =>
                          {
                            while ( true )
                            {
                              token.ThrowIfCancellationRequested();
                              await Task.Delay( 100, token );
                              observer.OnNext( 0 );
                            }
                          } );
Run Code Online (Sandbox Code Playgroud)

由于模糊的重载,这不会编译.编译器的确切输出是:

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>)'
Run Code Online (Sandbox Code Playgroud)

但是只要我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 );
                            }
                          } );
Run Code Online (Sandbox Code Playgroud)

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

public static IObservable<TResult> Create<TResult>(Func<IObserver<TResult>, CancellationToken, Task> subscribeAsync) {...}
Run Code Online (Sandbox Code Playgroud)

那里发生了什么?

Jon*_*eet 31

这是一个有趣的:)它有多个方面.首先,让我们通过从图片中删除Rx和实际重载分辨率来非常简化它.在答案的最后处理过载分辨率.

用于委派转换和可访问性的匿名函数

这里的区别在于lambda表达式的终点是否可达.如果是,则该lambda表达式不返回任何内容,并且lambda表达式只能转换为a Func<Task>.如果无法访问lambda表达式的端点,则可以将其转换为any Func<Task<T>>.

while由于C#规范的这一部分,语句的形式有所不同.(这来自ECMA C#5标准;其他版本可能对同一概念的措辞略有不同.)

while如果至少满足下列条件之一,则可以访问语句的结束点:

  • while语句包含一个可到达的break语句,该语句退出while语句.
  • while语句是可访问的,并且布尔表达式没有常量值true.

当你有一个while (true)没有break语句的循环时,bullet都不是真的,所以while语句的结束点(以及你的case中的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); };
    }
}
Run Code Online (Sandbox Code Playgroud)

我们可以通过从等式中删除异步来进一步简化.如果我们有一个没有return语句的同步无参数lambda表达式,那么它总是可以转换为Action,但如果lambda表达式的末尾不可达,它也可以转换Func<T>为any 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); };
    }
}
Run Code Online (Sandbox Code Playgroud)

我们可以通过从混合中删除委托和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);
}
Run Code Online (Sandbox Code Playgroud)

虽然错误方法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) ; });
    }
}
Run Code Online (Sandbox Code Playgroud)

这会发出一个警告(没有等待运算符)但它会编译C#7编译器.输出让我感到惊讶:

Foo1
Bar2
Run Code Online (Sandbox Code Playgroud)

因此,分辨率Foo确定转换为转换Func<Task>为优于转换为Func<Task<int>>,而分辨率Bar确定转换为转换Func<int>为优于转换为Action.所有转换都是有效的 - 如果你注释掉Foo1Bar2方法,它仍然编译,但给出的输出Foo2,Bar1.

使用C#5编译器,调用解析时Foo调用是不明确的,就像使用C#7编译器一样.BarBar2

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

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

  • ...
  • E是一个匿名函数,T1是委托类型D1或表达式树类型表达式,T2是委托类型D2或表达式树类型表达式,并且以下之一成立:
    • D1是比D2更好的转换目标(与我们无关)
    • D1和D2具有相同的参数列表,并且具有以下特征之一:
    • D1具有返回类型Y1,并且D2具有返回类型Y2,在该参数列表(第12.6.3.13节)的上下文中存在针对E的推断返回类型X,并且从X到Y1的转换优于从X到Y2
    • E是异步,D1具有返回类型Task<Y1>,D2具有返回类型Task<Y2>,Task<X>在该参数列表(第12.6.3.13节)的上下文中存在E 的推断返回类型,并且从X到Y1的转换优于转换从X到Y2
    • D1具有返回类型Y,并且D2返回空白

因此,对于非异步情况也是有意义的 - 而且C#5编译器无法解决模糊性也是有意义的,因为这些规则不能打破这种关系.

我们还没有完整的C#6或C#7规范,但是有一个可用草案.它的重载决策规则表达方式有所不同,并且可能存在某些变化.

如果它要编译成任何东西,我会期望在Foo超载接受时Func<Task<int>>选择超载接受a Func<Task>- 因为它是一种更具体的类型.(有从基准转换Func<Task<int>>Func<Task>,反之则不行.)

请注意,lambda表达式的推断返回类型只是Func<Task>在C#5和草案C#6规范中.

最终,重载决策和类型推断实际上是规范的难点.这个答案解释了为什么while(true)循环有所不同(因为没有它,接受func返回a的重载Task<T>甚至不适用)但是我已经达到了我可以解决的关于C#7编译器选择的结果.


arm*_*enm 5

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

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

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

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

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