2009-07-13 24 views
25

Durante le indagini su this question mi sono incuriosito su come le nuove funzionalità di covarianza/controvarianza in C# 4.0 influiranno su di esso.Event and delegate controvariance in .NET 4.0 e C# 4.0

In Beta 1, C# sembra in disaccordo con il CLR. Eseguire in C# 3.0, se si ha:

public event EventHandler<ClickEventArgs> Click; 

... e poi altrove si ha:

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

... il compilatore sarebbe barf perché sono tipi delegato incompatibili. Ma in C# 4.0, si compila bene, perché in CLR 4.0 il parametro type è ora contrassegnato come in, quindi è controverso, e quindi il compilatore presuppone che il delegato multicast += funzionerà.

Ecco la mia prova:

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

Ma anche se si compila, non funziona in fase di esecuzione (ArgumentException: I delegati devono essere dello stesso tipo).

Va bene se si aggiunge solo uno dei due tipi di delegato. Ma la combinazione di due diversi tipi in un multicast provoca l'eccezione quando viene aggiunto il secondo.

Immagino che questo sia un bug nel CLR in beta 1 (il comportamento del compilatore sembra sperare bene).

Aggiornamento per Release Candidate:

Il codice di cui sopra non è più compila. Deve essere che la controvarianza di TEventArgs nel tipo di delegato EventHandler<TEventArgs> è stata ripristinata, quindi ora che il delegato ha la stessa definizione di .NET 3.5.

Cioè, la versione beta ho guardato deve aver avuto:

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

Ora è di nuovo a:

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

Ma il parametro Action<T> delegato T è ancora controvariante:

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

Lo stesso vale per di T essendo covariante.

Questo compromesso ha molto senso, a patto che si presuma che l'uso principale dei delegati multicast sia nel contesto degli eventi. Personalmente ho scoperto che non uso mai delegati multicast, tranne come eventi.

Quindi suppongo che gli standard di codifica C# possano ora adottare una nuova regola: non creare delegati multicast da più tipi di delegati correlati tramite covarianza/controvarianza. E se non sai cosa significa, basta evitare di usare Action per gli eventi sul lato sicuro.

Naturalmente, questa conclusione ha implicazioni per the original question that this one grew from ...

+1

Quanto vuoi interessante è la domanda? –

+0

Sono sorpreso che questo buco sia sfuggito all'attenzione del team C#, dovrebbe essere una delle prime cose che avrebbero testato dopo aver introdotto la varianza per i delegati generici vero? Mostra anche C# 5 (la versione clr è la stessa). – nawfal

risposta

0

stanno producendo l'ArgumentException da entrambi?Se l'eccezione viene lanciata solo dal nuovo gestore, allora penserei che sia compatibile con le versioni precedenti.

BTW, penso che i tuoi commenti siano confusi. In C# 3.0:

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

non avrebbe corso. C# 4.0

+0

Ho rimosso tutti i riferimenti al controllo delle versioni nella domanda, in quanto sembra aver causato solo confusione. I commenti su vecchi e nuovi relativi al problema a cui stavo originariamente pensando, da cui ho diviso questa domanda. Questa domanda non ha nulla a che fare con la retrocompatibilità di per sé.Riguarda il compilatore presupponendo che un delegato multicast possa legarsi a metodi di tipi controvarianti, il che risulta non essere vero in fase di runtime. –

9

Molto interessante. Non è necessario utilizzare gli eventi per vedere questo evento, e infatti trovo più semplice usare semplici delegati.

Considerare Func<string> e Func<object>. In C# 4.0 è possibile convertire implicitamente un Func<string> in Func<object> perché è sempre possibile utilizzare un riferimento di stringa come riferimento di un oggetto. Tuttavia, le cose vanno male quando si tenta di combinarle. Ecco un programma breve ma completo che dimostra il problema in due modi diversi:

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

Questo compila bene, ma entrambe le Combine chiamate (nascosto dal + = zucchero sintattico) generare eccezioni. (Commenta il primo a vedere il secondo.)

Questo è sicuramente un problema, anche se non sono esattamente sicuro di quale dovrebbe essere la soluzione. È possibile che in fase di esecuzione il codice delegato dovrà elaborare il tipo più appropriato da utilizzare in base ai tipi di delegati coinvolti. È un po 'brutto. Sarebbe molto carino avere una chiamata generica Delegate.Combine, ma non potresti davvero esprimere i tipi rilevanti in modo significativo.

Una cosa che è degno di nota è che la conversione covariante è una conversione di riferimento - in quanto sopra, multi1 e stringFactory si riferiscono allo stesso oggetto: è non lo stesso che scrivere

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

(A quel punto, la seguente riga verrà eseguita senza eccezioni.) In fase di esecuzione, il BCL deve effettivamente gestire uno Func<string> e un Func<object> combinato; non ha altre informazioni per andare avanti.

È brutto, e spero seriamente che venga risolto in qualche modo. Avvertirò Mads ed Eric a questa domanda in modo che possiamo ottenere un commento più informato.

+0

Fresco, sono arrivato praticamente allo stesso codice di esempio per armeggiare con esso sul treno di casa. Sarebbe molto interessante sentire i dettagli netti. –

1

Ho appena dovuto risolvere questo problema nella mia applicazione. Ho fatto quanto segue:

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

Non so se ci sono differenze rilevanti rispetto agli eventi multi-cast regolari. Per quanto ho usato, funziona ...

A proposito: I never liked the events in C#. Non capisco perché ci sia una funzionalità linguistica, quando non fornisce alcun vantaggio.

+1

La differenza principale è che i delegati multicast sono immutabili, quindi è sicuro aggiungere thread ai gestori durante la chiamata. Nella tua implementazione, l'elenco potrebbe mutare durante l'invocazione di un evento con risultati imprevedibili. –

+0

@ SørenBoisen: ottimo punto. Grazie per questo. Potrei usare un 'ConcurrentBag ' invece della lista. –

+1

Un'altra opzione sta utilizzando ImmutableArray o ImmutableList da http://blogs.msdn.com/b/dotnet/archive/2013/09/25/immutable-collections-ready-for-prime-time.aspx. Ciò ottimizzerebbe per la spedizione vs add/remove, ma soprattutto, sono disponibili nei progetti PCL :-) –