2010-02-02 16 views

risposta

234

La domanda è "qual è la differenza tra covarianza e controvarianza?"

Covarianza e controvarianza sono proprietà di una funzione di mappatura che associa un membro di un set a un altro. Più specificamente, una mappatura può essere covariante o controvariante rispetto a una relazione su quel set.

Considerare i seguenti due sottoinsiemi dell'insieme di tutti i tipi di C#.Primo:

{ Animal, 
    Tiger, 
    Fruit, 
    Banana }. 

E in secondo luogo, questo insieme chiaramente correlati:

{ IEnumerable<Animal>, 
    IEnumerable<Tiger>, 
    IEnumerable<Fruit>, 
    IEnumerable<Banana> } 

C'è un mappatura operazione dal primo set al secondo set. Vale a dire, per ogni T del primo set, il corrispondente al tipo nel secondo set è IEnumerable<T>. Oppure, in forma abbreviata, la mappatura è T → IE<T>. Si noti che questa è una "freccia sottile".

Con me finora?

Consideriamo ora una relazione . C'è una relazione di compatibilità dell'assegnazione tra coppie di tipi nella prima serie. Un valore di tipo Tiger può essere assegnato a una variabile di tipo Animal, quindi si dice che questi tipi siano "compatibili di assegnazione". Scriviamo "un valore di tipo X può essere assegnato a una variabile di tipo Y" in un formato più breve: X ⇒ Y. Si noti che questa è una "freccia grossa".

Così nel nostro primo sottoinsieme, qui ci sono tutti i rapporti di compatibilità assegnazione:

Tiger ⇒ Tiger 
Tiger ⇒ Animal 
Animal ⇒ Animal 
Banana ⇒ Banana 
Banana ⇒ Fruit 
Fruit ⇒ Fruit 

in C# 4, che supporta la compatibilità assegnazione covariante di alcune interfacce, v'è un rapporto di compatibilità incarico tra coppie di tipi nella secondo set:

IE<Tiger> ⇒ IE<Tiger> 
IE<Tiger> ⇒ IE<Animal> 
IE<Animal> ⇒ IE<Animal> 
IE<Banana> ⇒ IE<Banana> 
IE<Banana> ⇒ IE<Fruit> 
IE<Fruit> ⇒ IE<Fruit> 

Si noti che la mappatura T → IE<T>preserva l'esistenza e la direzione della compatibilità assegnazione. Cioè, se X ⇒ Y, allora è anche vero che IE<X> ⇒ IE<Y>.

Se abbiamo due elementi su entrambi i lati di una freccia grassa, possiamo sostituire entrambi i lati con qualcosa sul lato destro di una freccia sottile corrispondente.

Una mappatura che ha questa proprietà rispetto ad una particolare relazione è chiamata "mappatura covariante". Questo dovrebbe avere senso: una sequenza di Tigri può essere usata dove è necessaria una sequenza di Animali, ma il contrario non è vero. Una sequenza di animali non può essere necessariamente utilizzata dove è necessaria una sequenza di Tigri.

Questa è covarianza. Ora considerare questo sottoinsieme dell'insieme di tutti i tipi:

{ IComparable<Tiger>, 
    IComparable<Animal>, 
    IComparable<Fruit>, 
    IComparable<Banana> } 

ora abbiamo la mappatura dal primo set per il terzo set T → IC<T>.

In C# 4:

IC<Tiger> ⇒ IC<Tiger> 
IC<Animal> ⇒ IC<Tiger>  Backwards! 
IC<Animal> ⇒ IC<Animal> 
IC<Banana> ⇒ IC<Banana> 
IC<Fruit> ⇒ IC<Banana>  Backwards! 
IC<Fruit> ⇒ IC<Fruit> 

Cioè, la mappatura T → IC<T> ha conservato l'esistenza, ma invertito la direzione della compatibilità assegnazione. Cioè, se X ⇒ Y, quindi IC<X> ⇐ IC<Y>.

Una mappatura che conserve ma inverte una relazione è chiamato controvariante mappatura.

Anche in questo caso, questo deve essere chiaramente corretto. Un dispositivo che può confrontare due animali può anche confrontare due tigri, ma un dispositivo che può confrontare due tigri non può necessariamente confrontare due animali.

Quindi questa è la differenza tra covarianza e controvarianza in C# 4. Covariance conserva la direzione di assegnabilità. Contravarianza inverte it.

+2

Per qualcuno come me, sarebbe stato meglio aggiungere esempi che mostrassero ciò che NON è covariante e ciò che NON è controvariante e che NON è entrambi. – bjan

