Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does this Assert.Throws call resolve this way?

Tags:

c#

xunit

Using xUnit's Assert.Throws, I stumbled upon this (to me) hard to explain overload resolution issue. In xUnit, this method is 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(); }

The question is, why does an inline statement lambda (or expression) that simply throws an exception resolve to this overload (and therefore doesn't compile)?

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");
    }
}
like image 831
jeroenh Avatar asked Aug 17 '18 08:08

jeroenh


People also ask

What is assert throw?

assert-throws is an assertion method for Node. js which checks if a synchronous or asynchronous function throws. It can also compare properties of the error (such as message , code and stack and any other) with expected ones using string strict equality, a regular expression, or a function.

What is the difference between assert throws and assert catch?

Catch is similar to Assert. Throws but will pass for an exception that is derived from the one specified.

Do asserts throw exceptions?

Assert. Throws returns the exception that's thrown which lets you assert on the exception.


2 Answers

I was able to reproduce this, given the function declarations:

static void CallFunction(Action action) { }

static void CallFunction(Func<Task> func) { }

And calling them:

CallFunction(() => ThrowException());
CallFunction(() => { throw new Exception(); });

The second one resolves into CallFunction(Func<Task> func) overload. The weird thing is if I change the body like this:

CallFunction(() => { int x = 1; });

It resolves to CallFunction(Action action) overload.

If the last statement in the body is a throw statement, I guess the compiler thinks method is returning something, and picks the closest -more specific- overload to that scenario which is Func<Task>.

The closest thing I found in the documentation is this:

7.5.2.12 Inferred return type

• If F is async and the body of F is either an expression classified as nothing (§7.1), or a statement block where no return statements have expressions, the inferred return type is System.Threading.Tasks.Task

The function here is a statement block, but it's not async though. Note that I'm not saying this exact rule applies here. Still I'm guessing it's related to this.

This article from Eric Lippert explains it better. (thanks for the comment @Damien_The_Unbeliever).

like image 62
Selman Genç Avatar answered Nov 15 '22 11:11

Selman Genç


Here's a complete example which doesn't involve Task, to remove any hint of asynchrony being involved:

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

Using section numbering from ECMA 334 (5th edition), we're interested in section 12.6.4 - overload resolution. The two important steps are:

  • Identify applicable methods (12.6.4.2)
  • Identify the best method (12.6.4.3)

We'll look at each call in turn

Call 1: () => ThrowException()

Let's start with the first call, which has an argument of () => ThrowException(). To check for applicability, we need a conversion from that argument to either Action or Func<int>. We can check that without involving overloading at all:

// 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();

The CS1662 error is a little unfortunately worded in this case - it's not that there's a return type in the block that's not implicitly convertible to the delegate return type, it's that there isn't a return type in the lambda expression at all. The spec way of preventing this is in section 11.7.1. None of the permitted conversions there work. The closest is this (where F is the lambda expression and D is Func<int>):

If the body of F is an expression, and either F is non-async and D has a non-void return type T, or F is async and D has a return type Task<T>, then when each parameter of F is given the type of the corresponding parameter in D, the body of F is a valid expression (w.r.t §12) that is implicitly convertible to T.

In this case the expression ThrowException is not implicitly convertible to int, hence the error.

All of this means that only the first method is applicable for () => ThrowException(). Our pick for "best function member" is really easy when the set of applicable function members only has a single entry...

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

Now let's look at the second call, which has () => { throw new Exception(); } as the argument. Let's try the same conversions:

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

Both conversions work here. The latter one works because of this bullet from 11.7.1:

If the body of F is a statement block, and either F is non-async and D has a non-void return type T, or F is async and D has a return type Task<T>, then when each parameter of F is given the type of the corresponding parameter in D, the body of F is a valid statement block (w.r.t §13.3) with a nonreachable end point in which each return statement specifies an expression that is implicitly convertible to T.

I realize it sounds odd that this works, but:

  • The end point of the block is not reachable
  • There are no return statements, so the condition of "each return statement specifies [...]" is indeed met

To put it another way: you could use that block as the body of a method that's declared to return int.

That means both our methods are applicable in this case.

So which is better?

Now we need to look at section 12.6.4.3 to work out which method will actually be picked.

There are lots of rules here, but the one that decides things here is the conversion from the lambda expression to either Action or Func<int>. That's resolved in 12.6.4.4 (better conversion from expression):

Given an implicit conversion C1 that converts from an expression E to a type T1, and an implicit conversion C2 that converts from an expression E to a type T2, 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<D1>, T2 is either a delegate type D2 or an expression tree type Expression<D2> and one of the following holds:
    • D1 is a better conversion target than D2
    • 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 [... - skipped because it's not]
      • D1 has a return type Y, and D2 is void returning

The part I've put in bold is the important bit. When you consider the following scenario:

  • E is () => { throw new Exception(); }
  • T1 is Func<int> (so D1 is Func<int> too)
  • T2 is Action (so D2 is Action too)

... then both D1 and D2 have empty parameter lists, but D1 has a return type int, and D2 is void returning.

Therefore the conversion to Func<int> is better than the conversion to Action... which means that Method(Action) is a better function member than Member(Func<int>) for the second call.

Phew! Don't you just love overload resolution?

like image 40
Jon Skeet Avatar answered Nov 15 '22 13:11

Jon Skeet