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



在 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( false )var condition = true; while( condition )...替换while( true )

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) {...}

这是怎么回事?

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

用于委派转化和可达性的匿名功能

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

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

如果满足以下至少一项条件,则可以到达while语句的终点:

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

当您有一个没有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); };
}
}

我们可以通过从等式中删除异步来进一步简化。如果我们有一个没有 return 语句的同步无参数 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) ; });
}
}

这会发出警告(无等待运算符),但它使用 C# 7 编译器进行编译。输出让我感到惊讶:

Foo1
Bar2

因此,Foo的分辨率是确定转换为Func<Task>优于转换为Func<Task<int>>,而Bar的分辨率是确定转换为Func<int>比转换为Action更好。所有转换都是有效的 - 如果您注释掉Foo1Bar2方法,它仍然可以编译,但给出Foo2Bar1的输出。

使用 C# 5 编译器时,Foo

调用不明确,Bar调用解析为Bar2,就像 C# 7 编译器一样。

经过更多的研究,同步形式在 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 返回
    • 无效

因此,这对于非异步情况是有意义的 - 对于 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>的 func 的重载甚至不适用),但我已经到了关于 C# 7 编译器做出的选择的尽头。

除了 @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();
})
);

相关内容

  • 没有找到相关文章

最新更新