Visual Studio 2013 年的 C# 方法重载解析问题
C# method overload resolution issues in Visual Studio 2013
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) {...}
那里发生了什么事?
这很有趣 :) 它有多个方面。首先,让我们通过从图片中删除 Rx 和实际重载分辨率来非常显着地简化它。过载解决方案在答案的最后处理。
委托转换和可达性的匿名函数
这里的区别在于lambda表达式的终点是否可达。如果是,那么那个 lambda 表达式就没有 return 任何东西,lambda 表达式只能转换为 Func<Task>
。如果 lambda 表达式的终点 不是 可到达的,那么它可以转换为任何 Func<Task<T>>
.
while
语句的形式因 C# 规范的这一部分而有所不同。 (这是来自 ECMA C# 5 标准;其他版本对同一概念的措辞可能略有不同。)
The end point of a while
statement is reachable if at least one of the following is true:
- The
while
statement contains a reachable break statement that exits the while statement.
- The
while
statement is reachable and the Boolean expression does not have the constant value 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 表达式,那么 always 可转换为 Action
,但它也是 also如果无法到达 lambda 表达式的末尾,则可将任何 T
转换为 Func<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
的错误方法是"not all code paths return a value",但检测到的方法是"the end of the method is reachable"。现在想象那些方法体是 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
更好。所有转换都是有效的——如果你注释掉 Foo1
和 Bar2
方法,它仍然可以编译,但会给出 Foo2
、Bar1
.
的输出
对于 C# 5 编译器,Foo
调用是不明确的,因为 Bar
调用解析为 Bar2
,就像 C# 7 编译器一样。
经过更多研究,同步形式在 ECMA C# 5 规范的 12.6.4.4 中指定:
C1 is a better conversion than C2 if at least one of the following holds:
- ...
- E is an anonymous function, T1 is either a delegate type D1 or an expression tree type Expression, T2 is either a delegate type D2 or an expression tree type Expression and one of the following holds:
- D1 is a better conversion target than D2 (irrelevant for us)
- D1 and D2 have identical parameter lists, and one of the following holds:
- D1 has a return type Y1, and D2 has a return type Y2, an inferred return type X exists for E in the context of that parameter list (§12.6.3.13), and the conversion from X to Y1 is better than the conversion from X to Y2
- E is async, D1 has a return type
Task<Y1>
, and D2 has a return type Task<Y2>
, an inferred return type Task<X>
exists for E in the context of that parameter list (§12.6.3.13), and the conversion from X to Y1 is better than the conversion from X to Y2
- D1 has a return type Y, and D2 is void returning
所以这对于非异步情况是有意义的 - 而且对于 C# 5 编译器如何无法解决歧义也是有意义的,因为这些规则不会打破平局。
我们还没有完整的 C# 6 或 C# 7 规范,但有 draft one available。它的重载解析规则表达方式有些不同,变化可能就在那里。
如果它要编译成任何东西,我希望 Foo
重载接受 Func<Task<int>>
而不是接受 Func<Task>
的重载 - 因为它更具体类型。 (有从 Func<Task<int>>
到 Func<Task>
的参考转换,但反之则不然。)
请注意,lambda 表达式的 推断 return 类型 在 C# 5 和草案 C# 6 规范中只是 Func<Task>
。
最终,重载解析和类型推断是规范中非常困难 的部分。这个答案解释了为什么 while(true)
循环会有所不同(因为没有它,接受 func returning a Task<T>
的重载甚至不适用)但我已经走到了尽头关于 C# 7 编译器做出的选择,我能得出什么结论。
除了@Daisy Shipton 的回答之外,我想补充一点,在以下情况下也可以观察到相同的行为:
var sequence = Observable.Create<int>(
async (observer, token) =>
{
throw new NotImplementedException();
});
基本上是因为同样的原因——编译器认为 lambda 函数从不 returns 所以任何 return 类型都会匹配,这反过来又使 lambda 匹配任何 Observable.Create
过载。
最后,一个简单解决方案的示例:您可以将 lambda 转换为所需的签名类型,以提示编译器选择哪个 Rx 重载。
var sequence =
Observable.Create<int>(
(Func<IObserver<int>, CancellationToken, Task>)(async (observer, token) =>
{
throw new NotImplementedException();
})
);
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) {...}
那里发生了什么事?
这很有趣 :) 它有多个方面。首先,让我们通过从图片中删除 Rx 和实际重载分辨率来非常显着地简化它。过载解决方案在答案的最后处理。
委托转换和可达性的匿名函数
这里的区别在于lambda表达式的终点是否可达。如果是,那么那个 lambda 表达式就没有 return 任何东西,lambda 表达式只能转换为 Func<Task>
。如果 lambda 表达式的终点 不是 可到达的,那么它可以转换为任何 Func<Task<T>>
.
while
语句的形式因 C# 规范的这一部分而有所不同。 (这是来自 ECMA C# 5 标准;其他版本对同一概念的措辞可能略有不同。)
The end point of a
while
statement is reachable if at least one of the following is true:
- The
while
statement contains a reachable break statement that exits the while statement.- The
while
statement is reachable and the Boolean expression does not have the constant valuetrue
.
当你有一个没有 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 表达式,那么 always 可转换为 Action
,但它也是 also如果无法到达 lambda 表达式的末尾,则可将任何 T
转换为 Func<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
的错误方法是"not all code paths return a value",但检测到的方法是"the end of the method is reachable"。现在想象那些方法体是 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
更好。所有转换都是有效的——如果你注释掉 Foo1
和 Bar2
方法,它仍然可以编译,但会给出 Foo2
、Bar1
.
对于 C# 5 编译器,Foo
调用是不明确的,因为 Bar
调用解析为 Bar2
,就像 C# 7 编译器一样。
经过更多研究,同步形式在 ECMA C# 5 规范的 12.6.4.4 中指定:
C1 is a better conversion than C2 if at least one of the following holds:
- ...
- E is an anonymous function, T1 is either a delegate type D1 or an expression tree type Expression, T2 is either a delegate type D2 or an expression tree type Expression and one of the following holds:
- D1 is a better conversion target than D2 (irrelevant for us)
- D1 and D2 have identical parameter lists, and one of the following holds:
- D1 has a return type Y1, and D2 has a return type Y2, an inferred return type X exists for E in the context of that parameter list (§12.6.3.13), and the conversion from X to Y1 is better than the conversion from X to Y2
- E is async, D1 has a return type
Task<Y1>
, and D2 has a return typeTask<Y2>
, an inferred return typeTask<X>
exists for E in the context of that parameter list (§12.6.3.13), and the conversion from X to Y1 is better than the conversion from X to Y2- D1 has a return type Y, and D2 is void returning
所以这对于非异步情况是有意义的 - 而且对于 C# 5 编译器如何无法解决歧义也是有意义的,因为这些规则不会打破平局。
我们还没有完整的 C# 6 或 C# 7 规范,但有 draft one available。它的重载解析规则表达方式有些不同,变化可能就在那里。
如果它要编译成任何东西,我希望 Foo
重载接受 Func<Task<int>>
而不是接受 Func<Task>
的重载 - 因为它更具体类型。 (有从 Func<Task<int>>
到 Func<Task>
的参考转换,但反之则不然。)
请注意,lambda 表达式的 推断 return 类型 在 C# 5 和草案 C# 6 规范中只是 Func<Task>
。
最终,重载解析和类型推断是规范中非常困难 的部分。这个答案解释了为什么 while(true)
循环会有所不同(因为没有它,接受 func returning a Task<T>
的重载甚至不适用)但我已经走到了尽头关于 C# 7 编译器做出的选择,我能得出什么结论。
除了@Daisy Shipton 的回答之外,我想补充一点,在以下情况下也可以观察到相同的行为:
var sequence = Observable.Create<int>(
async (observer, token) =>
{
throw new NotImplementedException();
});
基本上是因为同样的原因——编译器认为 lambda 函数从不 returns 所以任何 return 类型都会匹配,这反过来又使 lambda 匹配任何 Observable.Create
过载。
最后,一个简单解决方案的示例:您可以将 lambda 转换为所需的签名类型,以提示编译器选择哪个 Rx 重载。
var sequence =
Observable.Create<int>(
(Func<IObserver<int>, CancellationToken, Task>)(async (observer, token) =>
{
throw new NotImplementedException();
})
);