Tipos de instancias covariantes en delegados de instancias abiertas

Estoy tratando de crear delegados de instancia abiertos para métodos que comparten una firma común, pero están definidos en muchos tipos diferentes y no relacionados. Estos métodos están etiquetados con un atributo personalizado, y en tiempo de ejecución busco todos los métodos etiquetados con este atributo para construir delegados a partir de su MethodInfos. Por ejemplo, dado el delegado:

delegate void OpenActionDelegate(object instance, float someParam);

Me gustaría hacer coincidir los métodos:

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

Dónde Foo y Bar son clases completamente ajenas. Armado con el MethodInfo para cualquier método, me gustaría finalmente poder obtener un delegado abierto como este:

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

El problema con el que me encuentro es el tipo de especificación de instancia explícita en el delegado. Dado que estos métodos no tienen un tipo base garantizado en el que se definirán, he intentado configurar object. Pero tratando de unir el MethodInfo falla, presumiblemente porque los tipos de parámetros no son covariantes cuando se vinculan delegados. Cambiar la firma del delegado para que el parámetro de instancia sea de tipo Foo or Bar trabaja para encuadernar el correspondiente MethodInfo.

No creo que sea realmente posible vincular un delegado abierto como este, porque entonces el parámetro de instancia explícito no sería del tipo apropiado para llamar al método. Lo que me molesta es is posible vincular un delegado cerrado a un MethodInfo de cualquier tipo de declaración, ya que no incluye el tipo de instancia problemático. Por ejemplo, puedo vincular delegados cerrados a null instancias, y luego use GetField("_target").SetValue(del, instance) sobre los delegados justo antes de invocarlos. Pero eso es un poco hacker.

Ahora, en caso de que existan soluciones alternativas, la razón por la que estoy buscando hacer esto es para evitar la asignación de montón y el tipo de caja de valor al invocar directamente el MethodInfos, es decir:

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

Esto provoca el boxeo del float tipo, y el object[] La matriz se asigna en el montón, y ambos generan lentamente basura del montón para una invocación de otro modo desechable.

preguntado el 16 de mayo de 11 a las 20:05

4 Respuestas

Los tipos de parámetros, incluido el parámetro implícito "this", claramente no se pueden covariante y aún así tener seguridad en los tipos. Olvídese de los métodos de instancia por un momento y piense en los métodos estáticos. Si usted tiene

static void Foo(Mammal m) {}

entonces no puede asignar eso a un delegado que toma un Animal, porque la persona que llama a ese delegado podría pasar en una medusa. Sin embargo, puede asignarlo a un delegado que tome una jirafa, porque entonces la persona que llama solo puede pasar jirafas, y las jirafas son mamíferos.

En resumen, para tener seguridad en los tipos de letra necesita contravarianzano, covarianza en los parámetros.

C # admite eso de varias maneras. En primer lugar, en C # 4 puede hacer esto:

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

Es decir, las conversiones en el tipo de acción genérico son contravariantes cuando los parámetros de tipo variable son tipos de referencia.

En segundo lugar, en C # 2 y superior puede hacer esto:

Action<Giraffe> aa = myMammal.GrowHair;

Es decir, las conversiones de grupos de métodos para delegar son contravariantes en los tipos de parámetros del método.

Pero el tipo de covarianza que desea no es de tipo seguro y, por lo tanto, no es compatible.

contestado el 17 de mayo de 11 a las 01:05

Eso tiene sentido, cuando se considera estrictamente el manual "esto" como parámetro. Pero al vincular el delegado abierto, en realidad tengo un methodinfo con un tipo de declaración válido; como tal, esperaba que CLR me permitiera vincularlo y arriesgarme a dispararme en el pie si luego lo invocaba con un tipo no válido. ¡Pero supongo que este no es el caso! - Camille

Su problema es que desea crear un delegado que haga dos cosas: una conversión y una llamada a un método. Si fuera posible, lo haría con genéricos:

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

Desafortunadamente, el primero solo se puede hacer con genéricos y el segundo solo con reflexión, ¡por lo que no puede combinar los dos!

