2010-02-21 18 views
34

comportamenti strani con la co- e controvarianza sostegno C# 4.0:co- e controvarianza bug in .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); 
    } 
} 

risultati di con ArgumentException: Delegates must be of the same type.

Strano, non è vero? Perché Delegate.Combine() (che è stato chiamato quando si esegue l'operazione += sui delegati) non supporta la co- e la controvarianza in fase di esecuzione?

Inoltre, ho trovato che il tipo di delegato BCL System.EventHandler<TEventArgs> non ha annotazioni controvarianti sul suo parametro generico TEventArgs! Perché? È perfettamente legale, tipo TEventArgs utilizzato solo nella posizione di input. Forse non c'è un'annotazione controvariante perché nasconde bene il bug con lo Delegate.Combine()? ;)

p.s. Tutto ciò influenza il VS2010 RC e le versioni successive.

+2

___ * ___ * Ыome – Nifle

+0

dispiace locale della tastiera, ho cambiato posto sbagliato ... – ControlFlow

+0

@ControlFlow - Vedi http://stackoverflow.com/questions/1120688/event-and-delegate-contravariance-in-net-4-0-and-c-4-0 - Ho notato questo problema nella prima beta di VS2010. A quel punto, 'EventHandler ' era controvariante w.r.t. 'TEventArgs'. Ma da allora è stato cambiato di nuovo come hai trovato. –

risposta

37

Per farla breve: la combinazione dei delegati è tutti incasinati rispetto alla varianza. L'abbiamo scoperto tardi nel ciclo. Stiamo lavorando con il team CLR per vedere se possiamo inventare un modo per far sì che tutti gli scenari comuni funzionino senza compromettere la compatibilità con le versioni precedenti, e così via, ma qualsiasi cosa creeremo non sarà probabilmente nella versione 4.0. Speriamo di risolvere tutto in un pacchetto di servizi. Mi scuso per l'inconveniente.

+19

Eric le tue risposte e soprattutto la loro candidatura sono molto apprezzato dal momento che pochissimi di noi (presumibilmente) hanno qualche idea di ciò che accade internamente a Microsoft con .NET e le tue informazioni spesso rendono gli interni molto meno di una scatola nera a me stesso e al resto della comunità qui (ovvio con il tuo 28K rep). –

+5

@ Chris: grazie! Sto tutto per rendere l'intero processo più trasparente. –

+1

È successo qualcosa in quest'area da .net 4.0? 'EventHandler ' non è ancora contrassegnato come contro-variante nella documentazione di anteprima 4.5. – CodesInChaos

6

Covarianza e controvarianza specifica la relazione di ereditarietà tra tipi generici. Quando si ha controvarianza di covarianza &, le classi G<A> e G<B> possono trovarsi in una relazione di ereditarietà a seconda di cosa sia A e B. Puoi trarne vantaggio quando chiami metodi generici.

Tuttavia, il metodo Delegate.Combine non è generica e la documentation clearly says quando verrà generata l'eccezione:

ArgumentException - Sia un e b non sono null di riferimento (Nothing in Visual Basic), e a e b non sono istanze dello stesso tipo di delegato.

Ora, Action<object> e Action<string> sono certamente le istanze di un diverso tipo delegato (anche se correlato via relazione di ereditarietà), quindi in base alla documentazione, è un'eccezione. Sembra ragionevole che il metodo Delegate.Combine possa supportare questo scenario, ma questa è solo una possibile proposta (ovviamente questo non era necessario fino ad ora, perché non è possibile dichiarare delegati ereditati, quindi prima di co/contro-varianza, nessun delegato aveva alcuna relazione di ereditarietà) .

+0

Sì, ho ragione riguardo la documentazione e il comportamento di Delegate.Combine() '... Ma riguardo il supporto di questo scenario : perché stai parlando dell'ereditarietà dei delegati? Per il mio scenario, i parametri generici dovrebbero avere una relazione di ereditarietà, non i tipi delegati stessi. Ad esempio, iscriversi a un 'EventHandler con un'istanza di delegato' EventHandler '. – ControlFlow

+0

Più precisamente, avrei dovuto parlare di sottotitoli (cioè, quando puoi trattare il valore di tipo 'A' come valore di tipo' B'). Covarianza e controvarianza definiscono la relazione di sottotipizzazione tra delegati generici (in base a una relazione di sottotipizzazione/ereditarietà tra i loro parametri di tipo). È possibile convertire qualsiasi sottotipo (ereditato) in un supertipo (base o interfaccia) e la covarianza tra i delegati utilizza la stessa regola di conversione. Tuttavia, ciò non si estende a 'Delegate.Combine' che probabilmente usa internals del delegato dato come argomento. –

+0

Questo è stato documentato ancor prima che la varianza generica entrasse in gioco dove la firma di 'Delegate.Combine (Delegate, Delegate)' prende due tipi 'Delegate', quindi una tale eccezione era valida per documentare. Quella documentazione è fuorviante nel contesto della varianza qui considerando che questo accade solo quando 'a' ha già un delegato di tipo diverso. – nawfal

1

Una difficoltà con la combinazione di delegati è che, a meno che non si specifichi quale operando si suppone sia il sottotipo e che è supertipo, non è chiaro quale sia il tipo di risultato. È possibile scrivere una factory wrapper che convertirà qualsiasi delegato con un numero specificato di argomenti e pattern di byval/byref in un supertipo, ma chiamare più volte un factory di questo tipo con lo stesso delegato produrrà wrapper diversi (questo potrebbe causare il caos con cancellazione dell'evento). In alternativa, è possibile creare una versione di Delegato.Combinare che costringerebbe il delegato di destra al tipo del delegato di sinistra (come bonus, il ritorno non dovrebbe essere typecast), ma si dovrebbe scrivere una versione speciale di delegate.remove per affrontarlo.

0

Questa soluzione è stata originariamente pubblicata da cdhowie per la mia domanda: Delegate conversion breaks equality and unables to disconnect from event ma sembra risolvere il problema della covarianza e della contravarianza nel contesto dei delegati multicast.

è necessario prima un metodo di supporto:

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

Quando si dispone di un delegato:

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

Si può usare combinando delegati semplicemente convertendo un delegato che fare tipo desiderato:

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

Supporta anche la disconnessione dall'evento allo stesso modo, utilizzandoMetodo. Diversamente dall'utilizzo di alcuni elenchi personalizzati di delegati, questa soluzione fornisce una sicurezza dei thread pronta all'uso.

completo del codice con alcuni campioni si possono trovare qui:? http://ideone.com/O6YcdI