Esempio di covarianza e contraddizione nel mondo reale


162

Sto avendo un po 'di problemi nel capire come userei la covarianza e la contraddizione nel mondo reale.

Finora, gli unici esempi che ho visto sono stati lo stesso vecchio esempio di array.

object[] objectArray = new string[] { "string 1", "string 2" };

Sarebbe bello vedere un esempio che mi permetterebbe di usarlo durante il mio sviluppo se potessi vederlo usato altrove.


1
Esplorerò la covarianza in questa risposta alla (mia) domanda: tipi di covarianza: per esempio . Penso che lo troverai interessante e, si spera, istruttivo.
Cristian Diaconescu,

Risposte:


109

Supponiamo che tu abbia una persona di classe e una classe che ne deriva, insegnante. Hai alcune operazioni che prendono IEnumerable<Person>come argomento. Nella tua classe di scuola hai un metodo che restituisce un IEnumerable<Teacher>. Covariance ti consente di utilizzare direttamente quel risultato per i metodi che accettano un IEnumerable<Person>, sostituendo un tipo più derivato con un tipo meno derivato (più generico). La contraddizione, in modo intuitivo, consente di utilizzare un tipo più generico, in cui è specificato un tipo più derivato.

Vedi anche Covarianza e contraddizione in Generics su MSDN .

Classi :

public class Person 
{
     public string Name { get; set; }
} 

public class Teacher : Person { } 

public class MailingList
{
    public void Add(IEnumerable<out Person> people) { ... }
}

public class School
{
    public IEnumerable<Teacher> GetTeachers() { ... }
}

public class PersonNameComparer : IComparer<Person>
{
    public int Compare(Person a, Person b) 
    { 
        if (a == null) return b == null ? 0 : -1;
        return b == null ? 1 : Compare(a,b);
    }

    private int Compare(string a, string b)
    {
        if (a == null) return b == null ? 0 : -1;
        return b == null ? 1 : a.CompareTo(b);
    }
}

Utilizzo :

var teachers = school.GetTeachers();
var mailingList = new MailingList();

// Add() is covariant, we can use a more derived type
mailingList.Add(teachers);

// the Set<T> constructor uses a contravariant interface, IComparer<in T>,
// we can use a more generic type than required.
// See https://msdn.microsoft.com/en-us/library/8ehhxeaf.aspx for declaration syntax
var teacherSet = new SortedSet<Teachers>(teachers, new PersonNameComparer());

14
@FilipBartuzi - se, come me quando ho scritto questa risposta, sei stato impiegato in un'università che è un vero esempio del mondo reale.
tvanfosson,

5
Come può essere contrassegnato questo come risposta quando non risponde alla domanda e non fornisce alcun esempio di come utilizzare la contro / varianza in c #?
Barakcaf,

@barakcaf ha aggiunto un esempio di contraddizione. non so perché non stavi vedendo l'esempio della covarianza - forse dovevi scorrere il codice verso il basso - ma ho aggiunto alcuni commenti in merito.
tvanfosson,

@tvanfosson il codice usa il co / contra, per cui non mostra come dichiararlo. L'esempio non mostra l'uso di in / out nella dichiarazione generica mentre l'altra risposta lo fa.
Barakcaf,

Quindi, se ho capito bene, la covarianza è ciò che consente il principio di sostituzione di Liskov in C #, giusto?
Miguel Veloso,

136
// Contravariance
interface IGobbler<in T> {
    void gobble(T t);
}

// Since a QuadrupedGobbler can gobble any four-footed
// creature, it is OK to treat it as a donkey gobbler.
IGobbler<Donkey> dg = new QuadrupedGobbler();
dg.gobble(MyDonkey());

// Covariance
interface ISpewer<out T> {
    T spew();
}

// A MouseSpewer obviously spews rodents (all mice are
// rodents), so we can treat it as a rodent spewer.
ISpewer<Rodent> rs = new MouseSpewer();
Rodent r = rs.spew();

Per completezza ...

// Invariance
interface IHat<T> {
    void hide(T t);
    T pull();
}

// A RabbitHat…
IHat<Rabbit> rHat = RabbitHat();

// …cannot be treated covariantly as a mammal hat…
IHat<Mammal> mHat = rHat;      // Compiler error
// …because…
mHat.hide(new Dolphin());      // Hide a dolphin in a rabbit hat??

