How can I force a throw to be a statement and not an expression (in a lambda expression)?
Asked Answered
H

4

8

Starting from C# 7.0 the throw keyword can be used both as an expression and as a statement, which is nice. Though, consider these overloads

public static void M(Action doIt) { /*use doIt*/ }
public static void M(Func<int> doIt) { /*use doIt*/ }

When invoking like this

M(() => throw new Exception());

or even like this (with a statement lambda)

M(() => { throw new Exception(); });

the M(Func<>) overload is selected by the compiler indicating that the throw is here considered as an expression. How can I elegantly and intent-clear force the compiler to select the M(Action) overload?

One way to do it is this

M(() => { throw new Exception(); return; });

but the reason for the return statement seems non-obvious, and runs the risk of being changed by the next developer especially since Resharper warns about the unreachable code.

(Of course I can change the method naming to avoid overloading, but that is not the question. :-)

Hekking answered 15/1, 2019 at 23:8 Comment(1)
To clarify the question: the version with ()=>{throw...} was always assignable to Func<int> since C# 3; that's not new for C# 7. Fun fact: delegate { throw new Exception(); } is assignable to any delegate type that does not have "out" parameters.Abstergent
I
5

You could add a cast to for Action, although it does get a bit LISP'y with all the parentheses:

M((Action)(() => throw new Exception()));

Not ideal, but if you want maximum clarity:

Action thrw = () => throw new Exception();
M(thrw);
Implant answered 15/1, 2019 at 23:12 Comment(0)
W
8

This has nothing to do with whether the lambda is a statement lambda or an expression lambda (as is most succinctly shown by you changing the lambda from an expression lambda to a statement lambda and the behavior not changing).

There are numerous ways you can make a lambda match multiple possible overloads. This one is specific to newer versions, but other methods have applied since C# 1.0 (and the specific handling of anonymous methods and the resulting overload resolution disambiguation has needed to exist since the introduction of anonymous methods).

The rules for determining which overload is called are spelled out in section 7.5.3.3 of the C# specs. Specifically, when the parameter is an anonymous method, it will always prefer the overload who's delegate (or expression) has a return value over one that has no return value. This will be true whether it's a statement lambda or expression lambda; it applies to any form of anonymous function.

Thus you either need to prevent both overload from matching by making the anonymous method not valid for a Func<int>, or explicitly force the type to be an Action so the compiler is not disambiguating it itself.

Wirth answered 15/1, 2019 at 23:25 Comment(2)
I take it the fact that all code paths throw an exception, and therefore do not require a return method, is the reason that the Func<int> overload gets considered as a candidate in the first place? And once up for consideration, it always beats the Action overload by virtue of having a returning value. That's an interesting interaction.Eruption
@JonathonChase: That's correct. To be compatible with Func<int> every point at which there is a return must return something compatible with int, and that condition is vacuously met because zero out of all zero returns meet the requirement. "Interesting" is one word for it. "Super fun to implement" is not another. Let's just say there were a lot of test cases to write.Abstergent
I
5

You could add a cast to for Action, although it does get a bit LISP'y with all the parentheses:

M((Action)(() => throw new Exception()));

Not ideal, but if you want maximum clarity:

Action thrw = () => throw new Exception();
M(thrw);
Implant answered 15/1, 2019 at 23:12 Comment(0)
S
4

One possible approach is to use named parameters:

public static void M(Action action) { /* do stuff */ }

public static void M(Func<int> func) { /* do stuff */ }

public static void Main()
{
    M(action: () => throw new Exception());
}

This should probably be documented in the code so as not to surprise the next developer to come along, and as noted in the comments write an appropriate automated test to verify the correct overload is called.

Spree answered 15/1, 2019 at 23:14 Comment(7)
I'd recommend an automated test that proves Main calls the correct overload so someone renaming the arguments names does break the implementation by making it call the Func overload instead.Implant
@Implant - good point, I've updated the answer to note this. Thanks!Spree
@ErikPhilips - the OP does note that renaming the methods to avoid overloading is a simple way to avoid the problem (and would be something to consider in a real codebase), but I think it is still a valid question about overload resolution.Spree
@ErikPhilips I expect I've written many dozens of methods that accept both an action and a func in different overloads. There are quite a few examples that come to mind of the .NET framework as well. I would say that it's a perfectly acceptable pattern to see, and that you should be prepared to deal with as a C# programmer.Wirth
@Wirth while i agree with everything you've said and your answer, it sometimes in the best interest of the OP and future readers to really think about whether or not it's truly an overload or actually different.Dichy
@ErikPhilips: The fundamental problem here is that the CLR type system makes such a strong distinction between "void" and all other types. If there was instead a "Unit" type that only had one value (null), then we would not need Action at all; it would just be Func<Unit>. The decision to make void a very special type has a lot of repercussions on a type system. We also lack a "never returns normally" type, which could be the return type of a function that always throws.Abstergent
@ErikPhilips, as an example where the unit concept is applied as described by @Eric, see here in the F# documentation.Eldreeda
A
4

To add to all the reasonable answers given, here's a charming unreasonable answer:

((Action<Action>)M)(() => throw new Exception());

That one should bake the noodle of any maintenance programmers who come along, and they'll leave it alone. See if you can figure out why it works.

Abstergent answered 16/1, 2019 at 0:3 Comment(2)
Am I suppose to downvote for the unreasonable answer or upvote for the matrix reference?Dichy
May as well just go full ((Func<Action<Action<Action>>>)(() => x => x(() => throw new Exception())))()(M). Very clear, very reasonable.Eruption

© 2022 - 2024 — McMap. All rights reserved.