Perbedaan antara Kovarian & Kontra-varians

Jawaban:

266

Pertanyaannya adalah "apa perbedaan antara kovarians dan contravariance?"

Kovarian dan contravariance adalah properti dari fungsi pemetaan yang mengaitkan satu anggota dari satu set dengan yang lainnya . Lebih khusus lagi, pemetaan bisa bersifat kovarian atau contravarian sehubungan dengan suatu relasi pada set itu.

Pertimbangkan dua himpunan bagian dari himpunan semua tipe C # berikut. Pertama:

{ Animal, 
  Tiger, 
  Fruit, 
  Banana }.

Dan kedua, set yang jelas terkait ini:

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

Ada operasi pemetaan dari set pertama ke set kedua. Yaitu, untuk setiap T di set pertama, jenis yang sesuai di set kedua adalah IEnumerable<T>. Atau, dalam bentuk singkat, pemetaannya adalah T → IE<T>. Perhatikan bahwa ini adalah "panah tipis".

Dengan saya sejauh ini?

Sekarang mari kita pertimbangkan hubungan . Ada hubungan kompatibilitas penugasan antara pasangan jenis di set pertama. Nilai tipe Tigerdapat ditugaskan ke variabel tipe Animal, jadi tipe ini dikatakan "kompatibel dengan tugas". Mari kita menulis "nilai tipe Xdapat ditugaskan ke variabel tipe Y" dalam bentuk yang lebih pendek:X ⇒ Y . Perhatikan bahwa ini adalah "panah gemuk".

Jadi di subset pertama kami, berikut adalah semua hubungan kompatibilitas penugasan:

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

Dalam C # 4, yang mendukung kompatibilitas penugasan kovarian dari antarmuka tertentu, ada hubungan kompatibilitas penugasan antara pasangan jenis di set kedua:

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

Perhatikan bahwa pemetaan T → IE<T> mempertahankan keberadaan dan arah kompatibilitas tugas . Artinya, jika X ⇒ Y, maka benar jugaIE<X> ⇒ IE<Y> .

Jika kita memiliki dua hal di kedua sisi panah gemuk, maka kita dapat mengganti kedua sisi dengan sesuatu di sisi kanan panah tipis yang sesuai.

Pemetaan yang memiliki properti ini sehubungan dengan hubungan tertentu disebut "pemetaan kovarian". Ini harus masuk akal: urutan Macan dapat digunakan di mana urutan Hewan diperlukan, tetapi yang sebaliknya tidak benar. Sekuens binatang tidak perlu digunakan jika sekuens harimau diperlukan.

Itu kovarians. Sekarang pertimbangkan subset himpunan semua jenis ini:

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

sekarang kita memiliki pemetaan dari set pertama ke set ketiga T → IC<T>.

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

Artinya, pemetaan T → IC<T>telah mempertahankan keberadaan tetapi membalik arah kompatibilitas tugas. Kalau begitu X ⇒ Y, kalau begituIC<X> ⇐ IC<Y> .

Pemetaan yang mempertahankan tetapi membalikkan suatu relasi disebut pemetaan contravarian .

Sekali lagi, ini harus jelas benar. Perangkat yang dapat membandingkan dua Hewan juga dapat membandingkan dua Harimau, tetapi perangkat yang dapat membandingkan dua Harimau tidak selalu dapat membandingkan dua Hewan.

Jadi itulah perbedaan antara kovarians dan contravariance dalam C # 4. Covariance mempertahankan arah penugasan. Kontravarians membalikkannya .

Eric Lippert
sumber
4
Untuk orang seperti saya, akan lebih baik untuk menambahkan contoh yang menunjukkan apa yang TIDAK kovarian dan apa yang TIDAK kontravarian dan apa yang TIDAK keduanya.
bjan
2
@ Bargitta: Sangat mirip. Perbedaannya adalah bahwa C # menggunakan varians situs yang pasti dan Java menggunakan varians situs panggilan . Jadi cara variasinya sama, tetapi pengembang mengatakan "Saya perlu varian ini" berbeda. Kebetulan, fitur dalam kedua bahasa itu sebagian dirancang oleh orang yang sama!
Eric Lippert
2
@AshishNegi: Baca panah sebagai "dapat digunakan sebagai". "Suatu hal yang dapat membandingkan hewan dapat digunakan sebagai hal yang dapat membandingkan harimau". Masuk akal sekarang?
Eric Lippert
1
@AshishNegi: Tidak, itu tidak benar. IEnumerable adalah kovarian karena T hanya muncul dalam pengembalian metode IEnumerable. Dan IComparable bersifat contravariant karena T hanya muncul sebagai parameter formal dari metode IComparable .
Eric Lippert
2
@AshishNegi: Anda ingin memikirkan alasan logis yang mendasari hubungan ini. Mengapa kita bisa masuk IEnumerable<Tiger>dengan IEnumerable<Animal>aman? Karena tidak ada cara untuk memasukkan jerapah IEnumerable<Animal>. Mengapa kita dapat mengkonversi IComparable<Animal>ke IComparable<Tiger>? Karena tidak ada cara untuk mengambil jerapah dari IComparable<Animal>. Masuk akal?
Eric Lippert
111

