Differenza tra covarianza e contro-varianza


Risposte:


266

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

Covarianza e contraddizione 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 contraddittoria rispetto a una relazione su quell'insieme.

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

{ Animal, 
  Tiger, 
  Fruit, 
  Banana }.

E in secondo luogo, questo set chiaramente correlato:

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

Esiste un'operazione di mappatura dal primo al secondo set. Cioè, per ogni T nel primo set, il tipo corrispondente nel secondo set è IEnumerable<T>. O, in breve, la mappatura èT → IE<T> . Si noti che questa è una "freccia sottile".

Con me finora?

Ora consideriamo una relazione . Esiste una relazione di compatibilità delle assegnazioni tra coppie di tipi nel primo set. Un valore di tipo Tigerpuò essere assegnato a una variabile di tipo Animal, quindi si dice che questi tipi siano "compatibili con l'assegnazione". Scriviamo "un valore di tipo Xpuò essere assegnato a una variabile di tipo Y" in una forma più breve:X ⇒ Y . Si noti che questa è una "freccia grassa".

Quindi nel nostro primo sottoinsieme, ecco tutte le relazioni di compatibilità delle assegnazioni:

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

In C # 4, che supporta la compatibilità di assegnazione covariante di alcune interfacce, esiste una relazione di compatibilità di assegnazione tra coppie di tipi nel 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> conserva l'esistenza e la direzione della compatibilità delle assegnazioni . Cioè, se X ⇒ Y, allora è anche veroIE<X> ⇒ IE<Y> .

Se abbiamo due cose su entrambi i lati di una freccia grassa, allora 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 non è vero il contrario. Una sequenza di animali non può essere necessariamente utilizzata laddove è necessaria una sequenza di Tigri.

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

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

ora abbiamo il mapping dal primo al 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 preservato l'esistenza ma ha invertito la direzione della compatibilità delle assegnazioni. Cioè, se X ⇒ Y, alloraIC<X> ⇐ IC<Y> .

Una mappatura che preserva ma inverte una relazione è chiamata mappatura contraddittoria .

Ancora una volta, questo dovrebbe essere chiaramente corretto. Un dispositivo in grado di confrontare due animali può anche confrontare due tigri, ma un dispositivo in grado di confrontare due tigri non può necessariamente confrontare due animali.

Quindi questa è la differenza tra covarianza e contravarianza in C # 4. La covarianza mantiene la direzione dell'assegnabilità. La contraddizione lo inverte .


4
Per qualcuno come me, sarebbe meglio aggiungere esempi che mostrino cosa NON è covariante e cosa NON è contraddittorio e cosa NON è entrambi.
bjan,

2
@Bargitta: è molto simile. La differenza è che C # utilizza la varianza del sito definita e Java utilizza la varianza del sito di chiamata . Quindi il modo in cui le cose variano è lo stesso, ma dove lo sviluppatore dice "Ho bisogno che questa sia una variante" è diverso. Per inciso, la funzione in entrambe le lingue è stata in parte progettata dalla stessa persona!
Eric Lippert,

2
@AshishNegi: leggi la freccia come "può essere usato come". "Una cosa che può confrontare gli animali può essere usata come una cosa che può confrontare le tigri". Ha senso adesso?
Eric Lippert,

1
@AshishNegi: No, non è vero. IEnumerable è covariante perché T appare solo nei ritorni dei metodi di IEnumerable. E IComparable è contraddittorio perché T appare solo come parametri formali dei metodi di IComparable .
Eric Lippert,

2
@AshishNegi: vuoi pensare alle ragioni logiche che stanno alla base di queste relazioni. Perché possiamo convertire IEnumerable<Tiger>in IEnumerable<Animal>modo sicuro? Perché non c'è modo di inserire una giraffa IEnumerable<Animal>. Perché possiamo convertire un IComparable<Animal>in IComparable<Tiger>? Perché non c'è modo di estrarre una giraffa da un IComparable<Animal>. Ha senso?
Eric Lippert,

