Ковариантные типы экземпляров в открытых экземплярах делегатов - PullRequest
1 голос
/ 17 мая 2011

Я пытаюсь создать делегаты с открытым экземпляром для методов, которые имеют общую сигнатуру, но определены для множества разных и не связанных типов. Эти методы помечены пользовательским атрибутом, и во время выполнения я ищу все методы, помеченные этим атрибутом, чтобы создать делегатов из их 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 не удалась, предположительно, потому что типы параметров не являются ковариантными при связывании делегатов. Переключение подписи делегата на параметр param типа Foo или Bar работает для привязки соответствующего MethodInfo.

Я не верю, что на самом деле возможно связать открытый делегат подобным образом, потому что тогда явный параметр экземпляра не будет подходящего типа для вызова метода. Что меня беспокоит, так это , можно связать закрытый делегат с MethodInfo любого объявленного типа, поскольку это не включает проблемный тип экземпляра. Например, я могу привязать закрытые делегаты к null экземплярам, ​​а затем использовать GetField("_target").SetValue(del, instance) для делегатов непосредственно перед их вызовом. Но это отчасти хакерски.

Теперь, в случае, если есть альтернативные решения, я пытаюсь это сделать, чтобы избежать выделения кучи и упаковки типов значений при непосредственном вызове MethodInfo s, т.е.:

someActionInfo.Invoke(instance, new object[] { someParam });

Это вызывает упаковку типа float, и массив object[] выделяется в куче, причем оба медленно генерируют мусор в куче для вызова, выдаваемого в противном случае.

Ответы [ 4 ]

4 голосов
/ 17 мая 2011

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

static void Foo(Mammal m) {}

тогда вы не можете назначить это делегату, который принимает Animal, потому что вызывающий этот делегат может передать Медузу. Вы можете назначить его делегату, который получает жирафа, потому что тогда вызывающий абонент может передать только жирафов, а жирафы - млекопитающие.

Короче говоря, чтобы быть безопасным, вам нужно контрвариантность , а не ковариация по параметрам.

C # поддерживает это несколькими способами. Во-первых, в C # 4 вы можете сделать это:

Action<Mammal> aa = m=>m.GrowHair();
Action<Giraffe> ag = aa;

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

Во-вторых, в C # 2 и выше вы можете сделать это:

Action<Giraffe> aa = myMammal.GrowHair;

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

Но требуемая ковариация не является типобезопасной и поэтому не поддерживается.

3 голосов
/ 19 мая 2011

Ваша проблема в том, что вы хотите создать делегата, который выполняет две вещи - приведение и вызов метода. Если бы это было возможно, вы бы сделали это с помощью дженериков:

public OpenActionDelegate GetDelegate<T>(MethodInfo method) {
    return (object instance, float someParam) => {
        ((T)instance).method(someParam);
    };
}

К сожалению, первое можно сделать только с помощью дженериков, а второе - только с отражением - поэтому вы не можете объединить их!

Однако, если вы создаете делегаты один раз и используете их много раз, как это, кажется, имеет смысл, может быть эффективно динамически скомпилировать выражение, которое это делает. Прелесть 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 голос
/ 27 мая 2011

Итак, как оказалось, платформа Xbox 360 .NET не очень любит использовать отражение для изменения непубличных полей. (Читайте: это прямо отказывается.) Я предполагаю, что это предотвратит обратное проектирование некоторой конфиденциальной информации XDK. В любом случае, это исключило небольшой взлом рефлексии из вопроса, который я закончил использовать.

Однако я кое-что собрал вместе с общими делегатами. Заменить:

delegate void OpenActionDelegate(object instance, float someParam);

С:

delegate void OpenActionDelegate<T>(T instance, float someParam);

Во время первоначального отражения у меня есть MethodInfo для всех соответствующих типов, что означает, что я могу использовать отражение и MakeGenericType, чтобы создавать безопасные для типов делегаты для действий, не создавая их вручную. Получающиеся открытые делегаты связываются безупречно, поэтому мой список делегатов заполнен. Тем не менее, они хранятся в виде простых Delegate с, что означает, что я не мог получить к ним безопасный доступ без использования запретительного DynamicInvoke.

Как оказалось, метод вызова действия первоначально передавал свои экземпляры как object s, оказывается, что я могу , фактически, сделать его универсальным, чтобы получить правильный тип для обобщенные делегаты и приведите Delegate обратно к OpenActionDelegate<Foo>, например:

internal static void CallAction<T>(int actionID, T instance, float param)
{
    OpenActionDelegate<T> d = (OpenActionDelegate<T>)_delegateMap[actionID];

}

Таким образом, я полностью уклоняюсь от проблемы ковариации, получаю скорость прямого вызова делегата и избегаю бокса. Единственное неудобство заключается в том, что этот подход не будет работать с наследованием, поскольку делегаты привязаны к своему объявленному типу. Мне придется значительно улучшить процесс отражения, если мне когда-нибудь понадобится поддержка наследования.

0 голосов
/ 20 марта 2012

Поскольку делегаты предшествуют шаблонам, они не могут делать все то, что могут универсальные интерфейсы.Я не совсем понимаю, что вы пытаетесь сделать, но кажется, что интерфейсы могут обойтись без Reflection, если вы можете добавить соответствующие интерфейсы к классам, чьи методы вы хотите вызывать.Например, если все интересующие вас методы находятся в IWoozable и принимают параметр float, то вы можете определить интерфейс IWoozer с методом void Woozle(IWoozable target, float someParam); Одна реализация IWoozer может быть

void Woozle(IWoozable target, float someParam)
{
  target.Method1(someParam);
}

Другой может быть похожим, но использовать Method2 и т. Д. Можно легко вставить произвольный код без необходимости делегатов и иметь выбор действия, не зависящий от выбора цели.

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

interface IActUpon<T>
{
  void Act(ref T target);
  void ActWithParam<PT1>(ref T target, ref PT param);
}

Класс, который содержит T, может затем представить его методу, подобному приведенному выше:

  void ActUponMyThing<ActorType>(ref ActorType Actor) where ActorType:IActUpon<T> 
  {
    Actor.Act(ref T myThing);
  }
  void ActUponMyThingWithParam<ActorType,PT>(ref IActUpon<PT> Actor, ref PT param)
    where ActorType:IActUpon<T> 
  {
    Actor.ActWithParam(ref T myThing, ref PT param);
  }

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...