// It also cannot be treated contravariantly as a cottontail hat…
IHat<CottonTail> cHat = rHat;  // Compiler error
// …because…
rHat.hide(new MarshRabbit());
cHat.pull();                   // Pull a marsh rabbit out of a cottontail hat??

138
Mi piace questo esempio realistico. La settimana scorsa stavo scrivendo un po 'di codice divorando asino ed ero così felice che adesso abbiamo la covarianza. :-)
Eric Lippert,

4
Questo commento sopra con @javadba che racconta a THE EricLippert cosa sono la covarianza e la contraddizione è un esempio realistico e covariante di me che dico a mia nonna come succhiare le uova! : p
iAteABug_And_iLiked_it

1
La domanda non chiedeva cosa possono fare la contraddizione e la covarianza , ma perché avresti bisogno di usarla . Il tuo esempio è tutt'altro che pratico perché non richiede neanche. Posso creare un QuadrupedGobbler e trattarlo come se stesso (assegnarlo a IGobbler <Quadruped>) e può ancora inghiottire gli asini (posso passare un asino al metodo Gobble che richiede un quadrupede). Non è necessaria alcuna contraddizione. È bello poter trattare un QuadrupedGobbler come un DonkeyGobbler, ma perché dovremmo, in questo caso, se un QuadrupedGobbler può già inghiottire gli asini?
wired_in

1
@wired_in Perché quando ti preoccupi solo degli asini, essere più generici può interferire. Ad esempio, se hai una fattoria che fornisce asini da divorare, puoi esprimerlo come void feed(IGobbler<Donkey> dg). Se invece prendessi un IGobbler <Quadruped> come parametro, non potresti passare un drago che mangia solo asini.
Marcelo Cantos,

1
Waaay tardi alla festa, ma questo è il miglior esempio scritto che ho visto in giro per SO. Ha un senso completo pur essendo ridicolo. Dovrò riempire il mio gioco di risposte ...
Jesse Williams,

122

Ecco cosa ho messo insieme per aiutarmi a capire la differenza

public interface ICovariant<out T> { }
public interface IContravariant<in T> { }

public class Covariant<T> : ICovariant<T> { }
public class Contravariant<T> : IContravariant<T> { }

public class Fruit { }
public class Apple : Fruit { }

public class TheInsAndOuts
{
    public void Covariance()
    {
        ICovariant<Fruit> fruit = new Covariant<Fruit>();
        ICovariant<Apple> apple = new Covariant<Apple>();

        Covariant(fruit);
        Covariant(apple); //apple is being upcasted to fruit, without the out keyword this will not compile
    }

    public void Contravariance()
    {
        IContravariant<Fruit> fruit = new Contravariant<Fruit>();
        IContravariant<Apple> apple = new Contravariant<Apple>();

        Contravariant(fruit); //fruit is being downcasted to apple, without the in keyword this will not compile
        Contravariant(apple);
    }

    public void Covariant(ICovariant<Fruit> fruit) { }

    public void Contravariant(IContravariant<Apple> apple) { }
}

TLDR

ICovariant<Fruit> apple = new Covariant<Apple>(); //because it's covariant
IContravariant<Apple> fruit = new Contravariant<Fruit>(); //because it's contravariant

10
Questa è la cosa migliore che ho visto finora che sia chiara e concisa. Grande esempio!
Rob L

