2009-07-13 9 views
25

En examinant this question, j'ai été curieux de savoir comment les nouvelles caractéristiques de covariance/contravariance dans C# 4.0 l'affecteront.La contravariance des événements et des délégués dans .NET 4.0 et C# 4.0

Dans la version bêta 1, C# semble être en désaccord avec le CLR. Back in C# 3.0, si vous aviez:

public event EventHandler<ClickEventArgs> Click; 

... et ailleurs, vous aviez:

button.Click += new EventHandler<EventArgs>(button_Click); 

... le compilateur barf parce qu'ils sont les types de délégués incompatibles. Mais dans C# 4.0, il compile bien, car dans CLR 4.0 le paramètre de type est maintenant marqué comme in, donc il est contravariant, et donc le compilateur suppose que le délégué de multidiffusion += fonctionnera.

Voici mon test:

public class ClickEventArgs : EventArgs { } 

public class Button 
{ 
    public event EventHandler<ClickEventArgs> Click; 

    public void MouseDown() 
    { 
     Click(this, new ClickEventArgs()); 
    } 
} 

class Program 
{  
    static void Main(string[] args) 
    { 
     Button button = new Button(); 

     button.Click += new EventHandler<ClickEventArgs>(button_Click); 
     button.Click += new EventHandler<EventArgs>(button_Click); 

     button.MouseDown(); 
    } 

    static void button_Click(object s, EventArgs e) 
    { 
     Console.WriteLine("Button was clicked"); 
    } 
} 

Mais bien qu'il compile, il ne fonctionne pas lors de l'exécution (ArgumentException: Les délégués doivent être du même type).

Ce n'est pas grave si vous ajoutez uniquement l'un des deux types de délégué. Mais la combinaison de deux types différents dans une multidiffusion provoque l'exception lorsque la seconde est ajoutée.

Je suppose qu'il s'agit d'un bogue dans le CLR de la version bêta 1 (le comportement du compilateur semble idéal).

Mise à jour pour Release Candidate:

Le code ci-dessus ne compile. Il doit être que la contravariance de TEventArgs dans le type de délégué EventHandler<TEventArgs> a été annulée, alors que ce délégué a la même définition que dans .NET 3.5.

C'est, la bêta je regardais doit avoir:

public delegate void EventHandler<in TEventArgs>(object sender, TEventArgs e); 

Maintenant il est de retour à:

public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e); 

Mais le paramètre délégué Action<T>T est encore contravariant:

public delegate void Action<in T>(T obj); 

La même chose vaut pour Func<T>T étant covariant.

Ce compromis a beaucoup de sens, à condition de supposer que l'utilisation principale des délégués de multidiffusion est dans le contexte des événements. J'ai personnellement constaté que je n'utilise jamais de délégués de multidiffusion sauf en tant qu'événements. Je suppose que les normes de codage C# peuvent désormais adopter une nouvelle règle: ne formez pas de délégués de multidiffusion à partir de plusieurs types de délégués liés par covariance/contravariance. Et si vous ne savez pas ce que cela signifie, évitez simplement d'utiliser Action pour que les événements soient du bon côté.

Bien entendu, cette conclusion a des implications pour the original question that this one grew from ...

+1

Thoiugh intéressant est la question? –

+0

Je suis surpris que ce trou a échappé à l'attention de l'équipe C#, devrait être l'une des premières choses qu'ils auraient testé après l'introduction de la variance pour les délégués génériques n'est-ce pas? C# 5 l'exhibe aussi (la version clr étant la même). – nawfal

Répondre

0

Êtes-vous obtenir les ArgumentException à la fois?Si l'exception est lancée uniquement par le nouveau gestionnaire, je pense que c'est rétro-compatible.

BTW, je pense que vous avez vos commentaires mélangés. En C# 3.0 ceci:

button.Click += new EventHandler<EventArgs>(button_Click); // old

aurait pas courir. C'est C# 4.0

+0

J'ai supprimé toutes les références à la gestion des versions dans la question, car cela semble vous avoir causé une certaine confusion. Les commentaires sur les nouveaux et les anciens se rapportaient à la question à laquelle je pensais à l'origine, dont j'ai séparé la question. Cette question n'a rien à voir avec la rétrocompatibilité en soi.Il s'agit du compilateur en supposant qu'un délégué de multidiffusion peut se lier à des méthodes de types contravariants, ce qui s'avère ensuite ne pas être vrai au moment de l'exécution. –

