Kovarian dan contravariance contoh dunia nyata


162

Saya mengalami sedikit kesulitan memahami bagaimana saya akan menggunakan kovarians dan contravariance di dunia nyata.

Sejauh ini, satu-satunya contoh yang saya lihat adalah contoh array lama yang sama.

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

Akan menyenangkan untuk melihat contoh yang memungkinkan saya untuk menggunakannya selama pengembangan saya jika saya bisa melihatnya digunakan di tempat lain.


1
Saya mengeksplorasi kovarian dalam jawaban untuk pertanyaan (saya sendiri): tipe kovarians: dengan contoh . Saya pikir Anda akan menemukan itu menarik, dan mudah-mudahan bermanfaat.
Cristian Diaconescu

Jawaban:


109

Katakanlah Anda memiliki Pribadi kelas dan kelas yang berasal darinya, Guru. Anda memiliki beberapa operasi yang menganggap IEnumerable<Person>sebagai argumen. Di kelas Sekolah Anda, Anda memiliki metode yang mengembalikan IEnumerable<Teacher>. Kovarian memungkinkan Anda untuk langsung menggunakan hasil itu untuk metode yang mengambil IEnumerable<Person>, menggantikan jenis yang lebih diturunkan untuk jenis yang lebih turunan (lebih generik). Kontravarians, kontra-intuitif, memungkinkan Anda untuk menggunakan jenis yang lebih umum, di mana jenis yang lebih diturunkan ditentukan.

Lihat juga Kovarian dan Kontravarian dalam Generik pada MSDN .

Kelas :

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

Penggunaan :

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 - jika, seperti saya ketika saya menulis jawaban ini, Anda dipekerjakan di Universitas yang sangat banyak contoh dunia nyata.
tvanfosson

5
Bagaimana ini bisa ditandai jawaban ketika tidak menjawab pertanyaan dan tidak memberikan contoh menggunakan co / contra variance di c #?
barakcaf

@barakcaf menambahkan contoh contravariance. tidak yakin mengapa Anda tidak melihat contoh kovarians - mungkin Anda perlu menggulirkan kode ke bawah - tetapi saya menambahkan beberapa komentar tentang itu.
tvanfosson

@vanfosson kode menggunakan co / contra, saya ment bahwa itu tidak menunjukkan cara mendeklarasikannya. Contoh ini tidak menunjukkan penggunaan in / out dalam deklarasi generik sementara jawaban lainnya tidak.
barakcaf

Jadi, jika saya melakukannya dengan benar, kovarian adalah yang memungkinkan prinsip substitusi Liskov dalam C #, benarkah itu?
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();

Untuk kelengkapan ...

// 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
Saya suka contoh realistis ini. Saya baru saja menulis beberapa kode gobbling keledai minggu lalu dan saya sangat senang bahwa kita memiliki kovarians sekarang. :-)
Eric Lippert

4
Komentar di atas dengan @javadba memberi tahu THE EricLippert apa itu kovarians dan contravariance adalah contoh kovarian yang realistis tentang saya memberi tahu nenek saya bagaimana cara menghisap telur! : p
iAteABug_And_iLiked_it

1
Pertanyaannya tidak menanyakan apa yang dapat dilakukan oleh kontravarian dan kovarian , ia bertanya mengapa Anda perlu menggunakannya . Contoh Anda jauh dari praktis karena tidak memerlukan keduanya. Saya bisa membuat QuadrupedGobbler dan memperlakukannya seperti itu sendiri (tetapkan ke IGobbler <Quadruped>) dan masih bisa melahap Donkeys (saya bisa meneruskan Donkey ke metode Gobble yang membutuhkan Quadruped). Tidak perlu contravariance. Itu keren bahwa kita dapat memperlakukan QuadrupedGobbler sebagai DonkeyGobbler, tetapi mengapa kita perlu, dalam hal ini, jika QuadrupedGobbler sudah dapat melahap Donkeys?
wired_in

1
@wired_in Karena ketika Anda hanya peduli tentang keledai, menjadi lebih umum dapat menghalangi. Misalnya, jika Anda memiliki peternakan yang memasok keledai untuk dimakan, Anda dapat menyatakannya sebagai void feed(IGobbler<Donkey> dg). Jika Anda menggunakan IGobbler <Quadruped> sebagai parameter, Anda tidak bisa meneruskan naga yang hanya memakan keledai.
Marcelo Cantos

1
Waaay terlambat ke pesta, tapi ini tentang contoh tulisan terbaik yang pernah kulihat di sekitar SO. Masuk akal sepenuhnya saat menjadi konyol. Saya harus menyelesaikan permainan saya dengan jawaban ...
Jesse Williams

120

