2010-02-21 17 views
34

un comportamiento extraño con la C# 4.0 co- y contravarianza apoyo:co- y contravarianza errores en .NET 4.0

using System; 

class Program { 
    static void Foo(object x) { } 
    static void Main() { 
    Action<string> action = _ => { }; 

    // C# 3.5 supports static co- and contravariant method groups 
    // conversions to delegates types, so this is perfectly legal: 
    action += Foo; 

    // since C# 4.0 much better supports co- and contravariance 
    // for interfaces and delegates, this is should be legal too: 
    action += new Action<object>(Foo); 
    } 
} 

resultados de las TI con ArgumentException: Delegates must be of the same type.

extraño, ¿verdad? ¿Por qué Delegate.Combine() (que se ha llamado al realizar la operación += en los delegados) no es compatible con co y contravariancia en tiempo de ejecución?

Además, he encontrado que el tipo de delegado de BCL System.EventHandler<TEventArgs> no tiene una anotación contravariante en su parámetro genérico TEventArgs! ¿Por qué? Es perfectamente legal, el tipo TEventArgs se usa solo en la posición de entrada. Tal vez no hay ninguna anotación contravariante porque oculta muy bien el error con el Delegate.Combine()? ;)

p.s. Todo esto afecta al VS2010 RC y versiones posteriores.

+2

___ * * ___ Ыome – Nifle

+0

lo siento, he cambiado configuración regional de teclado lugar equivocado ... – ControlFlow

+0

@ControlFlow - Ver http://stackoverflow.com/questions/1120688/event-and-delegate-contravariance-in-net-4-0-and-c-4-0 - Noté este problema en la primera versión beta de VS2010. En ese punto, 'EventHandler ' era contravariante w.r.t. 'TEventArgs'. Pero desde entonces ha sido cambiado de nuevo como has encontrado. –

Respuesta

37

Resumen de la historia larga: la combinación de delegados es todo arruinado con respecto a la varianza. Descubrimos esto tarde en el ciclo. Estamos trabajando con el equipo de CLR para ver si podemos encontrar la forma de que funcionen todos los escenarios comunes sin romper la compatibilidad con versiones anteriores, etc., pero lo que sea que se nos ocurra probablemente no llegue a la versión 4.0. Con suerte lo solucionaremos en algún service pack. Pido disculpas por las molestias.

+19

Eric sus respuestas y especialmente su candidatura son muy apreciado ya que muy pocos de nosotros (supongo) tenemos alguna idea de lo que sucede internamente en Microsoft con .NET y su información a menudo hace que las funciones internas sean mucho menos que una caja negra para mí y el resto de la comunidad aquí (obvio con su 28K rep). –

+5

@Chris: ¡gracias! Me refiero a hacer todo este proceso más transparente. –

+1

¿Ha sucedido algo en esta área desde .net 4.0? 'EventHandler ' aún no está marcado como contravariante en la documentación de vista previa 4.5. – CodesInChaos

6

Covarianza y contravarianza especifica la relación de herencia entre los tipos genéricos. Cuando tiene covarianza & contravariancia, las clases G<A> y G<B> pueden estar en alguna relación de herencia dependiendo de qué A y B sea. Puede beneficiarse de esto cuando llame a métodos genéricos.

Sin embargo, el método Delegate.Combine no es genérico y el documentation clearly says cuando se produce la excepción:

ArgumentException - Tanto un y b no son null referencia (Nothing en Visual Basic), y a y b no son instancias del mismo tipo de delegado.

Ahora, Action<object> y Action<string> son sin duda las instancias de un tipo de delegado diferentes (aunque relacionados a través de relación de herencia), por lo que de acuerdo a la documentación, se lanzará una excepción. Parece razonable que el método Delegate.Combine sea compatible con este escenario, pero eso es solo una posible propuesta (obviamente, esto no era necesario hasta ahora, porque no se pueden declarar delegados heredados, por lo que antes de la co/contravariancia, ningún delegado tenía una relación de herencia) .