111

Probabilmente è più facile fare esempi - è certamente così che li ricordo.

covarianza

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

Puoi convertire da IEnumerable<string>a IEnumerable<object>, o Func<string>a Func<object>. I valori escono solo da questi oggetti.

Funziona perché se stai estraendo valori dall'API e restituirà qualcosa di specifico (come string), puoi considerare quel valore restituito come un tipo più generale (come object).

controvarianza

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

Puoi convertire da IComparer<object>a IComparer<string>, oppure Action<object>a Action<string>; i valori vanno solo in questi oggetti.

Questa volta funziona perché se l'API si aspetta qualcosa di generale (come object) puoi dargli qualcosa di più specifico (come string).

Più generalmente

Se hai un'interfaccia IFoo<T>, può essere covariante T(cioè dichiararla come IFoo<out T>se Tfosse usata solo in una posizione di uscita (es. Un tipo di ritorno) all'interno dell'interfaccia. Può essere contraddittoria in T(cioè IFoo<in T>) seT è usato solo in una posizione di ingresso ( ad es. un tipo di parametro).

Diventa potenzialmente confuso perché "posizione di uscita" non è così semplice come sembra - un parametro di tipo Action<T>sta ancora usando solo Tin una posizione di uscita - la contraddizione di Action<T>gira intorno, se capisci cosa intendo. È un "output" dal fatto che i valori possono passare dalla implementazione del metodo verso codice del chiamante, proprio come una lattina valore di ritorno. Di solito questo genere di cose non viene fuori, per fortuna :)


1
Per qualcuno come me, sarebbe meglio aggiungere esempi che mostrino cosa NON è covariante e cosa NON è contraddittorio e cosa NON è entrambi.
bjan,

1
@Jon Skeet Un bell'esempio, non capisco solo "un parametro di tipo Action<T>sta ancora usando solo Tin una posizione di output" . Action<T>il tipo restituito è nullo, come può essere utilizzato Tcome output? O è quello che significa, perché non restituisce nulla che si possa vedere che non può mai violare la regola?
Alexander Derck,

2
Per il mio sé futuro, che sta tornando a questa risposta eccellente ancora una volta di imparare di nuovo la differenza, questa è la linea che si desidera: "[Covarianza] funziona perché se si sta solo prendendo valori fuori l'API, e sta andando a restituire qualcosa specifico (come stringa), puoi considerare quel valore restituito come un tipo più generale (come oggetto). "
Matt Klein,

La parte più confusa di tutto ciò è che per covarianza o contraddizione, se si ignora la direzione (dentro o fuori), si ottiene comunque più specifico per una conversione più generica! Voglio dire: "puoi considerare quel valore restituito come un tipo più generale (come un oggetto)" per la covarianza e: "L'API si aspetta qualcosa di generale (come un oggetto) puoi dargli qualcosa di più specifico (come una stringa)" per la contravarianza . Per me questi sembrano quasi uguali!
XMight

@AlexanderDerck: Non sono sicuro del perché non ti abbia risposto prima; Sono d'accordo che non è chiaro e cercherò di chiarirlo.
Jon Skeet,

16

Spero che il mio post aiuti a ottenere una visione agnostica dell'argomento.

Per i nostri corsi di formazione interni ho lavorato con il meraviglioso libro "Smalltalk, Objects and Design (Chamond Liu)" e ho riformulato i seguenti esempi.