Inilah yang saya kumpulkan untuk membantu saya memahami perbedaannya

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
Ini adalah hal terbaik yang saya lihat sejauh ini yang jelas dan ringkas. Contoh bagus!
Rob L

6
Bagaimana buah bisa diturunkan menjadi apel (dalam Contravariancecontoh) kapan Fruitinduknya Apple?
Tobias Marschall

@TobiasMarschall itu artinya Anda harus belajar lebih banyak tentang "polimorfisme"
snr

56

Kata kunci masuk dan keluar mengontrol aturan casting kompiler untuk antarmuka dan delegasi dengan parameter umum:

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

Mengasumsikan Ikan adalah subtipe Hewan. Omong-omong jawaban yang bagus.
Rajan Prasad

48

Berikut adalah contoh sederhana menggunakan hierarki warisan.

Dengan hierarki kelas yang sederhana:

masukkan deskripsi gambar di sini

Dan dalam kode:

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

Invarian (mis. Parameter tipe umum * tidak * dihiasi inatau outkata kunci)

Tampaknya, metode seperti ini

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

... harus menerima koleksi yang heterogen: (yang memang ada)

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

Namun, melewati koleksi tipe yang lebih diturunkan gagal!

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>'

Mengapa? Karena parameter generikIList<LifeForm> bukan kovarian - IList<T>invarian, jadi IList<LifeForm>hanya menerima koleksi (yang mengimplementasikan IList) di mana tipe parameternya Tharus LifeForm.

Jika metode implementasi PrintLifeForms berbahaya (tetapi memiliki tanda tangan metode yang sama), alasan mengapa kompiler mencegah lewat List<Giraffe>menjadi jelas:

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

Karena IListmengizinkan penambahan atau penghapusan elemen, maka setiap subkelas LifeFormdapat ditambahkan ke parameterlifeForms , dan akan melanggar tipe kumpulan dari setiap tipe turunan yang diteruskan ke metode. (Di sini, metode jahat akan berusaha menambahkan Zebrake var myGiraffes). Untungnya, kompiler melindungi kita dari bahaya ini.

Kovarian (Generik dengan tipe parameterisasi yang didekorasi out)

Kovarian secara luas digunakan dengan koleksi yang tidak dapat diubah (yaitu ketika elemen baru tidak dapat ditambahkan atau dihapus dari koleksi)

Solusi untuk contoh di atas adalah untuk memastikan bahwa tipe pengumpulan generik kovarian digunakan, misalnya IEnumerable(didefinisikan sebagai IEnumerable<out T>). IEnumerabletidak memiliki metode untuk mengubah ke koleksi, dan sebagai hasil dari outkovarian, koleksi dengan subtipe LifeFormsekarang dapat diteruskan ke metode:

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

PrintLifeFormssekarang dapat dipanggil dengan Zebras, Giraffesdan IEnumerable<>sembarang subkelas dariLifeForm

Contravariance (Generic dengan tipe parameterized yang didekorasi dengan in)

Kontravarians sering digunakan ketika fungsi dilewatkan sebagai parameter.

Berikut adalah contoh fungsi, yang menggunakan Action<Zebra>parameter, dan menjalankannya pada turunan Zebra yang dikenal:

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

Seperti yang diharapkan, ini berfungsi dengan baik:

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

Secara intuitif, ini akan gagal:

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

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

Namun, ini berhasil

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

dan bahkan ini juga berhasil:

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

Mengapa? Karena Actiondidefinisikan sebagai Action<in T>, yaitu contravariant, artinya untuk Action<Zebra> myAction, yang myActiondapat berupa "sebagian besar" Action<Zebra>, tetapi superclasses yang kurang turunan dariZebra juga dapat diterima.

Meskipun ini mungkin tidak intuitif pada awalnya (misalnya bagaimana bisa Action<object>dilewatkan sebagai parameter yang memerlukan Action<Zebra>?), Jika Anda membongkar langkah-langkahnya, Anda akan mencatat bahwa fungsi yang dipanggil ( PerformZebraAction) itu sendiri bertanggung jawab untuk mengirimkan data (dalam hal ini Zebracontoh ) ke fungsi - data tidak berasal dari kode panggilan.

Karena pendekatan terbalik menggunakan fungsi urutan yang lebih tinggi dengan cara ini, pada saat Actiondipanggil, itu adalah turunan yang lebih banyak Zebrayang dipanggil terhadap zebraActionfungsi (dilewatkan sebagai parameter), meskipun fungsi itu sendiri menggunakan tipe yang kurang diturunkan.


7
Ini adalah penjelasan yang bagus untuk opsi varian yang berbeda, karena ia berbicara melalui contoh dan juga menjelaskan mengapa kompiler membatasi atau mengizinkan tanpa kata kunci masuk / keluar
Vikhram

Di mana inkata kunci yang digunakan untuk contravariance ?
javadba