Mungkin paling mudah untuk memberikan contoh - itu tentu cara saya mengingatnya.

Kovarian

Contoh kanonik: IEnumerable<out T>,Func<out T>

Anda dapat mengonversi dari IEnumerable<string>ke IEnumerable<object>, atau Func<string>ke Func<object>. Nilai hanya keluar dari benda-benda ini.

Ini berfungsi karena jika Anda hanya mengambil nilai dari API, dan itu akan mengembalikan sesuatu yang spesifik (seperti string), Anda bisa memperlakukan nilai yang dikembalikan itu sebagai tipe yang lebih umum (seperti object).

Contravariance

Contoh kanonik: IComparer<in T>,Action<in T>

Anda dapat mengonversi dari IComparer<object>ke IComparer<string>, atau Action<object>ke Action<string>; nilai hanya masuk ke objek ini.

Kali ini berfungsi karena jika API mengharapkan sesuatu yang umum (seperti object) Anda dapat memberikannya sesuatu yang lebih spesifik (seperti string).

Lebih umum

Jika Anda memiliki antarmuka, IFoo<T>ia dapat menjadi kovarian T(yaitu menyatakannya seolah- IFoo<out T>olah Thanya digunakan dalam posisi keluaran (misalnya tipe pengembalian) di dalam antarmuka. Ini dapat menjadi contravarian di T(yaitu IFoo<in T>) jikaT hanya digunakan dalam posisi input ( misalnya tipe parameter).

Ini berpotensi membingungkan karena "posisi output" tidak sesederhana kedengarannya - parameter tipe Action<T>masih hanya menggunakan Tdalam posisi output - contravariance dari Action<T>memutarnya, jika Anda melihat apa yang saya maksud. Ini adalah "output" di mana nilai-nilai dapat lulus dari implementasi metode menuju kode pemanggil, seperti nilai balik yang bisa. Biasanya hal semacam ini tidak muncul, untungnya :)

Jon Skeet
sumber
1
Untuk orang seperti saya, akan lebih baik untuk menambahkan contoh yang menunjukkan apa yang TIDAK kovarian dan apa yang TIDAK kontravarian dan apa yang TIDAK keduanya.
bjan
1
@ Jon Skeet Contoh yang bagus, saya hanya tidak mengerti "parameter tipe Action<T>masih hanya menggunakan Tdalam posisi output" . Action<T>tipe kembali batal, bagaimana bisa digunakan Tsebagai output? Atau apakah itu artinya, karena tidak mengembalikan apa pun yang Anda lihat bahwa itu tidak akan pernah melanggar aturan?
Alexander Derck
2
Untuk diri saya di masa depan, yang datang kembali ke jawaban ini sangat baik lagi untuk mempelajari kembali perbedaan, ini adalah garis yang Anda inginkan: "[Kovarian] bekerja karena jika Anda hanya mengambil nilai-nilai dari API, dan itu akan kembali sesuatu spesifik (seperti string), Anda bisa memperlakukan nilai yang dikembalikan sebagai tipe yang lebih umum (seperti objek). "
Matt Klein
Bagian yang paling membingungkan dari semua ini adalah baik untuk kovarians atau contravariance, jika Anda mengabaikan arah (masuk atau keluar), Anda tetap mendapatkan konversi yang Lebih Spesifik menjadi Lebih Generik! Maksud saya: "Anda dapat memperlakukan nilai yang dikembalikan sebagai jenis yang lebih umum (seperti objek)" untuk kovarians dan: "API mengharapkan sesuatu yang umum (seperti objek), Anda dapat memberikannya sesuatu yang lebih spesifik (seperti string)" untuk contravariance . Bagi saya ini terdengar sama!
XMight
@AlexanderDerck: Tidak yakin mengapa saya tidak membalas Anda sebelumnya; Saya setuju itu tidak jelas, dan akan mencoba mengklarifikasi hal itu.
Jon Skeet
16

