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");
}
}
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.
Catch is similar to Assert. Throws but will pass for an exception that is derived from the one specified.
Assert. Throws returns the exception that's thrown which lets you assert on the exception.
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).
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:
We'll look at each call in turn
() => 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 eitherF
is non-async andD
has a non-void return typeT
, orF
is async andD
has a return typeTask<T>
, then when each parameter of F is given the type of the corresponding parameter inD
, the body ofF
is a valid expression (w.r.t §12) that is implicitly convertible toT
.
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...
() => { 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 eitherF
is non-async andD
has a non-void return typeT
, orF
is async andD
has a return typeTask<T>
, then when each parameter ofF
is given the type of the corresponding parameter inD
, the body ofF
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 toT
.
I realize it sounds odd that this works, but:
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.
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 typeExpression<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:
() => { throw new Exception(); }
Func<int>
(so D1 is Func<int>
too)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?
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With