代码之家  ›  专栏  ›  技术社区  ›  jeroenh

为什么这样断言.throws调用解析?

  •  9
  • jeroenh  · 技术社区  · 6 年前

    使用Xunit's 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 回复  |  直到 6 年前
        1
  •  3
  •   Jon Skeet    6 年前

    这是一个完整的例子,不涉及 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()。 . 为了检查适用性,我们需要将该论点转换为 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是 功能<int> ):

    如果身体 F 是表达式,并且 f 是非异步的,并且 D 具有非空返回类型 T ,或者 f 是异步的 D 具有返回类型 Task<T> ,则当给f的每个参数指定了 D ,的主体 f 是可隐式转换为 T .

    在这种情况下,表达式 ThrowException 隐式可转换为 int 因此出现了错误。

    所有这些都意味着只有第一种方法适用于 ()=>throwException()。 . 当一组适用的函数成员只有一个条目时,我们选择“最佳函数成员”就非常容易了…

    呼叫2: () => { throw new Exception(); }

    现在让我们看看第二个电话, ()=>引发新的异常(); 作为论据。让我们尝试相同的转换:

    // Fine
    Action action = () => { throw new Exception(); };
    // Fine
    Func<int> func = () => { throw new Exception(); };
    

    两种转换都在这里工作。后者之所以起作用是因为 11.7.1中的项目符号:

    如果身体 f 是语句块,并且 f 是非异步的,并且 D 有一个 非空返回类型 T ,或者 f 是异步的 D 具有返回类型 任务<t> , 然后当每个参数 f 在中给出了相应参数的类型 D ,的主体 f 是一个有效的语句块(W.R.T_§13.3),具有不可更改的 结束点,其中每个返回语句都指定一个隐式表达式 可转换为 T .

    我知道这听起来很奇怪,但是:

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

    换一种说法:你 能够 将该块用作已声明返回的方法的主体 int

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

    那么哪个更好呢?

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

    太多了 这里的规则,但是决定这里事情的是从lambda表达式到 行动 功能<int> . 这在12.6.4.4中得到了解决(更好地从表达式转换):

    给定一个从表达式e转换为类型t1的隐式转换c1,以及一个从表达式e转换为类型t2的隐式转换c2,如果至少有以下一个条件成立,c1比c2更好的转换:

    • e是匿名函数,T1是委托类型d1或表达式树 类型 Expression<D1> ,t2是委托类型d2或表达式树类型 Expression<D2> 和 下列情况之一:
      • d1是比d2更好的转换目标
      • d1和d2具有相同的参数列表,以下其中一个保持不变:
        • d1有一个返回类型y1,d2有一个返回类型y2,在参数列表(§12.6.3.13)的上下文中,e存在一个推断的返回类型x,从x到y1的转换比从x到y2的转换更好。
        • E是异步的。-跳过,因为它不是]
        • d1为返回类型y,d2为空返回

    我用粗体写的部分很重要。当您考虑以下情况时:

    • E是 ()=>引发新的异常();
    • T1是 功能<int> (所以d1是 功能<int> 太)
    • T2是 行动 (所以d2是 行动 太)

    …然后,d1和d2都有空的参数列表,但d1有返回类型 int d2为空返回。

    因此转化为 功能<int> 比转换为 行动 …也就是说 Method(Action) 函数成员是否比 Member(Func<int>) 第二个电话。

    唷!你不喜欢超负荷的解决方案吗?

        2
  •  4
  •   Selman Genç    6 年前

    考虑到函数声明,我能够重现这一点:

    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的主体是一个分类为Nothing的表达式(_§7.1),或者是一个没有返回语句有表达式的语句块, 推断返回类型为System.Threading.Tasks.Task

    这里的函数是一个语句块,但它不是异步的。注意我不是说 这个确切的规则在这里适用 . 不过,我猜这和这个有关。

    This article 埃里克·利珀特解释得更好。(感谢你的评论@damien_the_Unbeliever)。