2009-07-13 2 views
25

Bei der Untersuchung von this question wurde ich neugierig, wie die neuen Kovarianz-/Kontravarianzfunktionen in C# 4.0 sich darauf auswirken.Kontravarianz von Ereignissen und Delegaten in .NET 4.0 und C# 4.0

In Beta 1 scheint C# mit der CLR nicht übereinzustimmen. Zurück in C# 3.0, wenn Sie hatte:

public event EventHandler<ClickEventArgs> Click; 

... und dann an anderer Stelle Sie hatte:

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

... der Compiler würde kotzen, weil sie unvereinbar Delegattypen sind. Aber in C# 4.0 kompiliert es gut, denn in CLR 4.0 ist der Typparameter jetzt als in markiert, also ist es kontravariant, und so nimmt der Compiler an, dass der Multicastdelegate += funktioniert.

Hier ist mein 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"); 
    } 
} 

Aber obwohl es kompiliert, es nicht zur Laufzeit arbeiten (ArgumentException: Die Teilnehmer müssen vom gleichen Typ sein).

Es ist in Ordnung, wenn Sie nur einen der beiden Delegattypen hinzufügen. Aber die Kombination von zwei verschiedenen Typen in einem Multicast verursacht die Ausnahme, wenn der zweite hinzugefügt wird.

Ich denke, das ist ein Fehler in der CLR in Beta 1 (das Verhalten des Compilers sieht hoffentlich richtig aus).

Update für Release Candidate:

Der obige Code nicht mehr kompiliert. Es muss sein, dass die Kontravarianz von TEventArgs im EventHandler<TEventArgs> Delegattyp zurückgesetzt wurde, so dass dieser Delegat nun dieselbe Definition wie in .NET 3.5 hat.

Das heißt, die Beta-ich sah gehabt haben muss:

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

Jetzt ist es wieder da:

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

Aber der Action<T> Delegat Parameter T ist noch kontra:

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

Das gleiche gilt für 's T ist kovariant.

Dieser Kompromiss ist sehr sinnvoll, solange wir davon ausgehen, dass die primäre Verwendung von Multicast-Delegaten im Kontext von Ereignissen liegt. Ich persönlich habe festgestellt, dass ich Multicast-Delegierte niemals als Ereignisse verwende.

Also ich denke, C# -Coding-Standards können jetzt eine neue Regel übernehmen: nicht Multicast-Delegaten von mehreren Delegaten Typen über Kovarianz/Kontravarianz bezogen bilden. Und wenn Sie nicht wissen, was das bedeutet, vermeiden Sie einfach die Verwendung von Action für Ereignisse, um auf der sicheren Seite zu sein.

Natürlich hat diese Schlussfolgerung Auswirkungen auf the original question that this one grew from ...

+1

Though interessant wollen die Frage? –

+0

Ich bin überrascht, dass dieses Loch der Bekanntmachung des C# -Teams entgangen ist, sollte eines der ersten Dinge sein, die sie getestet hätten, nachdem sie die Varianz für generische Delegierte eingeführt haben, nicht wahr? C# 5 zeigt es auch (die clr-Version ist die gleiche). – nawfal

Antwort

0

Sind Sie das Argument von beiden bekommen?Wenn die Ausnahme nur durch den neuen Handler ausgelöst wird, würde ich denken, dass sie abwärtskompatibel ist.

BTW, ich denke, Sie haben Ihre Kommentare durcheinander gebracht. In C# 3.0:

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

würde nicht laufen haben. Das ist C# 4.0

+0

Ich habe alle Verweise auf Versionierung in der Frage entfernt, da es nur Verwirrung verursacht zu haben scheint. Die Kommentare über Neues und Altes bezogen sich auf das Thema, über das ich ursprünglich nachgedacht habe und aus dem ich diese Frage heraustrenne. Diese Frage hat mit der Rückwärtskompatibilität an sich nichts zu tun.Es geht um den Compiler unter der Annahme, dass ein Multicast-Delegat an Methoden kontravarianter Typen binden kann, die sich dann zur Laufzeit als nicht zutreffend herausstellen. –

9

Sehr interessant. Sie müssen keine Ereignisse verwenden, um dies zu sehen, und tatsächlich finde ich es einfacher, einfache Delegaten zu verwenden.

Betrachten Sie Func<string> und Func<object>. In C# 4.0 können Sie implizit eine Func<string> in Func<object> konvertieren, da Sie immer eine String-Referenz als Objektreferenz verwenden können. Wenn Sie versuchen, sie zu kombinieren, gehen die Dinge jedoch schief. Hier ist ein kurzes, aber vollständiges Programm, das Problem auf zwei verschiedene Arten zeigen:

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

Dies kompiliert gut, aber beide der Combine Anrufe (verdeckt durch den + = syntaktischen Zucker) Ausnahmen werfen. (Kommentieren Sie den ersten, um den zweiten zu sehen.)

Dies ist definitiv ein Problem, obwohl ich nicht genau sicher bin, was die Lösung sein sollte. Es ist möglich, dass zur Ausführungszeit der Delegiertencode den am besten geeigneten Typ basierend auf den beteiligten Delegattypen ausarbeiten muss. Das ist ein bisschen eklig. Es wäre sehr nett, einen generischen Delegate.Combine Aufruf zu haben, aber Sie konnten die relevanten Typen nicht wirklich sinnvoll ausdrücken.

Eine Sache, die erwähnenswert ist, dass die kovariante Umwandlung ist eine Referenz-Konvertierung - in den oben genannten, multi1 und stringFactory beziehen sich auf das gleiche Objekt: es ist nicht die gleiche wie

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

(Darauf Punkt, die folgende Zeile wird ohne Ausnahme ausgeführt.) Zur Ausführungszeit muss die BCL wirklich mit einer Func<string> und einer Func<object> kombiniert werden; Es hat keine anderen Informationen zu machen.

Es ist böse, und ich hoffe ernsthaft, dass es in irgendeiner Weise behoben wird. Ich werde Mads und Eric auf diese Frage aufmerksam machen, damit wir mehr informierte Kommentare bekommen können.

+0

Cool, ich kam praktisch zum selben Beispielcode an, als ich mit dem Zug nach Hause bastelte. Wäre sehr interessant, die knorrigen Details zu hören. –

1

Ich musste das nur in meiner Anwendung beheben. Ich habe folgendes getan:

Ich weiß nicht, ob es relevante Unterschiede zu regulären Multicast-Events gibt. Soweit ich es benutzt habe, funktioniert es ...

Übrigens: I never liked the events in C#. Ich verstehe nicht, warum es ein Sprachfeature gibt, wenn es keine Vorteile bietet.

+1

Der Hauptunterschied besteht darin, dass Multicastdelegaten unveränderlich sind. Daher ist das Hinzufügen/Entfernen von Handlern während des Aufrufs Thread-sicher. In Ihrer Implementierung könnte die Liste während des Ereignisaufrufs mit unvorhersehbaren Ergebnissen mutieren. –

+0

@ SørenBoisen: sehr guter Punkt. Danke für diesen. Ich könnte einen 'ConcurrentBag ' anstelle der Liste verwenden. –

+1

Eine andere Option verwendet ImmutableArray oder ImmutableList von http://blogs.msdn.com/b/dotnet/archive/2013/09/25/immutable-collections-ready-for-prime-time.aspx. Das würde für Versand vs hinzufügen/entfernen optimieren, aber noch wichtiger, sie sind in PCL-Projekten verfügbar :-) –