Тип возврата делегата, отличный от лямбда-функции
Рассмотрим этот 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
Думаю, вам нужно больше, чем мое предположение. Извините, если я ошибаюсь!