Тип возврата делегата, отличный от лямбда-функции

Рассмотрим этот 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
    }
}

Почему отражение типа возврата для статического метода factory дает конкретный тип, а вызов конструктора inline дает интерфейс? Я бы ожидал, что оба они будут одинаковыми.

Кроме того, есть ли способ указать в лямбда-версии, что я хочу, чтобы возвращаемое значение было конкретным типом? Или вызывает статический метод единственный способ сделать это?

Ответы

Ответ 1

Возвращаемый тип выражения лямбда не выводится из того, что лямбда активно возвращается, а из типа, к которому он относится. I.e., вы не можете назначить такую ​​лямбду (за исключением случаев, когда параметры родового типа являются инвертированными, см. Комментарии Эрика Липперта):

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

Вы всегда должны делать что-то вроде этого (lambdas всегда назначаются типу делегата):

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

Поэтому в Test(() => new Foo()); тип возвращаемого значения лямбда определяется по типу параметра, которому он назначен (IThing, возвращаемый тип ThingCreator).

В Test(Foo.Create); у вас вообще нет лямбда, а метод объявлен как public static Foo Create() .... Здесь тип указан явно и равен Foo (не имеет значения, является ли это статическим или экземпляром метода).

Ответ 2

Ответ Оливье в основном правильный, но он может использовать некоторую дополнительную экспликацию.

Почему отражение типа возврата для статического метода factory дает конкретный тип, а вызов конструктора inline дает интерфейс? Я бы ожидал, что оба они будут одинаковыми

Ваш класс Program эквивалентен следующему классу:

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

И теперь должно быть понятно, почему программа печатает, что она делает.

Мораль этой истории заключается в том, что когда мы генерируем скрытый метод для лямбда, эти скрытые методы возвращают все, что требовалось делегату, а не то, что возвращает лямбда.

Почему это?

Более примерный пример продемонстрирует, что:

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

Надеюсь, вы согласитесь, что это должно быть сгенерировано как

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

а не

static int Anon() { return 123; }

Поскольку последнее не может быть преобразовано в Func<object>! Команде бокса некуда идти, но вызов ToString ожидает, что ссылочный тип будет возвращен f().

Таким образом, общее правило заключается в том, что лямбда, когда она используется как скрытый метод, должна иметь возвращаемый тип, предоставленный ему преобразованием в тип делегата.

Кроме того, есть ли способ указать в лямбда-версии, что я хочу, чтобы возвращаемое значение было конкретным типом? Или вызывает статический метод единственный способ сделать это?

Конечно.

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

Почему это другое?

Поскольку вывод типа выводит, что T является Foo, и поэтому мы преобразуем lambda в ThingCreator<Foo>, у которого есть возвращаемый тип Foo. Поэтому полученный метод возвращает Foo, как ожидается тип делегата.

НО ПОДОЖДИТЕ, вы говорите... Я не могу изменить подпись Test или ThingCreator

Не беспокойтесь! Вы можете выполнить эту работу:

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

Нижняя сторона заключается в том, что теперь каждый из ваших не общих делегатов ThingCreator является делегатом, который вызывает делегат ThingCreator<T>, который является пустой тратой времени и памяти. Но вы получаете вывод типа, преобразование группы методов и преобразование лямбда, чтобы сделать вам метод желаемого типа возвращаемого значения и делегат к этому методу.

Обратите внимание на ограничение class. Вы видите, почему это ограничение должно быть там? Это остается как упражнение для читателя.

Ответ 3

Мое личное предположение - это место вызова. Когда вы передаете () => new Foo() функции, он захватывает ее как ThingCreator и вызывает ее, чтобы получить IThing. Но когда вы посылаете метод factory конкретного типа методу Test, когда метод метода вызывает его, он переходит к конкретному типу Create(), который, в свою очередь, возвращает конкретный объект, который вполне приемлем, поскольку он тоже IThing

Думаю, вам нужно больше, чем мое предположение. Извините, если я ошибаюсь!