Jawaban:
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 Tiger
dapat ditugaskan ke variabel tipe Animal
, jadi tipe ini dikatakan "kompatibel dengan tugas". Mari kita menulis "nilai tipe X
dapat 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 .
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?
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 T
hanya 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 T
dalam 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 :)
Action<T>
masih hanya menggunakan T
dalam posisi output" . Action<T>
tipe kembali batal, bagaimana bisa digunakan T
sebagai output? Atau apakah itu artinya, karena tidak mengembalikan apa pun yang Anda lihat bahwa itu tidak akan pernah melanggar aturan?
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.
Bird
mendefinisikan 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.
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!
extends
, Consumer super
".
Delegasi konverter membantu saya untuk memahami perbedaannya.
delegate TOutput Converter<in TInput, out TOutput>(TInput input);
TOutput
mewakili kovarians di mana metode mengembalikan tipe yang lebih spesifik .
TInput
mewakili 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();
Varians Co dan Contra adalah hal yang cukup logis. Sistem tipe bahasa memaksa kita untuk mendukung logika kehidupan nyata. Mudah dimengerti dengan contoh.
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 C
adalah subclass dari B
, jika A
menghasilkan nilai-nilai generik (kembali sebagai akibat dari fungsi). Kovarian adalah tentang produsen, itu sebabnya C # menggunakan kata kunci out
untuk 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();
}
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 C
adalah subclass dari B
, jika A
mengkonsumsi nilai generik. Contravariance adalah tentang konsumen, itu sebabnya C # menggunakan kata kunci in
untuk 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());