6
In che modo il frutto può essere trasferito su Apple ( Contravariancenell'esempio) quando Fruitè genitore Apple?
Tobias Marschall,

@TobiasMarschall significa che devi studiare di più sul "polimorfismo"
snr

56

Le parole chiave in e out controllano le regole di casting del compilatore per interfacce e delegati con parametri generici:

interface IInvariant<T> {
    // This interface can not be implicitly cast AT ALL
    // Used for non-readonly collections
    IList<T> GetList { get; }
    // Used when T is used as both argument *and* return type
    T Method(T argument);
}//interface

interface ICovariant<out T> {
    // This interface can be implicitly cast to LESS DERIVED (upcasting)
    // Used for readonly collections
    IEnumerable<T> GetList { get; }
    // Used when T is used as return type
    T Method();
}//interface

interface IContravariant<in T> {
    // This interface can be implicitly cast to MORE DERIVED (downcasting)
    // Usually means T is used as argument
    void Method(T argument);
}//interface

class Casting {

    IInvariant<Animal> invariantAnimal;
    ICovariant<Animal> covariantAnimal;
    IContravariant<Animal> contravariantAnimal;

    IInvariant<Fish> invariantFish;
    ICovariant<Fish> covariantFish;
    IContravariant<Fish> contravariantFish;

    public void Go() {

        // NOT ALLOWED invariants do *not* allow implicit casting:
        invariantAnimal = invariantFish; 
        invariantFish = invariantAnimal; // NOT ALLOWED

        // ALLOWED covariants *allow* implicit upcasting:
        covariantAnimal = covariantFish; 
        // NOT ALLOWED covariants do *not* allow implicit downcasting:
        covariantFish = covariantAnimal; 

        // NOT ALLOWED contravariants do *not* allow implicit upcasting:
        contravariantAnimal = contravariantFish; 
        // ALLOWED contravariants *allow* implicit downcasting
        contravariantFish = contravariantAnimal; 

    }//method

}//class

// .NET Framework Examples:
public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable { }
public interface IEnumerable<out T> : IEnumerable { }


class Delegates {

    // When T is used as both "in" (argument) and "out" (return value)
    delegate T Invariant<T>(T argument);

    // When T is used as "out" (return value) only
    delegate T Covariant<out T>();

    // When T is used as "in" (argument) only
    delegate void Contravariant<in T>(T argument);

    // Confusing
    delegate T CovariantBoth<out T>(T argument);

    // Confusing
    delegate T ContravariantBoth<in T>(T argument);

    // From .NET Framework:
    public delegate void Action<in T>(T obj);
    public delegate TResult Func<in T, out TResult>(T arg);

}//class

Supponendo che il pesce sia un sottotipo di animale. Ottima risposta a proposito.
Rajan Prasad,

49

Ecco un semplice esempio usando una gerarchia ereditaria.

Data la semplice gerarchia di classi:

inserisci qui la descrizione dell'immagine

E nel codice:

public abstract class LifeForm  { }
public abstract class Animal : LifeForm { }
public class Giraffe : Animal { }
public class Zebra : Animal { }

Invarianza (ovvero parametri di tipo generico * non * decorato con ino outparole chiave)

Apparentemente, un metodo come questo

public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
    foreach (var lifeForm in lifeForms)
    {
        Console.WriteLine(lifeForm.GetType().ToString());
    }
}

... dovrebbe accettare una raccolta eterogenea: (cosa che fa)

var myAnimals = new List<LifeForm>
{
    new Giraffe(),
    new Zebra()
};
PrintLifeForms(myAnimals); // Giraffe, Zebra

Tuttavia, il passaggio di una raccolta di un tipo più derivato non riesce!

var myGiraffes = new List<Giraffe>
{
    new Giraffe(), // "Jerry"
    new Giraffe() // "Melman"
};
PrintLifeForms(myGiraffes); // Compile Error!

cannot convert from 'System.Collections.Generic.List<Giraffe>' to 'System.Collections.Generic.IList<LifeForm>'

Perché? Poiché il parametro generico IList<LifeForm>non è covariante - IList<T>è invariante, quindi IList<LifeForm>accetta solo raccolte (che implementano IList) dove Tdeve essere il tipo con parametri LifeForm.

Se l'implementazione del metodo PrintLifeFormsera dannosa (ma ha la stessa firma del metodo), il motivo per cui il compilatore impedisce il passaggio List<Giraffe>diventa ovvio:

 public static void PrintLifeForms(IList<LifeForm> lifeForms)
 {
     lifeForms.Add(new Zebra());
 }

Poiché IListconsente l'aggiunta o la rimozione di elementi, qualsiasi sottoclasse di LifeFormpotrebbe quindi essere aggiunta al parametro lifeFormse violerebbe il tipo di qualsiasi raccolta di tipi derivati ​​passati al metodo. (Qui, il metodo dannoso tenterebbe di aggiungere un Zebraa var myGiraffes). Fortunatamente, il compilatore ci protegge da questo pericolo.

Covarianza (Generico con tipo parametrico decorato con out)

La covarianza è ampiamente utilizzata con raccolte immutabili (ovvero dove nuovi elementi non possono essere aggiunti o rimossi da una raccolta)

La soluzione all'esempio precedente è garantire che venga utilizzato un tipo di raccolta generico covariante, ad esempio IEnumerable(definito come IEnumerable<out T>). IEnumerablenon ha metodi per modificare la raccolta e, a causa della outcovarianza, qualsiasi raccolta con sottotipo di LifeFormpuò ora essere passata al metodo:

