Why does adding throw inside a lambda without a return value get inferred as a Func<T> and not as Action? [duplicate]
Asked Answered
K

1

8

I'm running into an issue where some test code for a library I'm writing won't compile due to an ambiguous call but the usage seemed clear to me. Upon further investigation I've found that adding throw inside a lambda that has no return value seems to be inferred as a Func<T> of any T and not an Action as I would expect.

Contrived example below (can paste into .NET Fiddle)

using System;

public class Program
{
    class Foo
    {

        public void Method(Action action)
        {
            Console.WriteLine("Method A: " + action.GetType());
        }

        public void Method(Func<int> func)
        {
            Console.WriteLine("Method B: " + func.GetType());
        }

        /* // second call to Method becomes ambiguous if this is commented out.
        public void Method(Func<bool> func)
        {
            Console.WriteLine(func.GetType());
        }
        */

    }

    public static void Main()
    {
        var foo = new Foo();
        foo.Method(() => { });
        foo.Method(() => { throw new Exception("Foo!"); });
    }
}

Which results in

Method A: System.Action
Method B: System.Func`1[System.Int32]

Maybe it assumes Func<object> because with a throw it can't infer any return type... But why can't it? And why would it infer and call the concrete Func<int> ?


Additionally, if I try to create an implicit Func<string> like so:

foo.Method(() => 
{ 
    if (false) 
    { 
        throw new Exception("Foo!");
    }
    return "foo";
});

I get three separate compile errors I've not encountered before:

Compilation error (line 38, col 16): Cannot implicitly convert type 'string' to 'int'
Compilation error (line 38, col 16): 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
Compilation error (line 38, col 9): Anonymous function converted to a void returning delegate cannot return a value

Researching these still doesn't make much sense in the contrived example above because these errors kinda contradict themselves. If the compiler can figure out it was returning string and can't convert to int, why is it then upset about about different return types or a void delegate returning a value?

Can anyone shed some light on why the compiler seems to have trouble understanding my intent? Is this a C# limitation or am I not seeing the ambiguity?

Kannada answered 23/2, 2020 at 4:21 Comment(8)
The Func<string> errors occur because there are two overloads and non of them accept a function that returns a string. First two errors: the compiler explains why it can't resolve the overload to that taking a Func<int>. Third error: the compiler explains why it can't resolve the overload taking an Action.Aryn
Have you tried examining the IL that the code generates?Lipolysis
@AluanHaddad Individually I understand those errors, but when they appear together they seem to contradict each other. If the first error was true, then it seems the compiler has inferred the type as Func<string>, which makes the third confusing as it implies it has inferred the lambda as a void returning delegate. Additionally, the second error in isolation seems strange, there is only one return type. Why does adding throw make the compiler think there are multiple return types?Kannada
IIRC, it's reporting multiple errors because it tried multiple overloads and none succeeded. throw does not make the compiler think that. It does not think thatAryn
I wonder if it might be because throw... can be considered an expression instead of a statement?Caraway
@Caraway I don't believe so. Anyway, it's used solely as a statement in the code above.Aryn
When the compiler doesn't know how to infer the argument, it shows errors related to them all in order to not discriminate one before the other. I agree, the analyzer here could be improved to consolidate the errors into one: Cannot infer lambda expression argument from usage. Did you mean Action or Func<int>?Nodal
@Nodal thank you for clarifying, that's what I was getting at.Aryn
W
2

As @Servy stated in the duplicated link

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.

In the below code:

var foo = new Foo();
foo.Method(() => { });
foo.Method(() => { throw new Exception("Foo!"); }); 

Because () => { throw new Exception("Foo!");} fits to either Action or Func<int>. And also, "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" then Func<int> is selected.

Concerning other exceptions:

Compilation error (line 38, col 16): Cannot implicitly convert type 'string' to 'int'
Compilation error (line 38, col 16): 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
Compilation error (line 38, col 9): Anonymous function converted to a void returning delegate cannot return a value 

The above exceptions are because of bad output types. In the test, you have called below anonymous method that returns a string but it should return int because your Func<int> returns int:

foo.Method(() => 
{ 
    if (false) 
    { 
        throw new Exception("Foo!");
    }
    return "foo";
});

For avoiding exception you should return an int:

foo.Method(() => 
{ 
    if (false) 
    { 
        throw new Exception("Foo!");
    }
    return 1;
});

References

Wilhelmstrasse answered 23/2, 2020 at 7:40 Comment(10)
I'm a little perplexed by this answer because in the OP, all of the delegates are nullary so the situation has nothing to do with Action<T> vs Func<TResult>. I also don't understand why you say that throw implies a return value. Maybe I'm missing something....Aryn
But the action in his overload is Action. It is nullary.Aryn
@AluanHaddad yea i did consider any situation either no parameters or with parameters.Wilhelmstrasse
Ok. but the question doesn't involve delegate parameter types. they aren't relevant in return type inferenceAryn
@AluanHaddad nope i am trying to say that Action or Action<T> or Action<T1,T2> are not important and you can consider any of them but same result.Wilhelmstrasse
But isn't it just the case that it's ambiguous because a function that consists solely of a throw is assignable to assignable to both Func<TResult> or Action and there's no reason to prefer either?Aryn
@AluanHaddad i have edited the answer and put some new facts. i would like to get your opinions.Wilhelmstrasse
It makes a lot more sense now. From vocabulary perspective, you say "returns an exception" and I think it would be better phrased "throws an exception". Elsewise, this is an instructive answer and is now pertinent to the question. +1Aryn
Any reason why this answer references java documentation? I don't believe this answer answers the question at hand, since a method without a return statement does in fact not return anything. The fact that it throws an exception inside does not imply that it returns anything. Specifically, if you remove the Func<T> overload, the other method is still applicable, which means it could resolve to that method. There has to be some priority system in here, and no, a method that throws an exception does not return an exception.Decarbonate
@LasseV.Karlsen thanks for enlightening me. I fixed the problems.Wilhelmstrasse

© 2022 - 2024 — McMap. All rights reserved.