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 );
                            }
                          } );

错误消失了,方法调用解决了这个问题:

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

那里发生了什么?

4

2 回答 2

31

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

委托转换和可达性的匿名函数

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

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

while如果以下至少一项为真,则语句的终点是可到达的:

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

当您有一个while (true)没有语句的循环时break,两个项目符号都不是真的,因此语句的终点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); };
    }
}

我们可以通过从等式中删除 async 来进一步简化。如果我们有一个没有 return 语句的同步无参数 lambda 表达式,它总是可以转换为Action,但如果 lambda 表达式的结尾不可到达,它也可以转换为Func<T>for 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); };
    }
}

我们可以通过从混合中删除委托和 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。所有的转换都是有效的——如果你注释掉Foo1andBar2方法,它仍然可以编译,但是输出Foo2, Bar1

在 C# 5 编译器中,由于调用解析为,所以Foo调用是不明确的,就像使用 C# 7 编译器一样。BarBar2

通过更多研究,同步形式在 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 规范,但有一个可用的草稿。它的重载决议规则的表达方式有些不同,变化可能在某处。

如果它要编译成任何东西,我希望Foo接受 a 的重载Func<Task<int>>被选择而不是接受重载Func<Task>- 因为它是一种更具体的类型。(有一个从Func<Task<int>>to的参考转换Func<Task>,但反之则不然。)

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

归根结底,重载决议和类型推断是规范的真正难点。这个答案解释了为什么while(true)循环会有所作为(因为没有它,接受返回 a 的 func 的重载Task<T>甚至不适用)但我已经到了关于 C# 7 编译器做出的选择的尽头。

于 2018-04-20T10:04:12.983 回答
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();
        })
      );
于 2018-04-20T10:34:45.557 回答