使用xUnit的Assert.Throws
,我偶然发现了这个(对我来说)很难解释重载解决方案的问题。在xUnit中,此方法为marked obsolete:
[Obsolete("You must call Assert.ThrowsAsync<T> (and await the result) " +
"when testing async code.", true)]
public static T Throws<T>(Func<Task> testCode) where T : Exception
{ throw new NotImplementedException(); }
问题是,为什么简单地引发异常的内联语句lambda(或表达式)可以解决此重载(因此无法编译)?
using System;
using Xunit;
class Program
{
static void Main(string[] args)
{
// this compiles (of course)
// resolves into the overload accepting an `Action`
Assert.Throws<Exception>(() => ThrowException());
// line below gives error CS0619 'Assert.Throws<T>(Func<Task>)' is obsolete:
// 'You must call Assert.ThrowsAsync<T> (and await the result) when testing async code.'
Assert.Throws<Exception>(() => { throw new Exception(); });
}
static void ThrowException()
{
throw new Exception("Some message");
}
}
答案 0 :(得分:4)
考虑到函数声明,我能够重现这一点:
static void CallFunction(Action action) { }
static void CallFunction(Func<Task> func) { }
并打电话给他们
CallFunction(() => ThrowException());
CallFunction(() => { throw new Exception(); });
第二个解析为CallFunction(Func<Task> func)
重载。奇怪的是,如果我这样改变身体:
CallFunction(() => { int x = 1; });
解析为CallFunction(Action action)
重载。
如果主体中的最后一条语句是throw语句,则我猜编译器认为方法正在返回某些内容,并选择与该情况最接近的[更具体的]重载为Func<Task>
。
我在文档中找到的最接近的东西是:
7.5.2.12推断的返回类型
•如果F是异步的,并且F的主体是分类为“无”的表达式(第7.1节)或没有return语句没有表达式的语句块,则推断的返回类型为System.Threading.Tasks。任务
这里的函数是一个语句块,但不是异步的。请注意,我并不是说此确切规则在这里适用。还是我猜想这与此有关。
埃里克·利珀特(Eric Lippert)的This article对此做了更好的解释。 (感谢@Damien_The_Unbeliever的评论)。
答案 1 :(得分:3)
下面是一个不涉及Task
的完整示例,以消除涉及异步的任何提示:
using System;
class Program
{
static void Method(Action action)
{
Console.WriteLine("Action");
}
static void Method(Func<int> func)
{
Console.WriteLine("Func<int>");
}
static void ThrowException()
{
throw new Exception();
}
static void Main()
{
// Resolvse to the Action overload
Method(() => ThrowException());
// Resolves to the Func<int> overload
Method(() => { throw new Exception(); });
}
}
使用ECMA 334 (5th edition)中的节编号,我们对12.6.4节-重载解析感兴趣。两个重要步骤是:
我们将依次查看每个呼叫
() => ThrowException()
让我们从第一个调用开始,该调用的参数为() => ThrowException()
。为了检查适用性,我们需要从该参数转换为Action
或Func<int>
。我们可以检查一下而根本不涉及过载:
// Fine
Action action = () => ThrowException();
// Fails to compile:
// error CS0029: Cannot implicitly convert type 'void' to 'int'
// error CS1662: Cannot convert lambda expression to intended delegate type because
// some of the return types in the block are not implicitly convertible to the
// delegate return type
Func<int> func = () => ThrowException();
不幸的是,在这种情况下,出现了CS1662错误的措辞-不是在块中存在不能隐式转换为委托返回类型的返回类型,而是在不是返回完全输入lambda表达式。防止这种情况的规范方法在第11.7.1节中。那里没有允许的转换有效。最接近的是这个(其中F是lambda表达式,D是Func<int>
):
如果
F
的主体是一个表达式,并且F
是非异步的,并且D
具有非无效的返回类型T
或{{1 }}是异步的,并且F
的返回类型为D
,那么当给F的每个参数提供Task<T>
中相应参数的类型时,D
的主体为可以隐式转换为F
的有效表达式(wrt§12)。
在这种情况下,表达式T
不能隐式转换为ThrowException
,因此会出错。
所有这些都意味着仅第一种方法适用于int
。当一组适用的功能成员只有一个条目时,我们选择“最佳功能成员”真的很容易。
() => ThrowException()
现在让我们来看第二个调用,它以() => { throw new Exception(); }
作为参数。让我们尝试相同的转换:
() => { throw new Exception(); }
这两种转换都可以在这里进行。后者的工作是由于11.7.1中的 this 项目符号:
如果
// Fine Action action = () => { throw new Exception(); }; // Fine Func<int> func = () => { throw new Exception(); };
的主体是一个语句块,并且F
是非异步的,而F
具有一个 非无效返回类型D
或T
是异步的,并且F
的返回类型为D
, 那么当给Task<T>
的每个参数赋予相应参数的类型时,F
,D
的主体是有效的语句块(w.r.t§13.3),且不可访问 每个return语句在其中指定隐式表达式的端点 可转换为F
。
我知道这行得通,但是:
换句话说:您可以使用该块作为声明为返回T
的方法的主体。
这意味着两者我们的方法都适用于这种情况。
现在,我们需要查看12.6.4.3节来确定将实际选择哪种方法。
这里有很多条规则,但是决定这里事情的规则是从lambda表达式到int
或Action
的转换。在12.6.4.4中解决了这一问题(通过表达式实现更好的转换):
鉴于从表达式E转换为类型T1的隐式转换C1和从表达式E转换为类型T2的隐式转换C2,如果满足以下至少一项条件,则C1比C2更好。 :
- ...
- E是一个匿名函数,T1是委托类型D1或表达式树 类型
Func<int>
,T2是委托类型D2或表达式树类型Expression<D1>
,并且 下列条件之一:
- D1比D2是更好的转化目标
- D1和D2具有相同的参数列表,并且满足以下条件之一:
- D1的返回类型为Y1,D2的返回类型为Y2,在该参数列表的上下文中存在E的推断返回类型X(第12.6.3.13节),并且从X到Y1的转换比从X到Y2的转化
- E异步[...-因为不是,所以跳过了]
- D1的返回类型为Y,D2返回的返回类型为空
我用粗体显示的部分很重要。考虑以下情况时:
Expression<D2>
() => { throw new Exception(); }
(所以D1也是Func<int>
)Func<int>
(所以D2也是Action
) ...然后D1和D2都具有空的参数列表,但是D1的返回类型为Action
,而D2的返回均为空。
因此,对于第二个调用,向int
的转换要比向Func<int>
的转换要好。这意味着Action
是比Method(Action)
更好的函数成员。
Ph!您不只是喜欢超载分辨率吗?