Великий и могучий Delegate.CreateDelegate
Сегодня хотелось бы поделиться мыслями относительно замечательного метода System.Delegate.
CreateDelegate
, доступного ещё с первых версий .NET Framework. Назначение - создать экземпляр делегата динамически по типу делегата (в виде System.Type
) и методу, заданному в виде строкового имени или экземпляра System.Reflection.MethodInfo
. Интерес представляет то, как данный метод позволяет сопоставить те или иные сигнатуры методов различным типам делегатов.
Давайте определим класс и структуру следующего вида:
class FooClass {
public static void StaticBar(object x) { }
public string InstanceBar(int x) { return "abc"; }
}
struct FooStruct {
public void InstanceBoo(int x) { }
}
И посмотрим делегаты какого типа мы можем создать:
- Делегат из статического метода
Так же как статически в C# из метода FooClass.StaticBar
можно создать делегат типа Action<object>
:
Action<object> staticBar = FooClass.StaticBar;
Можно аналогично использовать и Delegate.CreateDelegate
:
var staticBar = (Action<object>)
Delegate.CreateDelegate(
type: typeof(Action<object>),
method: typeof(FooClass).GetMethod("StaticBar"));
- Делегат из метода экземпляра
Опять же, аналогично статическому поведению C#:
Func<int, string> instanceBar1 = new FooClass().InstanceBar;
Можно создавать делегаты из методов уровня экземпляра, указывая через дополнительный параметр firstArgument
экземпляр объекта, для которого будет вызываться выбранный метод экземпляра:
var instanceBar = (Func<int, string>) Delegate.CreateDelegate(
type: typeof(Func<int, string>),
method: typeof(FooClass).GetMethod("InstanceBar"),
firstArgument: new FooClass()); /* <== */
Помимо методов уровня экземпляра классов, поддерживаются и методы экземпляров типов-значений, при этом структура будет подвергнута боксингу:
var instanceBoo = (Action<int>) Delegate.CreateDelegate(
type: typeof(Action<int>),
method: typeof(FooStruct).GetMethod("InstanceBoo"),
firstArgument: new FooStruct()); /* <== */
- Делегат из метода с отличающимся типом параметров
C# статически поддерживает контравариантность типов параметров при создании экземпляров делегатов из method group. Например, возможно подписаться методом с сигнатурой:
void Foo(object sender, EventArgs e)
на событие, ожидающее делегат типа:
void PropertyChangedEventHandler(object sender, PropertyChangedEventArgs e)
Так как класс PropertyChangedEventArgs
является наследником класса EventArgs
. Аналогично допустимо создать такой делегат, так как string
является наследником object
:
Action<string> contravariantParameterType = FooClass.StaticBar;
Delegate.CreateDelegate
повторяет статическое поведение C# и тоже поддерживает контравариантность:
var contravariantParameterType = (Action<string>) Delegate.CreateDelegate(
type: typeof(Action<string>),
method: typeof(FooClass).GetMethod("StaticBar"));
- Делегат из метода с отличающимся типом возвращаемого значения
Аналогично C# поддерживает ковариантность типа возвращаемого значения:
Func<int, object> covariantReturnType = new FooClass().InstanceBar;
Delegate.CreateDelegate
не отстаёт:
var covariantReturnType = (Func<int, object>) Delegate.CreateDelegate(
type: typeof(Func<int, object /* <== */>),
method: typeof(FooClass).GetMethod("InstanceBar"),
firstArgument: new FooClass());
- Делегат из метода экземпляра ссылочного типа с открытым первым аргументом
Тут всё становится интереснее, так как C# не позволяет создавать такие делегаты статически. Дело в том, что если не указывать экземпляр через параметр firstArgument
и подобрать тип делегата таковым, чтобы первый аргумент делегата был ссылочного типа, определяющего данный метод, то можно создать экземпляр делегата так, как если бы метод экземпляра был статическим:
var instanceBarAsStatic = (Func<FooClass, int, string>) Delegate.CreateDelegate(
type: typeof(Func<FooClass /* <== */, int, string>),
method: typeof(FooClass).GetMethod("InstanceBar"));
А затем вызывать делегат для различных экземпляров:
var foo1 = new FooClass();
var foo2 = new FooClass();
instanceBarAsStatic(foo1, 1);
instanceBarAsStatic(foo2, 2);
instanceBarAsStatic(null, 3); // NRE?
Но тут надо быть очень осторожным, так как в случае третьего вызова проверка экземпляра (this
) на null
перестаёт действовать:
Будьте аккуратны ;)
- Делегат из статического метода с закрытым (фиксированным) первым аргументом ссылочного типа
Теперь провернём предыдущий трюк наоборот: укажем firstArgument
в случае создания делегата из статического метода и уберём из типа делегата первый аргумент:
var staticBarWithFixedArg = (Action) Delegate.CreateDelegate(
type: typeof(Action),
method: typeof(FooClass).GetMethod("StaticBar"),
firstArgument: new object()); /* <== */
Теперь экземпляр new object()
“запомнится” внутри экземпляра делегата и будет автоматически подставляться как первый аргумент при каждом вызове. Для того, чтобы создать такой делегат, первый аргумент обязан быть ссылочного типа. Фактически мы получили каррирование первого аргумента метода.
- Делегат из метода экземпляра типа-значения с открытым первым аргументом
Возможность создавать делегаты такого типа я обнаружил совсем недавно, просто размышляя об устройстве методов уровня экземпляра, определённых для структур C#. На самом деле практически во всех методах экземпляров this
представляет собой обычный ref
-параметр (или out
-параметр в конструкторах структур), которому ещё и можно присваивать! Раз this
- это ref
-параметр, то логично было попробовать создать тип делегата соответствующей сигнатуры (все типы Action
- и Func
-делегатов не предполагают наличие ref
-/out
-параметров):
delegate void FooStructBooRef(ref FooStruct foo, int x);
И попробовать создать делегат, не указывая параметр firstArgument
:
var instanceBooAsStaticWithRef = (FooStructBooRef) Delegate.CreateDelegate(
type: typeof(FooStructBooRef /* <== */),
method: typeof(FooStruct).GetMethod("InstanceBoo"));
Оказалось, что это работает и можно без проблем подменять структуру при вызове:
var foo1 = new FooStruct();
var foo2 = new FooStruct();
instanceBooAsStaticWithRef(ref foo1, 1);
instanceBooAsStaticWithRef(ref foo2, 2);
Из нюансов тут следует отметить то, что можно создать и тип делегата с первым out
-параметром (для рантайма не существует различия между ref
- и out
-параметрами кроме атрибута, который фактически использует только компилятор C#), но нельзя создать такие делегаты из виртуальных методов GetHashCode
, Equals
и ToString
, унаследованных от System.Object
, так как вызов данных методов всегда требуют боксинга типов-значений.
Бонус
Хочется описать один workaround, раз пост посвящён делегатам, то пусть будет здесь. Однажды я столкнулся со следующей проблемой:
Expression<Func<string>> expr = () => "abc";
Func<string> func = expr.Compile();
// ArgumentException:
// The object must be a runtime Reflection object.
func.BeginInvoke(
a => Console.WriteLine(func.EndInvoke(a)),
null);
Оказывается среда выполнения не поддерживает методы BeginInvoke
/EndInvoke
делегатов, созданных с помощью класса System.Reflection.Emit.DynamicMethod
(который используют Expression Trees в .NET).
Исправить достаточно легко, надо лишь обернуть DynamicMethod-
делегат в другой делегат из обычного метода и вызывать у него BeginInvoke
, но мне не были заранее известны сигнатуры делегатов и это было затруднительно. Я решил проблему очень просто - создал делегат прямо из метода Invoke
экземпляра другого делегата:
Expression<Func<string>> expr = () => "abc";
Func<string> func = expr.Compile();
func = (Func<string>) Delegate.CreateDelegate(
type: typeof(Func<string>),
method: typeof(Func<string>).GetMethod("Invoke"),
firstArgument: func);
func.BeginInvoke( // OK now
a => Console.WriteLine(func.EndInvoke(a)),
null);
Быть может кому-нибудь это пригодится.