Delegate return type different with lambda function
Asked Answered
P

3

17

Consider this MCVE:

using System;

public interface IThing { }

public class Foo : IThing
{
    public static Foo Create() => new Foo();
}

public class Bar : IThing
{
    public static Bar Create() => new Bar();
}

public delegate IThing ThingCreator();

class Program
{
    static void Test(ThingCreator creator)
    {
        Console.WriteLine(creator.Method.ReturnType);
    }

    static void Main()
    {
        Test(Foo.Create);      // Prints: Foo
        Test(Bar.Create);      // Prints: Bar

        Test(() => new Foo()); // Prints: IThing
        Test(() => new Bar()); // Prints: IThing
    }
}

Why does reflecting the return type for the static factory method give the concrete type, while calling the constructor inline gives the interface? I would expect them to both be the same.

Also, is there a way to specify in the lambda version that I want the return value to be the concrete type? Or is calling a static method the only way to do that?

Paraphernalia answered 16/3, 2017 at 22:33 Comment(2)
A static method is not the only way. You can also use a non-static one. For example Func<Foo> f = () => new Foo(); Test(f.Invoke); But the real question is, why do you even check .Method.ReturnType on a (possibly multi-cast) delegate instance? What is the use for that; why do you care?Bussell
@Jeppe - A third-party library has a public API with a method similar to Test. I didn't expect the side effect by passing a lambda instead of a reference to an actual function. Had to dig to find they checked the method's return type. If it were my own code, I'd not create such a thing.Paraphernalia
B
15

The return type of a lambda expression is not inferred from what the lambda acutally returns, but from the type it is assigend to. I.e., you can not assign a lambda like this (except when generic type parameters are involed; see comments of Eric Lippert):

// This generates the compiler error:
// "Cannot assign lambda expression to an implicitly-typed variable".
var lambda = () => new Foo();

You must always do something like this (lambdas are always assigned to a delegate type):

Func<MyType> lambda = () => new Foo();

Therefore in Test(() => new Foo()); the return type of the lambda is determined from the type of the parameter it is assigned to (IThing, the return type of ThingCreator).

In Test(Foo.Create); you don't have a lambda at all, but a method declared as public static Foo Create() .... Here the type is specified explicitly and is Foo (it makes no difference whether it is a static or instance method).

Baram answered 16/3, 2017 at 22:54 Comment(3)
Leaving aside the specific question given by the original poster, I note that your claim that the type of a lambda expression is never inferred from what the lambda returns is false. Consider for example static void M<T>(Func<T> f) called as M( () => 1 ). In that case type inference reasons that the formal parameter types -- all zero of them -- are known, and therefore T can be inferred from the return type of the lambda, which is int.Thracian
@EricLippert: Does this only happen when generic type parameters are involved?Baram
Correct; the case I mentioned is IIRC the only time C# infers from the return type of a lambda. In all other cases the return type is checked for compatibility with the delegate's return type, which must already be known.Thracian
T
11

Olivier's answer is basically correct but it could use some additional explication.

Why does reflecting the return type for the static factory method give the concrete type, while calling the constructor inline gives the interface? I would expect them to both be the same

Your Program class is equivalent to the following class:

class Program
{
  static void Test(ThingCreator creator)
  {
    Console.WriteLine(creator.Method.ReturnType);
  }
  static IThing Anon1() 
  {
    return new Foo();
  }
  static IThing Anon2()
  {
    return new Bar();
  }
  static void Main()
  {
    Test(new ThingCreator(Foo.Create));
    Test(new ThingCreator(Bar.Create));
    Test(new ThingCreator(Program.Anon1));
    Test(new ThingCreator(Program.Anon2));
  }
}

And now it should be clear why the program prints what it does.

The moral of the story here is that when we generate the hidden method for the lambdas, those hidden methods return whatever was required by the delegate, and not whatever the lambda returns.

Why is that?

A more germane example would demonstrate that:

static void Blah(Func<object> f) 
{ 
  Console.WriteLine(f().ToString());
}
static void Main()
{
  Blah( () => 123 );
}

I hope you agree that this must be generated as

static object Anon() { return (object)123; }

and not

static int Anon() { return 123; }

Because the latter cannot be converted to Func<object>! There is nowhere for the boxing instruction to go, but the call to ToString expects a reference type to be returned by f().

Thus the general rule is that the lambda, when reified as a hidden method, must have the return type granted to it by the conversion to the delegate type.

Also, is there a way to specify in the lambda version that I want the return value to be the concrete type? Or is calling a static method the only way to do that?

Sure.

interface IThing {}
class Foo : IThing {}
delegate T ThingCreator<T>() where T : IThing;
public class Program
{
  static  void Test<T>(ThingCreator<T> tc) where T : IThing
  {
    Console.WriteLine(tc.Method.ReturnType);
  }
  public static void Main()
  {
    Test(() => new Foo());      
  }
}

Why is this different?

Because type inference deduces that T is Foo and therefore we are converting the lambda to ThingCreator<Foo>, which has return type Foo. Therefore the method generated returns Foo, as the delegate type expects.

BUT WAIT, you say... I cannot change the signature of Test or of ThingCreator

No worries! You can still make this work:

delegate T ThingCreator<T>() where T : IThing;
delegate IThing ThingCreator();    
public class Program
{
    static void Test(ThingCreator tc)
    {
      Console.WriteLine(tc.Method.ReturnType);
    }
    static ThingCreator DoIt<T>(ThingCreator<T> tc) where T : class, IThing
    { 
      return tc.Invoke; 
    }
    public static void Main()
    {
      Test(DoIt(() => new Foo()));
    }
}

The down side is that now every one of your non-generic ThingCreator delegates is a delegate which calls a ThingCreator<T> delegate, which is a bit of a waste of time and memory. But you get type inference, method group conversion and lambda conversion to make you a method of the desired return type, and a delegate to that method.

Note the class constraint. Do you see why that constraint must be there? This is left as an exercise to the reader.

Thracian answered 17/3, 2017 at 0:32 Comment(1)
Very nice explanation. Thanks!Paraphernalia
D
0

My personal guess is the invocation location. When you pass () => new Foo() to the function it grabs it as a ThingCreator and invokes it to get IThing. But, when you send the factory method of the concrete type to the Test method when test method invokes it, it goes to the concrete type's Create() which in turn returns the concrete object which is perfectly acceptable as it too is IThing

I guess you need more than my guess though. Sorry if I'm wrong!

Danieldaniela answered 16/3, 2017 at 22:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.