9

Très intéressant. Vous n'avez pas besoin d'utiliser des événements pour voir ce qui se passe, et en effet je trouve plus simple d'utiliser des délégués simples.

Envisager Func<string> et Func<object>. En C# 4.0, vous pouvez implicitement convertir un Func<string> en Func<object> car vous pouvez toujours utiliser une référence de chaîne comme référence d'objet. Cependant, les choses vont mal quand vous essayez de les combiner. Voici un programme court mais complet montrant le problème de deux façons différentes:

using System; 

class Program 
{  
    static void Main(string[] args) 
    { 
     Func<string> stringFactory =() => "hello"; 
     Func<object> objectFactory =() => new object(); 

     Func<object> multi1 = stringFactory; 
     multi1 += objectFactory; 

     Func<object> multi2 = objectFactory; 
     multi2 += stringFactory; 
    }  
} 

Cette compile très bien, mais les deux Combine appels (caché par le + = sucre syntaxique) lancer des exceptions. (Commentez le premier pour voir le second.)

Ceci est définitivement un problème, bien que je ne sois pas vraiment sûr de ce que la solution devrait être. Il est possible que au moment de l'exécution le code de délégué devra définir le type le plus approprié à utiliser en fonction des types de délégué impliqués. C'est un peu méchant. Il serait très agréable d'avoir un appel Delegate.Combine générique, mais vous ne pourriez pas vraiment exprimer les types pertinents d'une manière significative.

Une chose qui est intéressant de noter est que la conversion covariant est une conversion de référence - en ce qui précède, multi1 et stringFactory se réfèrent au même objet: il est pas la même chose que l'écriture

Func<object> multi1 = new Func<object>(stringFactory); 

(A ce point, la ligne suivante s'exécutera sans exception.) Au moment de l'exécution, la BCL doit vraiment composer avec un Func<string> et un Func<object> combiné; il n'a pas d'autres informations pour continuer.

C'est méchant, et j'espère sérieusement qu'il sera corrigé d'une manière ou d'une autre. J'alerterai Mads et Eric à cette question afin que nous puissions obtenir des commentaires plus éclairés.

+0

Cool, je suis arrivé à pratiquement le même code d'exemple en bricolant avec elle dans le train de retour. Serait très intéressant d'entendre les détails gnarly. –

1

J'ai juste dû résoudre ce problème dans mon application. J'ai fait ce qui suit:

// variant delegate with variant event args 
MyEventHandler<<in T>(object sender, IMyEventArgs<T> a) 

// class implementing variant interface 
class FiresEvents<T> : IFiresEvents<T> 
{ 
    // list instead of event 
    private readonly List<MyEventHandler<T>> happened = new List<MyEventHandler<T>>(); 

    // custom event implementation 
    public event MyEventHandler<T> Happened 
    { 
     add 
     { 
      happened.Add(value); 
     } 
     remove 
     { 
      happened.Remove(value); 
     } 
    } 

    public void Foo() 
    { 
     happened.ForEach(x => x.Invoke(this, new MyEventArgs<T>(t)); 
    } 
} 

Je ne sais pas s'il existe des différences importantes par rapport aux événements multi-acteurs habituels. Pour autant que je l'ai utilisé, cela fonctionne ...

Par ailleurs: I never liked the events in C#. Je ne comprends pas pourquoi il y a une caractéristique de langue, quand elle ne fournit aucun avantage.

+1

La principale différence est que les délégués de multidiffusion sont immuables. Il est donc facile d'ajouter ou de supprimer des gestionnaires lors de l'appel. Dans votre implémentation, la liste peut muter lors de l'invocation d'événement avec des résultats imprévisibles. –

+0

@ SørenBoisen: très bon point. Merci pour celui-ci. Je pourrais utiliser un 'ConcurrentBag ' au lieu de la liste. –

+1

Une autre option utilise ImmutableArray ou ImmutableList à l'adresse http://blogs.msdn.com/b/dotnet/archive/2013/09/25/immutable-collections-ready-for-prime-time.aspx. Cela permettrait d'optimiser la répartition par rapport à ajouter/supprimer, mais plus important encore, ils sont disponibles dans les projets PCL :-) –

Questions connexes