+0

Grazie Eric, sembra lo stesso di Java e . – Bargitta

+0

@Bargitta: è molto simile. La differenza è che C# usa * varianza del sito definita * e Java usa * call site variance *. Quindi il modo in cui le cose variano è lo stesso, ma dove lo sviluppatore dice "Ho bisogno che questo sia una variante" è diverso. Per inciso, la funzione in entrambe le lingue è stata in parte progettata dalla stessa persona! –

94

Probabilmente è più semplice fornire esempi: è certamente il modo in cui li ricordo.

covarianza

esempi canonici: IEnumerable<out T>, Func<out T>

È possibile convertire da IEnumerable<string> a IEnumerable<object>, o Func<string>-Func<object>. I valori vengono solo da questi oggetti.

Funziona perché se si stanno solo prendendo i valori dall'API e si restituirà qualcosa di specifico (come string), è possibile considerare tale valore restituito come un tipo più generale (come object).

controvarianza

esempi canonici: IComparer<in T>, Action<in T>

È possibile convertire da IComparer<object> a IComparer<string>, o Action<object> a Action<string>; i valori vanno solo a in questi oggetti.

Questa volta funziona perché se l'API si aspetta qualcosa di generale (come object), è possibile fornire qualcosa di più specifico (come string).

Più in generale

Se è presente un'interfaccia IFoo<T> può essere covariante T (cioè dichiara come IFoo<out T> se T viene utilizzato solo in una posizione di uscita (ad esempio, un tipo di ritorno) nell'interfaccia. Si . può essere contravariant in T (cioè IFoo<in T>) se T viene utilizzato solo in posizione di ingresso (ad esempio un tipo di parametro)

ottiene potenzialmente confusione perché "posizione di uscita" non è così semplice come sembra - un parametro di tipo Action<T> utilizza ancora T in una posizione di uscita: la controversione di Action<T> lo trasforma, se capisci cosa intendo. È un "risultato" in quanto i valori possono passare dall'implementazione del metodo al codice, proprio come può fare un valore di ritorno. Di solito questo genere di cose non viene fuori, per fortuna :)

+1

Per qualcuno come me, sarebbe stato meglio aggiungere degli esempi che mostrano ciò che NON è covariante e ciò che NON è controvariante e che NON è entrambi. – bjan

+1

@Jon Skeet Bel esempio, non capisco * * un parametro di tipo 'Azione ' sta ancora usando solo 'T' in una posizione di uscita" *. 'Azione ' tipo restituito è nullo, come può utilizzare 'T' come output? O è questo che significa, perché non restituisce nulla che puoi vedere che non può mai violare la regola? –

+1

Per il mio sé futuro, che sta tornando a questa eccellente risposta ** di nuovo ** per reimparare la differenza, questa è la riga che vuoi: _ "[Covariance] funziona perché se stai prendendo valori solo dall'API, e restituirà qualcosa di specifico (come una stringa), puoi trattare il valore restituito come un tipo più generale (come l'oggetto). "_ –

13

Spero che il mio post aiuti a ottenere una visione indipendente dall'approccio linguistico dell'argomento.

Per i nostri corsi interni ho lavorato con lo splendido libro "Smalltalk, Objects and Design (Chamond Liu)" e ho riformulato gli esempi seguenti.

Che cosa significa "coerenza"? L'idea è di progettare gerarchie di tipi sicuri, con tipi altamente sostituibili. La chiave per ottenere questa coerenza è la conformità di tipo secondario, se si lavora in un linguaggio tipizzato staticamente. (Discuteremo il principio di sostituzione di Liskov (LSP) su un alto livello qui.)