Saya harap posting saya membantu mendapatkan pandangan agnostik bahasa dari topik tersebut.

Untuk pelatihan internal kami, saya telah bekerja dengan buku indah "Smalltalk, Objects and Design (Chamond Liu)" dan saya mengulangi contoh-contoh berikut.

Apa yang dimaksud dengan "konsistensi"? Idenya adalah untuk mendesain hierarki tipe tipe aman dengan tipe yang sangat dapat disubstitusikan. Kunci untuk mendapatkan konsistensi ini adalah kesesuaian berbasis sub tipe, jika Anda bekerja dalam bahasa yang diketik secara statis. (Kami akan membahas Prinsip Pergantian Liskov (LSP) pada level tinggi di sini.)

Contoh praktis (kode semu / tidak valid dalam C #):

  • Kovarian: Mari kita asumsikan Burung yang bertelur "secara konsisten" dengan pengetikan statis: Jika jenis Burung bertelur, bukankah subtipe Burung meletakkan subtipe Telur? Misalnya jenis Bebek meletakkan DuckEgg, maka konsistensi diberikan. Mengapa ini konsisten? Karena dalam ungkapan seperti itu: Egg anEgg = aBird.Lay();referensi aBird dapat secara legal diganti oleh Burung atau dengan contoh Bebek. Kami mengatakan jenis kembali adalah kovarian dengan jenis, di mana Lay () didefinisikan. Penggantian subtipe dapat mengembalikan jenis yang lebih khusus. => “Mereka memberikan lebih banyak.”

  • Contravariance: Mari kita asumsikan piano bahwa Pianis dapat bermain "secara konsisten" dengan pengetikan statis: Jika seorang Pianis memainkan Piano, apakah dia dapat memainkan GrandPiano? Bukankah Virtuoso lebih suka memainkan GrandPiano? (Berhati-hatilah; ada twist!) Ini tidak konsisten! Karena dalam ungkapan seperti itu: aPiano.Play(aPianist);aPiano tidak dapat secara legal diganti dengan Piano atau dengan instance GrandPiano! GrandPiano hanya bisa dimainkan oleh Virtuoso, Pianis terlalu umum! GrandPianos harus dapat dimainkan oleh tipe yang lebih umum, maka permainannya konsisten. Kami mengatakan tipe parameter bertentangan dengan tipe, di mana Play () didefinisikan. Override subtipe mungkin menerima tipe yang lebih umum. => "Mereka membutuhkan lebih sedikit."

Kembali ke C #:
Karena C # pada dasarnya adalah bahasa yang diketik secara statis, "lokasi" dari antarmuka jenis yang harus co-atau contravariant (misalnya parameter dan tipe pengembalian), harus ditandai secara eksplisit untuk menjamin penggunaan / pengembangan yang konsisten dari jenis itu , untuk membuat LSP berfungsi dengan baik. Dalam bahasa yang diketik secara dinamis, konsistensi LSP biasanya tidak menjadi masalah, dengan kata lain Anda benar-benar dapat menghilangkan "markup" co-dan contravariant pada antarmuka dan delegasi .Net, jika Anda hanya menggunakan tipe dynamic pada tipe Anda. - Tapi ini bukan solusi terbaik di C # (Anda tidak boleh menggunakan dinamis di antarmuka publik).

Kembali ke teori:
Kesesuaian yang diuraikan (tipe pengembalian kovarian / tipe parameter kontravarian) adalah ideal teoretis (didukung oleh bahasa Emerald dan POOL-1). Beberapa bahasa oop (misalnya Eiffel) memutuskan untuk menerapkan jenis konsistensi lain, khususnya. juga tipe parameter kovarian, karena lebih menggambarkan realitas daripada ideal teoretis. Dalam bahasa yang diketik secara statis, konsistensi yang diinginkan harus sering dicapai dengan penerapan pola desain seperti "pengiriman ganda" dan "pengunjung". Bahasa lain menyediakan apa yang disebut "pengiriman ganda" atau metode multi (ini pada dasarnya memilih kelebihan fungsi pada waktu berjalan , misalnya dengan CLOS) atau mendapatkan efek yang diinginkan dengan menggunakan pengetikan dinamis.

Nico
sumber
Anda mengatakan penggantian subtipe dapat mengembalikan jenis yang lebih khusus . Tapi itu sama sekali tidak benar. Jika Birdmendefinisikan public abstract BirdEgg Lay();, maka Duck : Bird HARUS mengimplementasikan public override BirdEgg Lay(){}Jadi pernyataan Anda yang BirdEgg anEgg = aBird.Lay();memiliki jenis varians sama sekali tidak benar. Menjadi premis dari titik penjelasan, seluruh poin sekarang hilang. Apakah Anda malah mengatakan bahwa kovarians yang ada dalam pelaksanaan di mana DuckEgg secara implisit dilemparkan ke BirdEgg keluar jenis / kembali? Yang manapun, mohon jelaskan kebingungan saya.
Suamere
1
Singkatnya: Anda benar! Maaf bila membingungkan. DuckEgg Lay()bukan pengganti yang valid untuk Egg Lay() dalam C # , dan itulah intinya. C # tidak mendukung tipe pengembalian kovarian, tetapi Java serta C ++ lakukan. Saya agak menggambarkan ideal teoretis dengan menggunakan sintaks mirip C #. Dalam C # Anda perlu membiarkan Bird and Duck mengimplementasikan antarmuka umum, di mana Lay didefinisikan memiliki kovarian (tipe spesifikasi di luar), kemudian semuanya cocok!
Nico
1
Sebagai analog dengan komentar Matt-Klein pada jawaban @ Jon-Skeet, "untuk diri saya di masa depan": takeaway terbaik bagi saya di sini adalah "Mereka memberikan lebih banyak" (spesifik) dan "Mereka membutuhkan lebih sedikit" (spesifik). "Membutuhkan lebih sedikit dan memberikan lebih banyak" adalah mnemonik yang luar biasa! Ini analog dengan pekerjaan di mana saya berharap memerlukan instruksi yang kurang spesifik (permintaan umum) dan belum mengirimkan sesuatu yang lebih spesifik (produk kerja yang sebenarnya). Apa pun urutan subtipe (LSP) tidak terputus.
karfus
@karfus: Terima kasih, tetapi seingat saya, saya mengutip gagasan "Membutuhkan lebih sedikit dan mengirimkan lebih banyak" dari sumber lain. Bisa jadi itu adalah buku Liu yang saya rujuk di atas ... atau bahkan .NET Rock talk. Btw. di Jawa, orang mereduksi mnemonic menjadi "PECS", yang secara langsung berkaitan dengan cara sintaksis untuk mendeklarasikan varian, PECS adalah untuk "Produser extends, Consumer super".
Nico
5

Delegasi konverter membantu saya untuk memahami perbedaannya.

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();
woggles
sumber
0

Varians Co dan Contra adalah hal yang cukup logis. Sistem tipe bahasa memaksa kita untuk mendukung logika kehidupan nyata. Mudah dimengerti dengan contoh.

Kovarian

Misalnya Anda ingin membeli bunga dan Anda memiliki dua toko bunga di kota Anda: toko mawar dan toko bunga aster.

Jika Anda bertanya kepada seseorang "di mana toko bunga?" dan seseorang memberitahumu di mana rose shop, akankah itu baik-baik saja? Ya, karena mawar adalah bunga, jika Anda ingin membeli bunga, Anda bisa membeli bunga mawar. Hal yang sama berlaku jika seseorang menjawab Anda dengan alamat toko bunga aster.

Ini adalah contoh dari kovarians : Anda diijinkan untuk cor A<C>untuk A<B>, di mana Cadalah subclass dari B, jika Amenghasilkan nilai-nilai generik (kembali sebagai akibat dari fungsi). Kovarian adalah tentang produsen, itu sebabnya C # menggunakan kata kunci outuntuk kovarian.

Jenis:

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

Pertanyaannya adalah "di mana toko bunga itu?", Jawabannya adalah "toko mawar di sana":

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

Contravariance

Misalnya Anda ingin memberi hadiah bunga kepada pacar Anda dan pacar Anda menyukai bunga apa pun. Bisakah Anda menganggapnya sebagai orang yang mencintai mawar, atau sebagai orang yang mencintai aster? Ya, karena jika dia menyukai bunga apa pun, dia akan menyukai mawar dan bunga aster.

Ini adalah contoh dari contravariance : Anda diperbolehkan untuk cor A<B>untuk A<C>, di mana Cadalah subclass dari B, jika Amengkonsumsi nilai generik. Contravariance adalah tentang konsumen, itu sebabnya C # menggunakan kata kunci inuntuk contravariance.

Jenis:

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

Anda mempertimbangkan pacar Anda yang menyukai bunga apa pun sebagai seseorang yang mencintai mawar, dan memberinya bunga mawar:

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

Tautan

VadzimV
sumber