开放实例委托上的协变实例类型

时间:2011-05-16 20:21:39

标签: c# .net reflection delegates

我正在尝试为共享共同签名的方法创建开放实例委托,但是在许多不同且不相关的类型上定义。这些方法用自定义属性标记,并且在运行时我查找用此属性标记的所有方法,以便从MethodInfo构建委托。例如,给定代表:

delegate void OpenActionDelegate(object instance, float someParam);

我想匹配方法:

void Foo.SomeAction(float someParam);
void Bar.SomeOtherAction(float someParam);

FooBar完全不相关的类。对于任何一种方法都支持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失败,可能是因为绑定委托时参数类型不协变。切换委托签名以使实例参数的类型为FooBar,用于绑定相应的MethodInfo

我不相信它实际上可能绑定这样的开放委托,因为那时显式实例param将不适合调用该方法的类型。困扰我的是可以将一个封闭的委托绑定到任何声明类型的MethodInfo,因为那不包括麻烦的实例类型。举个例子,我可以将已关闭的委托绑定到null个实例,然后在调用它们之前在委托上使用GetField("_target").SetValue(del, instance)。但这有点像黑客。

现在,如果有替代解决方案,我希望这样做的原因是在直接调用MethodInfo时避免堆分配和值类型装箱,即:

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

这导致float类型的装箱,并且object[]数组在堆上分配,两者都缓慢地产生堆垃圾以进行新的调用。

4 个答案:

答案 0 :(得分:4)

参数类型,包括隐式“this”参数,显然不能是协变,并且仍然是类型安全的。暂时忘掉实例方法,只考虑静态方法。如果你有

static void Foo(Mammal m) {}

然后您无法将其分配给接受Animal的委托,因为该委托的调用者可以传入Jellyfish。你可以将它分配给一个带长颈鹿的代表,因为那时呼叫者只能传递长颈鹿,而长颈鹿则是哺乳动物。

简而言之,要进行类型安全,您需要逆转,而不是参数协方差

C#确实以两种方式支持它。首先,在C#4中你可以这样做:

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

也就是说,当变量类型参数是引用类型时,泛型操作类型的转换是逆变

其次,在C#2及以上版本中你可以这样做:

Action<Giraffe> aa = myMammal.GrowHair;

也就是说,方法组转换为委托在方法的参数类型中是逆变的。

但是你想要的那种协方差并不是类型安全的,因此不受支持。

答案 1 :(得分:3)

您的问题是您要创建一个执行两项操作的委托 - 强制转换和方法调用。如果有可能,你可以用泛型来做:

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

答案 2 :(得分:1)

事实证明,Xbox 360 .NET框架并不是非常喜欢使用反射来更改非公共字段。 (阅读:它完全拒绝。)我想这是为了防止对一些敏感的XDK信息进行逆向工程。无论如何,这排除了我最终使用的问题的反思。

然而,我确实与通用代表团结在一起。替换:

delegate void OpenActionDelegate(object instance, float someParam);

使用:

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

在初始反思期间,我对所有相关类型都有MethodInfo,这意味着我可以使用反射和MakeGenericType为操作创建类型安全的委托,而无需手动创建它们。生成的开放代理绑定完美,因此填充了我的委托列表。但是,它们存储为普通Delegate s,这意味着如果不使用禁止的DynamicInvoke我就无法安全地访问它们。

事实证明,动作调用方法最初将其实例作为object传递,事实证明,我可以,实际上,它是通用的,以便获得正确的为通用委托输入类型,并将Delegate强制转换为OpenActionDelegate<Foo>,例如:

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

}

所以这样,我完全避开协方差问题,获得直接委托召唤的速度并避免拳击。唯一不方便的是,这种方法不适用于继承,因为委托绑定到它们的声明类型。如果我需要支持继承,我将不得不大大改进反射过程。

答案 3 :(得分:0)

因为代理人早于泛型,所以他们无法完成通用接口所能做的所有事情。我不太清楚你想要做什么,但是如果你可以为你想要调用它们的方法的类添加适当的接口,那么接口似乎可以不需要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);
  }

对接口使用约束泛型类型可以在某些情况下使用结构并避免装箱,这是委托无法实现的。此外,开放泛型方法可以将多个约束应用于其泛型类型参数,并调用同样具有多个约束的泛型方法,即使实现约束的类不共享公共基类型。