#c# #.net #reflection #delegates
#c# #.net #отражение #делегаты
Вопрос:
Я пытаюсь создать делегаты открытых экземпляров для методов, которые используют общую сигнатуру, но определены для множества разных и несвязанных типов. Эти методы помечены пользовательским атрибутом, и во время выполнения я просматриваю все методы, помеченные этим атрибутом, чтобы сконструировать делегатов из их MethodInfo
s. Например, учитывая делегат:
delegate void OpenActionDelegate(object instance, float someParam);
Я хотел бы сопоставить методы:
void Foo.SomeAction(float someParam);
void Bar.SomeOtherAction(float someParam);
Где Foo
и Bar
являются совершенно не связанными классами. Вооружившись MethodInfo
для любого метода, я хотел бы в конечном итоге иметь возможность получить открытый делегат следующим образом:
MethodInfo fm = typeof(Foo).GetMethod("SomeAction", BindingFlags.Public | BindingFlags.Instance);
MethodInfo bm = typeof(Bar).GetMethod("SomeOtherAction", BindingFlags.Public | BindingFlags.Instance);
OpenActionDelegate fd = (OpenActionDelegate)Delegate.CreateDelegate(typeof(OpenActionDelegate), fm);
OpenActionDelegate bd = (OpenActionDelegate)Delegate.CreateDelegate(typeof(OpenActionDelegate), bm);
Проблема, с которой я сталкиваюсь, заключается в типе явной спецификации экземпляра в делегате. Поскольку у этих методов нет гарантированного базового типа, на котором они будут определены, я попробовал просто установить object
. Но попытка связать MethodInfo
завершается неудачей, предположительно потому, что типы параметров не являются ковариантными при привязке делегатов. Переключение подписи делегата так, чтобы параметр экземпляра имел тип Foo
или Bar
, работает для привязки соответствующего MethodInfo
.
Я не верю, что на самом деле возможно привязать открытый делегат подобным образом, потому что тогда явный параметр экземпляра не был бы подходящего типа для вызова метода. Что меня беспокоит, так это то, что возможно привязать закрытый делегат к MethodInfo
любому объявляемому типу, поскольку это не включает проблемный тип экземпляра. Например, я могу привязать закрытых делегатов к null
экземплярам, а затем использовать GetField("_target").SetValue(del, instance)
для делегатов непосредственно перед их вызовом. Но это своего рода халтура.
Теперь, на случай, если существуют альтернативные решения, причина, по которой я хочу это сделать, заключается в том, чтобы избежать выделения кучи и боксирования типа значения при прямом вызове MethodInfo
s, т.Е.:
someActionInfo.Invoke(instance, new object[] { someParam });
Это приводит к блокировке float
типа, и object[]
массив выделяется в куче, оба медленно генерируют мусор кучи для одноразового вызова в противном случае.
Ответ №1:
Типы параметров, включая неявный параметр «this», явно не могут быть ковариантными и при этом быть типозащищенными. Забудьте на мгновение о методах экземпляра и просто подумайте о статических методах. Если у вас есть
static void Foo(Mammal m) {}
тогда вы не можете назначить это делегату, который принимает Animal , потому что вызывающий этот делегат может передать Jellyfish. Вы можете назначить его делегату, который принимает Giraffe, хотя, потому что тогда вызывающий может передавать только Giraffes, а жирафы — млекопитающие.
Короче говоря, для обеспечения безопасности типов вам нужна контравариантность, а не ковариация параметров.
C # поддерживает это несколькими способами. Во-первых, в C # 4 вы можете сделать это:
Action<Mammal> aa = m=>m.GrowHair();
Action<Giraffe> ag = aa;
То есть преобразования для общего типа действия являются контравариантными, когда изменяющиеся параметры типа являются ссылочными типами.
Во-вторых, в C # 2 и выше вы можете сделать это:
Action<Giraffe> aa = myMammal.GrowHair;
То есть преобразования группы методов в делегат являются контравариантными в типах параметров метода.
Но тип ковариации, который вы хотите, не является типобезопасным и, следовательно, не поддерживается.
Комментарии:
1. Это имеет смысл, если строго учитывать «this» в руководстве в качестве параметра. Но при привязке открытого делегата у меня на самом деле есть methodinfo с допустимым типом объявления — таким образом, я надеялся, что среда CLR позволит мне привязать его и рискнуть прострелить себе ногу, если я позже вызову его с недопустимым типом. Но я думаю, что это не тот случай!
Ответ №2:
Ваша проблема в том, что вы хотите создать делегат, который выполняет две вещи — приведение и вызов метода. Если бы это было возможно, вы бы сделали это с помощью дженериков:
public OpenActionDelegate GetDelegate<T>(MethodInfo method) {
return (object instance, float someParam) => {
((T)instance).method(someParam);
};
}
К сожалению, первое может быть выполнено только с помощью generics, а второе — только с помощью reflection — так что вы не можете объединить их!
Однако, если вы создаете делегаты один раз и используете их много раз, как это кажется, может оказаться эффективным динамически компилировать выражение, которое выполняет это. Прелесть Expression<T>
в том, что с ним можно делать что угодно — по сути, вы выполняете метапрограммирование.
public static OpenActionDelegate GetOpenActionDelegate(Type type, string methodName) {
MethodInfo method = type.GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance);
ParameterExpression instance = Expression.Parameter(typeof(object));
ParameterExpression someParam = Expression.Parameter(typeof(float));
Expression<OpenActionDelegate> expression = Expression.Lambda<OpenActionDelegate>(
Expression.Call(
Expression.Convert(
instance,
type
),
method,
someParam
),
instance,
someParam
);
return expression.Compile();
}
Этот метод скомпилирует и вернет объект, OpenActionDelegate
который преобразует свои параметры в type
и вызывает methodName
его. Вот несколько примеров использования:
public static void Main() {
var someAction = GetOpenActionDelegate(typeof(Foo), "SomeAction");
var someOtherAction = GetOpenActionDelegate(typeof(Bar), "SomeOtherAction");
Foo foo = new Foo();
someAction(foo, 1);
Bar bar = new Bar();
someOtherAction(bar, 2);
// This will fail with an InvalidCastException
someOtherAction(foo, 2);
Console.ReadKey(true);
}
Комментарии:
1. На самом деле это очень элегантная альтернатива! Я не знал о выражениях. К сожалению, пространство имен недоступно в версии платформы Xbox 360, поэтому я не могу его использовать. Но приятно знать, что это есть.
Ответ №3:
Итак, как выясняется, платформа Xbox 360 .NET Framework не очень любит использовать отражение для изменения закрытых полей. (Читайте: он прямо отказывается.) Я полагаю, это для предотвращения обратного проектирования некоторой конфиденциальной информации XDK. В любом случае, это исключило небольшой взлом отражения из вопроса, который я в конечном итоге использовал.
Однако я кое-что собрал вместе с универсальными делегатами. Заменить:
delegate void OpenActionDelegate(object instance, float someParam);
С:
delegate void OpenActionDelegate<T>(T instance, float someParam);
Во время первоначального размышления у меня есть MethodInfo
для всех соответствующих типов, что означает, что я могу использовать отражение и MakeGenericType
создавать типобезопасные делегаты для действий без необходимости создавать их все вручную. Результирующие открытые делегаты связываются безупречно, поэтому мой список делегатов заполнен. Однако они хранятся в виде простых Delegate
файлов, что означает, что я не мог безопасно получить к ним доступ, не используя запретительный DynamicInvoke
.
Как оказалось, метод вызова action изначально передавал свои экземпляры как object
s, оказывается, я могу фактически сделать его универсальным, чтобы получить правильный тип для универсальных делегатов, и привести Delegate
s обратно к OpenActionDelegate<Foo>
, например:
internal static void CallAction<T>(int actionID, T instance, float param)
{
OpenActionDelegate<T> d = (OpenActionDelegate<T>)_delegateMap[actionID];
}
Таким образом, я полностью устраняю проблему ковариации, получаю скорость прямого вызова делегата и избегаю боксирования. Единственное неудобство заключается в том, что этот подход не будет работать с наследованием, поскольку делегаты привязаны к их объявляющему типу. Мне придется значительно улучшить процесс отражения, если мне когда-нибудь понадобится поддерживать наследование.
Ответ №4:
Поскольку делегаты предшествуют универсальным интерфейсам, они не могут выполнять все то, что могут универсальные интерфейсы. Я не совсем понимаю, что вы пытаетесь сделать, но это похоже на то, что интерфейсы могли бы делать без необходимости отражения, если вы можете добавить соответствующие интерфейсы к классам, методы которых вы хотите вызвать. Например, если все интересующие вас методы находятся в IWoozable и принимают float
параметр, тогда вы могли бы определить интерфейс IWoozer
с помощью метода, void Woozle(IWoozable target, float someParam);
Одна IWoozer
реализация могла бы быть
void Woozle (настраиваемая цель, плавающий некоторый параметр) { цель.Метод1 (некоторый параметр); }
Другой мог бы быть похожим, но использовать Method2 и т.д. Можно было бы легко вставить произвольный код без необходимости в делегатах и сделать так, чтобы выбор действия не зависел от выбора цели.
Еще одна вещь, которую можно сделать с универсальными интерфейсами, чего нельзя сделать с делегатами, — это включить открытые универсальные методы. Например, у одного мог бы быть интерфейс
IActUpon интерфейса<T> { недействительный акт (ссылка на цель); аннулирует ActWithParam<PT1>(ссылка на T target, ссылка на PT param); }
Класс, который содержит T
, может затем предоставить его методу, подобному приведенному выше:
аннулирует ActUponMyThing<ActorType>(ссылается на ActorType Actor), где ActorType:IActUpon<T> { Актер.Действие (ссылка на MyThing); } аннулирует actponmythingwithparam<ActorType,PT>(ссылка на actupon<PT> Actor, ссылка на параметр PT) где ActorType:IActUpon<T> { Актер.ActWithParam(ссылка на MyThing, ссылка на PT param); }
Использование ограниченных универсальных типов для интерфейсов позволяет в некоторых случаях использовать структуры и избегать упаковки, что было бы невозможно с делегатами. Кроме того, открытые универсальные методы могут применять множество ограничений к своим параметрам универсального типа и вызывать универсальные методы, которые также имеют множество ограничений, даже если классы, которые реализуют ограничения, не имеют общего базового типа.