public static void PrintLifeForms(IEnumerable<LifeForm> lifeForms)
{
    foreach (var lifeForm in lifeForms)
    {
        Console.WriteLine(lifeForm.GetType().ToString());
    }
}

PrintLifeFormspuò ora essere chiamato con Zebras, Giraffese qualsiasi IEnumerable<>di qualsiasi sottoclasse diLifeForm

Contravarianza (Generico con tipo parametrico decorato con in)

La contraddizione viene spesso utilizzata quando le funzioni vengono passate come parametri.

Ecco un esempio di una funzione, che accetta un Action<Zebra>come parametro e lo richiama su un'istanza nota di una Zebra:

public void PerformZebraAction(Action<Zebra> zebraAction)
{
    var zebra = new Zebra();
    zebraAction(zebra);
}

Come previsto, funziona perfettamente:

var myAction = new Action<Zebra>(z => Console.WriteLine("I'm a zebra"));
PerformZebraAction(myAction); // I'm a zebra

Intuitivamente, questo fallirà:

var myAction = new Action<Giraffe>(g => Console.WriteLine("I'm a giraffe"));
PerformZebraAction(myAction); 

cannot convert from 'System.Action<Giraffe>' to 'System.Action<Zebra>'

Tuttavia, questo ha successo

var myAction = new Action<Animal>(a => Console.WriteLine("I'm an animal"));
PerformZebraAction(myAction); // I'm an animal

e anche questo riesce anche:

var myAction = new Action<object>(a => Console.WriteLine("I'm an amoeba"));
PerformZebraAction(myAction); // I'm an amoeba

Perché? Perché Actionè definito come Action<in T>, cioè lo è contravariant, nel senso che per Action<Zebra> myAction, che myActionpuò essere al massimo "a" Action<Zebra>, ma Zebrasono accettabili anche superclassi di meno derivate .

Anche se all'inizio potrebbe non essere intuitivo (ad es. Come si può Action<object>passare come parametro che richiede Action<Zebra>?), Se si scompattano i passaggi, si noterà che la funzione chiamata ( PerformZebraAction) stessa è responsabile del passaggio dei dati (in questo caso Zebraun'istanza ) alla funzione: i dati non provengono dal codice chiamante.

A causa dell'approccio invertito di utilizzare le funzioni di ordine superiore in questo modo, quando Actionviene invocata, è l' Zebraistanza più derivata che viene invocata contro la zebraActionfunzione (passata come parametro), sebbene la funzione stessa utilizzi un tipo meno derivato.


7
Questa è una grande spiegazione per le diverse opzioni di varianza, poiché parla dell'esempio e chiarisce anche perché il compilatore limita o consente senza le parole chiave in / out
Vikhram,

Dove viene inutilizzata la parola chiave per la contraddizione ?
javadba,

