为什么此Assert.Throws调用以这种方式解决?

时间:2018-08-17 08:43:13

标签: c# xunit

使用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");
    }
}

2 个答案:

答案 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节-重载解析感兴趣。两个重要步骤是:

  • 确定适用的方法(12.6.4.2)
  • 确定最佳方法(12.6.4.3)

我们将依次查看每个呼叫

电话1:() => ThrowException()

让我们从第一个调用开始,该调用的参数为() => ThrowException()。为了检查适用性,我们需要从该参数转换为ActionFunc<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。当一组适用的功能成员只有一个条目时,我们选择“最佳功能成员”真的很容易。

呼叫2:() => 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具有一个   非无效返回类型DT是异步的,并且F的返回类型为D,   那么当给Task<T>的每个参数赋予相应参数的类型时,   FD的主体是有效的语句块(w.r.t§13.3),且不可访问   每个return语句在其中指定隐式表达式的端点   可转换为F

我知道这行得通,但是:

  • 无法到达该区块的终点
  • 没有返回语句,因此确实满足“每个返回语句指定[...]”的条件

换句话说:您可以使用该块作为声明为返回T的方法的主体。

这意味着两者我们的方法都适用于这种情况。

那哪个更好?

现在,我们需要查看12.6.4.3节来确定将实际选择哪种方法。

这里有很多条规则,但是决定这里事情的规则是从lambda表达式到intAction的转换。在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返回的返回类型为空
      •   
    •   
  •   

我用粗体显示的部分很重要。考虑以下情况时:

  • E是Expression<D2>
  • T1是() => { throw new Exception(); }(所以D1也是Func<int>
  • T2是Func<int>(所以D2也是Action

...然后D1和D2都具有空的参数列表,但是D1的返回类型为Action,而D2的返回均为空。

因此,对于第二个调用,向int的转换要比向Func<int>的转换要好。这意味着Action是比Method(Action)更好的函数成员。

Ph!您不只是喜欢超载分辨率吗?