Sin embargo, si crea los delegados una vez y los usa muchas veces, como parece ser el caso, podría ser eficiente compilar dinámicamente una expresión que lo haga. La belleza de Expression<T> es que tu puedes hacer cualquier cosa con él, básicamente estás haciendo metaprogramación.

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

Este método compilará y devolverá un OpenActionDelegate que lanza sus parámetros a type y llama methodName en eso. Aquí hay un ejemplo de uso:

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

contestado el 19 de mayo de 11 a las 05:05

¡Esa es en realidad una alternativa muy elegante! No sabía de Expressions. Desafortunadamente, el espacio de nombres no está disponible en la versión Xbox 360 del marco, por lo que no puedo usarlo. Pero es bueno saber que está ahí. - Camille

Entonces, resulta que al marco .NET de Xbox 360 no le gusta mucho usar la reflexión para cambiar los campos no públicos. (Leer: Se niega rotundamente a hacerlo). Me imagino que eso es para evitar la ingeniería inversa de cierta información confidencial de XDK. En cualquier caso, eso descartó el pequeño truco de reflexión de la pregunta que había terminado usando.

Sin embargo, hice algo junto con delegados genéricos. Reemplazar:

delegate void OpenActionDelegate(object instance, float someParam);

Con:

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

Durante la reflexión inicial, tengo la MethodInfo para todos los tipos relevantes, lo que significa que puedo usar la reflexión y MakeGenericType para crear delegados de tipo seguro para las acciones sin tener que crearlos todos a mano. Los delegados abiertos resultantes se unen sin problemas, por lo que mi lista de delegados se completa. Sin embargo, estos se almacenan como simples Delegates, lo que significa que no podría acceder a ellos de forma segura sin usar un prohibitivo DynamicInvoke.

Como resultado, el método de invocación de acción originalmente pasó sus instancias como objects, resulta que yo enlatado , de hecho, hazlo genérico para obtener el tipo adecuado para los delegados genéricos, y lanza el Delegates de vuelta a un OpenActionDelegate<Foo>, por ejemplo:

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

}

De esta manera, evito el problema de la covarianza por completo, obtengo la velocidad de una llamada directa a un delegado y evito el boxeo. El único inconveniente es que este enfoque no funcionará con la herencia, ya que los delegados están vinculados a su tipo de declaración. Tendré que mejorar considerablemente el proceso de reflexión si alguna vez necesito admitir la herencia.

contestado el 27 de mayo de 11 a las 06:05

Debido a que los delegados son anteriores a los genéricos, no pueden hacer todas las cosas que pueden hacer las interfaces genéricas. No tengo muy claro qué está tratando de hacer, pero parece que las interfaces podrían hacer sin necesidad de Reflection, si puede agregar las interfaces adecuadas a las clases cuyos métodos desea llamar. Por ejemplo, si todos los métodos que le interesan están en IWoozable y toma un float parámetro, entonces podría definir una interfaz IWoozer con metodo void Woozle(IWoozable target, float someParam); Una IWoozer la implementación podría ser

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

Otro podría ser similar pero usar Method2, etc. Se podría introducir fácilmente un código arbitrario sin necesidad de delegados, y la elección de la acción sería independiente de la elección del objetivo.

Otra cosa que se puede hacer con interfaces genéricas que no se puede hacer con delegados es incluir métodos genéricos abiertos. Por ejemplo, se podría tener una interfaz

interfaz IActUpon {acto nulo (ref T objetivo); void ActWithParam (ref T objetivo, ref PT param); }

Una clase que tiene un T luego puede exponerlo a un método como el anterior:

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

El uso de tipos genéricos restringidos para las interfaces hace posible en algunos casos usar estructuras y evitar el boxing, algo que no sería posible con los delegados. Además, es posible que los métodos genéricos abiertos apliquen múltiples restricciones a sus parámetros de tipo genérico y llamen a métodos genéricos que también tienen múltiples restricciones, incluso si las clases que implementan las restricciones no comparten un tipo base común.

respondido 20 mar '12, 00:03

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.