@javadba in sopra, Action<in T> e Func<in T, out TResult>sono contraddittori nel tipo di input. (I miei esempi usano tipi di invariante (Elenco), covariante (IEnumerable) e contravariante (Azione, Func)
esistenti

Ok non lo faccio C#, non lo saprei.
javadba,

È abbastanza simile in Scala, solo una diversa sintassi - [+ T] sarebbe covariante in T, [-T] sarebbe contraddittorio in T, Scala può anche imporre il vincolo 'tra' e la sottoclasse promiscua 'Nulla', che C # non ha.
StuartLC

32
class A {}
class B : A {}

public void SomeFunction()
{
    var someListOfB = new List<B>();
    someListOfB.Add(new B());
    someListOfB.Add(new B());
    someListOfB.Add(new B());
    SomeFunctionThatTakesA(someListOfB);
}

public void SomeFunctionThatTakesA(IEnumerable<A> input)
{
    // Before C# 4, you couldn't pass in List<B>:
    // cannot convert from
    // 'System.Collections.Generic.List<ConsoleApplication1.B>' to
    // 'System.Collections.Generic.IEnumerable<ConsoleApplication1.A>'
}

Fondamentalmente ogni volta che hai avuto una funzione che accetta un Enumerabile di un tipo, non puoi passare un Enumerabile di un tipo derivato senza esplicitamente lanciarlo.

Solo per avvertirti di una trappola però:

var ListOfB = new List<B>();
if(ListOfB is IEnumerable<A>)
{
    // In C# 4, this branch will
    // execute...
    Console.Write("It is A");
}
else if (ListOfB is IEnumerable<B>)
{
    // ...but in C# 3 and earlier,
    // this one will execute instead.
    Console.Write("It is B");
}

Questo è comunque un codice orribile, ma esiste e il comportamento mutevole in C # 4 potrebbe introdurre bug sottili e difficili da trovare se usi un costrutto come questo.


Quindi questo influenza le raccolte più di ogni altra cosa, perché in c # 3 potresti passare un tipo più derivato in un metodo di tipo meno derivato.
Rasoio

3
Sì, il grande cambiamento è che IEnumerable ora supporta questo, mentre prima non lo faceva.
Michael Stum

4

Da MSDN

L'esempio di codice seguente mostra il supporto per covarianza e contravarianza per gruppi di metodi

static object GetObject() { return null; }
static void SetObject(object obj) { }

static string GetString() { return ""; }
static void SetString(string str) { }

static void Test()
{
    // Covariance. A delegate specifies a return type as object, 
    // but you can assign a method that returns a string.
    Func<object> del = GetString;

    // Contravariance. A delegate specifies a parameter type as string, 
    // but you can assign a method that takes an object.
    Action<string> del2 = SetObject;
}

4

controvarianza

Nel mondo reale, puoi sempre usare un rifugio per animali invece di un rifugio per conigli perché ogni volta che un rifugio per animali ospita un coniglio è un animale. Tuttavia, se usi un rifugio per conigli invece di un rifugio per animali, il suo personale può essere mangiato da una tigre.

Nel codice, questo significa che se si dispone di un IShelter<Animal> animalssi può semplicemente scrivere IShelter<Rabbit> rabbits = animals se promettete e l'uso Tnel IShelter<T>solo come parametri del metodo in questo modo:

public class Contravariance
{
    public class Animal { }
    public class Rabbit : Animal { }

    public interface IShelter<in T>
    {
        void Host(T thing);
    }

    public void NoCompileErrors()
    {
        IShelter<Animal> animals = null;
        IShelter<Rabbit> rabbits = null;

        rabbits = animals;
    }
}

e sostituire un elemento con una più generica, cioè ridurre la varianza o introdurre contra varianza.

covarianza

Nel mondo reale, puoi sempre usare un fornitore di conigli invece di un fornitore di animali perché ogni volta che un fornitore di coniglio ti dà un coniglio è un animale. Tuttavia, se usi un fornitore di animali anziché un fornitore di conigli, puoi essere mangiato da una tigre.

Nel codice, questo significa che se si dispone di un ISupply<Rabbit> rabbitssi può semplicemente scrivere ISupply<Animal> animals = rabbits se promettete e l'uso Tnel ISupply<T>solo come valori di ritorno del metodo in questo modo:

public class Covariance
{
    public class Animal { }
    public class Rabbit : Animal { }

    public interface ISupply<out T>
    {
        T Get();
    }

    public void NoCompileErrors()
    {
        ISupply<Animal> animals = null;
        ISupply<Rabbit> rabbits = null;

        animals = rabbits;
    }
}

e sostituire un elemento con una più derivato uno, ossia aumentare la varianza o introdurre co varianza.

Tutto sommato, questa è solo una promessa verificabile in fase di compilazione da parte tua che tratteresti un tipo generico in un certo modo per mantenere la sicurezza del tipo e non far mangiare nessuno.

Si potrebbe voler dare questa una lettura a doppio avvolgere la testa intorno a questo.


puoi essere mangiato da una tigre che è valsa la pena
javadba

Il tuo commento su contravarianceè interessante. Lo sto leggendo come un requisito operativo : il tipo più generale deve supportare i casi d'uso di tutti i tipi derivati ​​da esso. Quindi, in questo caso, il canile deve essere in grado di supportare il ricovero di ogni tipo di animale. In tal caso l'aggiunta di una nuova sottoclasse potrebbe interrompere la superclasse! Cioè, se aggiungiamo un sottotipo Tyrannosaurus Rex , potrebbe rovinare il nostro rifugio per animali esistente .
Javavba

(Continua). Ciò differisce nettamente dalla covarianza che è chiaramente descritta strutturalmente : tutti i sottotipi più specifici supportano le operazioni definite nel super tipo, ma non necessariamente nello stesso modo.
javadba

3

Il delegato del convertitore mi aiuta a visualizzare entrambi i concetti lavorando insieme:

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