@javadba di atas, Action<in T>dan Func<in T, out TResult>bersifat contravarian dalam jenis input. (Contoh saya menggunakan jenis invarian (Daftar), kovarian (IEnumerable) dan contravariant (Action, Func) lainnya
StuartLC

Ok saya tidak C#jadi tidak akan tahu itu.
javadba

Ini cukup mirip di Scala, hanya sintaks yang berbeda - [+ T] akan menjadi kovarian dalam T, [-T] akan menjadi contravariant di T, Scala juga dapat menegakkan batasan 'antara', dan subkelas promiscuous 'Nothing', yang C # tidak punya.
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>'
}

Pada dasarnya setiap kali Anda memiliki fungsi yang mengambil Enumerable dari satu jenis, Anda tidak bisa meneruskan Enumerable dari tipe turunan tanpa secara eksplisit menuangnya.

Hanya untuk memperingatkan Anda tentang jebakan:

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

Itu adalah kode yang mengerikan, tetapi itu memang ada dan perilaku yang berubah di C # 4 mungkin memperkenalkan bug yang sulit ditemukan jika Anda menggunakan konstruksi seperti ini.


Jadi ini mempengaruhi koleksi lebih dari apa pun, karena dalam c # 3 Anda bisa meneruskan tipe yang lebih diturunkan ke metode tipe yang kurang diturunkan.
Pisau cukur

3
Ya, perubahan besar adalah bahwa IEnumerable sekarang mendukung ini, padahal tidak sebelumnya.
Michael Stum

4

Dari MSDN

Contoh kode berikut ini menunjukkan dukungan kovarians dan contravariance untuk grup metode

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

Contravariance

Di dunia nyata, Anda selalu dapat menggunakan tempat berlindung untuk hewan daripada tempat berlindung untuk kelinci karena setiap kali tempat penampungan hewan menampung kelinci, ia adalah hewan. Namun, jika Anda menggunakan tempat perlindungan kelinci alih-alih tempat perlindungan hewan, stafnya dapat dimakan oleh seekor harimau.

Dalam kode, ini berarti bahwa jika Anda memiliki IShelter<Animal> animalsAnda dapat hanya menulis IShelter<Rabbit> rabbits = animals jika Anda berjanji dan penggunaan Tdalam IShelter<T>hanya sebagai metode parameter suka begitu:

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

dan ganti item dengan yang lebih umum, yaitu kurangi varians atau perkenalkan contra variance.

Kovarian

Di dunia nyata, Anda selalu dapat menggunakan pemasok kelinci alih-alih pemasok hewan karena setiap kali pemasok kelinci memberi Anda kelinci itu adalah hewan. Namun, jika Anda menggunakan pemasok hewan alih-alih pemasok kelinci, Anda bisa dimakan oleh harimau.

Dalam kode, ini berarti bahwa jika Anda memiliki ISupply<Rabbit> rabbitsAnda dapat hanya menulis ISupply<Animal> animals = rabbits jika Anda berjanji dan penggunaan Tdalam ISupply<T>hanya sebagai metode kembali nilai-nilai seperti itu:

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

dan ganti item dengan item yang lebih diturunkan, yaitu menambah varians atau memperkenalkan varian bersama .

Semua dalam semua, ini hanya janji waktu kompilasi yang dapat diperiksa dari Anda bahwa Anda akan memperlakukan jenis generik dengan cara tertentu untuk menjaga keamanan jenis dan tidak membuat siapa pun dimakan.

Anda mungkin ingin memberikan ini sebuah baca dua kali lipat-membungkus kepala Anda sekitar ini.


Anda bisa dimakan oleh seekor harimau. Itu layak
diambil

Komentar Anda tentang contravarianceini menarik. Saya membacanya sebagai indikasi persyaratan operasional : bahwa jenis yang lebih umum harus mendukung kasus penggunaan dari semua jenis yang berasal darinya. Jadi dalam hal ini tempat perlindungan hewan harus dapat mendukung perlindungan setiap jenis hewan. Jika demikian, menambahkan subkelas baru dapat merusak superclass! Yaitu - jika kita menambahkan subtipe Tyrannosaurus Rex maka itu dapat menghancurkan tempat berlindung hewan yang ada .
javadba

(Lanjutan). Itu berbeda secara tajam dari kovarians yang secara jelas dijelaskan secara struktural : semua sub-tipe yang lebih spesifik mendukung operasi yang didefinisikan dalam tipe super - tetapi tidak harus dengan cara yang sama.
javadba

3

Delegasi konverter membantu saya memvisualisasikan kedua konsep yang bekerja bersama:

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

TOutputmewakili kovarians di mana metode mengembalikan tipe yang lebih spesifik .

TInputmewakili contravariance di mana metode dilewatkan jenis yang kurang spesifik .

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();
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.