Risposte:
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 Tiger
può essere assegnato a una variabile di tipo Animal
, quindi si dice che questi tipi siano "compatibili con l'assegnazione". Scriviamo "un valore di tipo X
può 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 .
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?
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 T
fosse 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 T
in 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 :)
Action<T>
sta ancora usando solo T
in una posizione di output" . Action<T>
il tipo restituito è nullo, come può essere utilizzato T
come output? O è quello che significa, perché non restituisce nulla che si possa vedere che non può mai violare la regola?
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.
Bird
definito 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.
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!
extends
, Consumatore super
".
Il delegato del convertitore mi aiuta a capire la differenza.
delegate TOutput Converter<in TInput, out TOutput>(TInput input);
TOutput
rappresenta la covarianza in cui un metodo restituisce un tipo più specifico .
TInput
rappresenta 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();
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.
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 A
produce valori generici (restituisce come risultato dalla funzione). La covarianza riguarda i produttori, ecco perché C # usa la parola chiave out
per 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();
}
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 A
consuma valore generico. La contraddizione riguarda i consumatori, ecco perché C # usa la parola chiave in
per 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());