+0

Sí, tengo razón sobre la documentación y el comportamiento 'Delegate.Combine()' ... Pero sobre apoyar este escenario : ¿por qué estás hablando de la herencia de los delegados? Para mi escenario, los parámetros genéricos deben tener una relación de herencia, no delegar tipos en sí. Por ejemplo, suscríbase a un 'EventHandler con una instancia de delegado' EventHandler '. – ControlFlow

+0

Más precisamente, debería haber estado hablando de subtipado (es decir, cuando puede tratar el valor del tipo 'A' como un valor de tipo' B'). La covarianza y la contravarianza definen la relación de subtipificación entre los delegados genéricos (en función de una relación subtipo/herencia entre sus parámetros de tipo). Puede convertir cualquier subtipo (heredado) en un supertipo (base o interfaz) y la covarianza entre delegados usa la misma regla de conversión. Sin embargo, eso no se extiende a 'Delegate.Combine' que probablemente usa elementos internos del delegado dado como argumento. –

+0

Eso se documentó incluso antes de que entre en juego la varianza genérica, donde la firma de 'Delegate.Combine (Delegate, Delegate)' toma dos tipos 'Delegate', por lo que dicha excepción era válida para documentar. Esa documentación es engañosa en el contexto de la varianza considerando que esto solo ocurre cuando 'a' ya tiene un delegado de diferente tipo. – nawfal

1

Una dificultad con la combinación de delegados es que a menos que uno especifique qué operando se supone que es el subtipo y cuál es supertipo, no está claro qué tipo debe ser el resultado. Es posible escribir una fábrica de envoltura que convierta a cualquier delegado con un número especificado de argumentos y patrones de byval/byref en un supertipo, pero llamar a esa fábrica varias veces con el mismo delegado produciría diferentes contenedores (esto podría causar estragos en anulación de la suscripción al evento). Uno podría crear alternativamente versiones de Delegate.Combine lo que obligaría al delegado del lado derecho al tipo del delegado izquierdo (como beneficio adicional, la devolución no tendría que ser encasillada), pero uno tendría que escribir una versión especial de delegate.remove para tratar con él.

0

Esta solución fue publicada originalmente por cdhowie para mi pregunta: Delegate conversion breaks equality and unables to disconnect from event pero parece resolver el problema de la covarianza y la contravarianza en el contexto de los delegados de multidifusión.

primero necesita un método de ayuda:

public static class DelegateExtensions 
{ 
    public static Delegate ConvertTo(this Delegate self, Type type) 
    { 
     if (type == null) { throw new ArgumentNullException("type"); } 
     if (self == null) { return null; } 

     if (self.GetType() == type) 
      return self; 

     return Delegate.Combine(
      self.GetInvocationList() 
       .Select(i => Delegate.CreateDelegate(type, i.Target, i.Method)) 
       .ToArray()); 
    } 

    public static T ConvertTo<T>(this Delegate self) 
    { 
     return (T)(object)self.ConvertTo(typeof(T)); 
    } 
} 

Cuando usted tiene un delegado:

public delegate MyEventHandler<in T>(T arg); 

Usted puede utilizarlo mientras que la combinación de los delegados simplemente mediante la conversión de un delegado hacerlo tipo deseado:

MyEventHandler<MyClass> handler = null; 
handler += new MyEventHandler<MyClass>(c => Console.WriteLine(c)).ConvertTo<MyEventHandler<MyClass>>(); 
handler += new MyEventHandler<object>(c => Console.WriteLine(c)).ConvertTo<MyEventHandler<MyClass>>(); 

handler(new MyClass()); 

También es compatible con la desconexión del evento de la misma manera, mediante el uso de ConvertTo() método. A diferencia del uso de una lista personalizada de delegados, esta solución ofrece seguridad de subprocesos de fábrica.

Código completo con algunas muestras se pueden encontrar aquí:? http://ideone.com/O6YcdI