Cosa significa "coerenza"? L'idea è quella di progettare gerarchie di tipi sicuri con tipi altamente sostituibili. La chiave per ottenere questa coerenza è la conformità basata sul sottotipo, se si lavora in un linguaggio tipicamente statico. (Discuteremo il principio di sostituzione di Liskov (LSP) ad alto livello qui.)

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

  • Covarianza: supponiamo che gli uccelli che depongono le uova "in modo coerente" con la tipizzazione statica: se il tipo Uccello depone un uovo, il sottotipo di Uccello non deponga un sottotipo di uovo? Ad esempio, il tipo Duck pone un DuckEgg, quindi viene fornita la coerenza. Perché è coerente? Perché in una tale espressione: Egg anEgg = aBird.Lay();il riferimento aBird potrebbe essere legalmente sostituito da un uccello o da un'istanza Duck. Diciamo che il tipo restituito è covariante al tipo, in cui è definito Lay (). L'override di un sottotipo può restituire un tipo più specializzato. => "Offrono di più."

  • Contravarianza: supponiamo che i pianoforti siano in grado di suonare "in modo coerente" con la tipizzazione statica: se una pianista suona il piano, sarebbe in grado di suonare un GrandPiano? Un virtuoso preferirebbe non suonare un GrandPiano? (Attenzione, c'è una svolta!) Questo è incoerente! Perché in una tale espressione: aPiano.Play(aPianist);aPiano non poteva essere legalmente sostituito da un piano o da un'istanza GrandPiano! Un GrandPiano può essere interpretato solo da un Virtuoso, i pianisti sono troppo generici! GrandPianos deve essere giocabile da tipi più generali, quindi il gioco è coerente. Diciamo che il tipo di parametro è contrario al tipo, in cui è definito Play (). L'override di un sottotipo può accettare un tipo più generalizzato. => "Richiedono meno."

Torna a C #:
poiché C # è fondamentalmente un linguaggio tipizzato staticamente, le "posizioni" dell'interfaccia di un tipo che dovrebbero essere co-o contraddittorie (ad esempio parametri e tipi di ritorno), devono essere contrassegnate esplicitamente per garantire un uso / sviluppo coerente di quel tipo , per far funzionare bene l'LSP. Nei linguaggi tipizzati dinamicamente la coerenza LSP non è in genere un problema, in altre parole si potrebbe eliminare completamente il "markup" co-e controverso su interfacce e delegati .Net, se si utilizzava solo la dinamica dei tipi nei tipi. - Ma questa non è la soluzione migliore in C # (non dovresti usare la dinamica nelle interfacce pubbliche).

Ritorno alla teoria:
la conformità descritta (tipi di ritorno covarianti / tipi di parametri contravarianti) è l'ideale teorico (supportato dalle lingue Emerald e POOL-1). Alcune lingue oop (ad esempio Eiffel) hanno deciso di applicare un altro tipo di coerenza, in particolare. anche tipi di parametri covarianti, perché descrive meglio la realtà rispetto all'ideale teorico. Nei linguaggi tipicamente statici la consistenza desiderata deve spesso essere raggiunta applicando modelli di progettazione come "doppio dispacciamento" e "visitatore". Altre lingue forniscono i cosiddetti "invio multiplo" o metodi multipli (si tratta essenzialmente di selezionare sovraccarichi di funzioni in fase di esecuzione , ad esempio con CLOS) o ottenere l'effetto desiderato utilizzando la digitazione dinamica.


Dici che l'override di un sottotipo può restituire un tipo più specializzato . Ma questo è completamente falso. Se Birddefinito public abstract BirdEgg Lay();, Duck : Bird DEVE implementare public override BirdEgg Lay(){}Quindi la tua affermazione che BirdEgg anEgg = aBird.Lay();ha qualsiasi tipo di varianza è semplicemente falsa. Essendo la premessa del punto della spiegazione, l'intero punto è ora sparito. Diresti invece che la covarianza esiste all'interno dell'implementazione in cui un DuckEgg viene implicitamente proiettato nel tipo out / return di BirdEgg? Ad ogni modo, per favore cancella 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 Java e C ++ lo fanno. Ho piuttosto descritto l'ideale teorico usando una sintassi simile a C #. In C # devi permettere a Bird and Duck di implementare un'interfaccia comune, in cui Lay è definito per avere un tipo di ritorno covariante (cioè fuori specifica), quindi le cose vanno bene insieme!
Nico,

1
Come analogo al commento di Matt-Klein sulla risposta di @ Jon-Skeet, "al mio sé futuro": Il miglior da asporto per me qui è "Consegnano di più" (specifico) e "Richiedono di meno" (specifico). "Richiedi meno e offri di più" è un eccellente mnemonico! È analogo a un lavoro in cui spero di richiedere istruzioni meno specifiche (richieste generali) e tuttavia fornire qualcosa di più specifico (un vero prodotto di lavoro). In ogni caso, l'ordine dei sottotipi (LSP) è ininterrotto.
karfus,

@karfus: Grazie, ma come ricordo ho parafrasato l'idea "Richiedi meno e consegna di più" da un'altra fonte. Potrebbe essere stato il libro di Liu a cui mi riferisco sopra ... o anche un discorso su .NET Rock. Btw. in Java, la gente ha ridotto il mnemonico a "PECS", che si riferisce direttamente al modo sintattico di dichiarare le varianze, PECS è per "Produttore extends, Consumatore super".
Nico,

5

Il delegato del convertitore mi aiuta a capire la differenza.

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

TOutputrappresenta la covarianza in cui un metodo restituisce un tipo più specifico .

TInputrappresenta la contraddizione in cui un metodo viene passato a 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();

0

La varianza tra Co e Contra è piuttosto logica. Il sistema del tipo di linguaggio ci obbliga a supportare la logica della vita reale. È facile da capire con l'esempio.

covarianza

Ad esempio, vuoi comprare un fiore e hai due negozi di fiori nella tua città: il negozio di rose e il negozio di margherite.

Se chiedi a qualcuno "dov'è il negozio di fiori?" e qualcuno ti dice dove si trova il negozio di rose, andrebbe bene? Sì, perché la rosa è un fiore, se vuoi comprare un fiore puoi comprare una rosa. Lo stesso vale se qualcuno ti ha risposto con l'indirizzo del negozio delle margherite.

Questo è un esempio di covarianza : è possibile eseguire il cast A<C>in A<B>, dove Cè una sottoclasse di B, se Aproduce valori generici (restituisce come risultato dalla funzione). La covarianza riguarda i produttori, ecco perché C # usa la parola chiave outper covarianza.

tipi:

class Flower {  }
class Rose: Flower { }
class Daisy: Flower { }

interface FlowerShop<out T> where T: Flower {
    T getFlower();
}

class RoseShop: FlowerShop<Rose> {
    public Rose getFlower() {
        return new Rose();
    }
}

class DaisyShop: FlowerShop<Daisy> {
    public Daisy getFlower() {
        return new Daisy();
    }
}

La domanda è "dov'è il negozio di fiori?", La risposta è "negozio di rose lì":

static FlowerShop<Flower> tellMeShopAddress() {
    return new RoseShop();
}

controvarianza

Ad esempio, vuoi regalare un fiore alla tua ragazza e alla tua ragazza piace qualsiasi fiore. Puoi considerarla come una persona che ama le rose o come una persona che ama le margherite? Sì, perché se lei ama qualsiasi fiore, amerebbe sia la rosa che la margherita.

Questo è un esempio del controvarianza : si è permesso di gettare A<B>a A<C>, dove Cè sottoclasse di B, se Aconsuma valore generico. La contraddizione riguarda i consumatori, ecco perché C # usa la parola chiave inper contravarianza.

tipi:

interface PrettyGirl<in TFavoriteFlower> where TFavoriteFlower: Flower {
    void takeGift(TFavoriteFlower flower);
}

class AnyFlowerLover: PrettyGirl<Flower> {
    public void takeGift(Flower flower) {
        Console.WriteLine("I like all flowers!");
    }
}

Stai considerando la tua ragazza che ama qualsiasi fiore come qualcuno che ama le rose e le dai una rosa:

PrettyGirl<Rose> girlfriend = new AnyFlowerLover();
girlfriend.takeGift(new Rose());

link

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.