Esempi pratici (pseudo codice/non valido in C#):

  • covarianza: Supponiamo uccelli che depongono le uova “ coerentemente "con la tipizzazione statica: se il tipo Bird depone un uovo, il sottotipo di Bird non sarebbe un sottotipo di Egg? Per esempio. il tipo Duck depone un DuckEgg, quindi viene data la consistenza. Perché è coerente? Perché in una tale espressione: Egg anEgg = aBird.Lay(); il riferimento aBird potrebbe essere legalmente sostituito da un Bird o da un'istanza di Duck. Diciamo che il tipo di ritorno è covariante al tipo, in cui è definito Lay(). L'override di un sottotipo può restituire un tipo più specializzato. => "Consegnano di più."

  • Controvarianza: Supponiamo che i pianisti possano suonare "coerentemente" con la tipizzazione statica: se un pianista suona il pianoforte, sarebbe in grado di suonare un GrandPiano? Non preferirebbe un virtuoso interpretare un GrandPiano? (Attenzione, c'è una svolta!) Questo è incoerente! Perché in una tale espressione: aPiano.Play(aPianist); aPiano non può essere legalmente sostituito da un Piano o da un'istanza GrandPiano! Un GrandPiano può essere suonato solo da un virtuoso, i pianisti sono troppo generici! I GrandPianos devono essere riproducibili con tipi più generali, quindi il gioco è coerente. Diciamo che il tipo di parametro è controverso rispetto al tipo in cui è definito Play(). L'override di un sottotipo può accettare un tipo più generalizzato. => “Essi richiedono meno”.

Torna a C#:
causa C# è fondamentalmente un linguaggio a tipizzazione statica, i "luoghi" di interfaccia di un tipo che dovrebbe essere co-o controvariante (ad esempio i parametri e ritorno tipi), devono essere contrassegnati esplicitamente per garantire un utilizzo/sviluppo coerente di quel tipo, per far funzionare l'LSP. In linguaggi tipizzati dinamicamente, la coerenza LSP non è in genere un problema, in altre parole è possibile eliminare completamente il "markup" co- e controvariante su interfacce e delegati .Net, se si è utilizzato il tipo dinamico solo nei tipi. - Ma questa non è la soluzione migliore in C# (non dovresti usare la dinamica nelle interfacce pubbliche).

Torna alla teoria:
La conformità descritta (tipi di ritorno covarianti/tipi di parametri controvarianti) è l'ideale teorico (supportato dalle lingue Emerald e POOL-1). Alcune lingue oop (ad es.Eiffel) ha deciso di applicare un altro tipo di coerenza, esp. anche i tipi di parametri covarianti, perché descrive meglio la realtà rispetto all'ideale teorico. Nelle lingue tipizzate staticamente, la consistenza desiderata deve spesso essere ottenuta applicando modelli di progettazione come "doppio dispacciamento" e "visitatore". Altre lingue forniscono i cosiddetti metodi di "invio multiplo" o multi (in pratica selezionando gli overload di funzioni al tempo di esecuzione, ad esempio con CLOS) o ottenendo l'effetto desiderato utilizzando la digitazione dinamica.

+0

Tu dici * Un override di un sottotipo può restituire un tipo più specializzato *. Ma questo è completamente falso. Se 'Bird' definisce' public abstract BirdEgg Lay(); ', quindi' Duck: Bird' * MUST * implementa 'public override BirdEgg Lay() {}' Quindi la tua affermazione che 'BirdEgg anEgg = aBird.Lay();' ha qualsiasi tipo di varianza è semplicemente falso. Essendo la premessa del punto della spiegazione, l'intero punto è ormai finito. Vorresti * invece * affermare che la covarianza esiste all'interno dell'implementazione in cui un DuckEgg viene implicitamente inserito nel tipo di uscita/ritorno di BirdEgg? In ogni caso, ti prego di cancellare la mia confusione. – Suamere

+1

Per farla breve: hai ragione! Dispiace per la confusione. 'DuckEgg Lay()' non è un override valido per 'Egg Lay()' _in C# _, e questo è il punto cruciale. C# non supporta i tipi di ritorno covarianti, ma anche Java e C++. Ho piuttosto descritto l'ideale teorico usando una sintassi simile a C#. In C# è necessario consentire a Bird e Duck di implementare un'interfaccia comune, in cui Lay è definito per avere un ritorno covariante (cioè il tipo out-specificazione), quindi le cose vanno bene insieme! – Nico

1

Il delegato del convertitore mi aiuta a capire la differenza.

delegate TOutput Converter<in TInput, out TOutput>(TInput input); 

TOutput rappresenta covarianza cui un metodo restituisce un tipo più specifico.

TInput rappresenta controvarianza cui un metodo viene passato un tipo meno specifico.

public class Dog { public string Name { get; set; } } 
public class Poodle : Dog { public void DoBackflip(){ System.Console.WriteLine("2nd smartest breed - woof!"); } } 

public static Poodle ConvertDogToPoodle(Dog dog) 
{ 
    return new Poodle() { Name = dog.Name }; 
} 

List<Dog> dogs = new List<Dog>() { new Dog { Name = "Truffles" }, new Dog { Name = "Fuzzball" } }; 
List<Poodle> poodles = dogs.ConvertAll(new Converter<Dog, Poodle>(ConvertDogToPoodle)); 
poodles[0].DoBackflip();