Memahami antarmuka Kovarian dan Kontravarian di C #

87

Saya telah menemukan ini di buku teks yang saya baca di C #, tetapi saya mengalami kesulitan untuk memahaminya, mungkin karena kurangnya konteks.

Apakah ada penjelasan singkat yang bagus tentang apa itu dan apa gunanya di luar sana?

Edit untuk klarifikasi:

Antarmuka kovarian:

interface IBibble<out T>
.
.

Antarmuka kontravarian:

interface IBibble<in T>
.
.
NibblyPig
sumber
3
Ini adalah expalnation IMHO pendek dan baik: blogs.msdn.com/csharpfaq/archive/2010/02/16/...
digEmAll
Semoga bermanfaat: Posting Blog
Krunal
Hmm itu bagus tapi tidak menjelaskan mengapa yang benar-benar membingungkan saya.
NibblyPig

Jawaban:

144

Dengan <out T>, Anda bisa memperlakukan referensi antarmuka sebagai referensi ke atas dalam hierarki.

Dengan <in T> , Anda dapat memperlakukan referensi antarmuka sebagai referensi ke bawah di hiearchy.

Izinkan saya mencoba menjelaskannya dalam istilah yang lebih bahasa Inggris.

Katakanlah Anda mengambil daftar hewan dari kebun binatang, dan Anda berniat untuk memprosesnya. Semua hewan (di kebun binatang Anda) memiliki nama, dan ID unik. Beberapa hewan adalah mamalia, beberapa reptil, beberapa amfibi, beberapa ikan, dll. Tetapi semuanya adalah hewan.

Jadi, dengan daftar hewan Anda (berisi hewan dari berbagai jenis), Anda dapat mengatakan bahwa semua hewan memiliki nama, jadi jelas akan aman untuk mendapatkan nama semua hewan.

Namun, bagaimana jika Anda hanya memiliki daftar ikan, tetapi perlu memperlakukannya seperti hewan, apakah itu berhasil? Secara intuitif, ini seharusnya berfungsi, tetapi di C # 3.0 dan sebelumnya, bagian kode ini tidak akan dikompilasi:

IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish>

Alasan untuk ini adalah bahwa kompilator tidak "tahu" apa yang Anda inginkan, atau bisa , lakukan dengan koleksi binatang setelah Anda mengambilnya. Untuk semua yang diketahui, mungkin ada jalan masukIEnumerable<T> untuk memasukkan kembali objek ke dalam daftar, dan itu berpotensi memungkinkan Anda untuk memasukkan hewan yang bukan ikan, ke dalam koleksi yang seharusnya hanya berisi ikan.

Dengan kata lain, kompilator tidak dapat menjamin bahwa ini tidak diperbolehkan:

animals.Add(new Mammal("Zebra"));

Jadi kompilator langsung menolak untuk mengkompilasi kode Anda. Ini adalah kovarians.

Mari kita lihat pelanggaran.

Karena kebun binatang kita bisa menangani semua hewan, pasti bisa menangani ikan, jadi mari kita coba menambahkan beberapa ikan ke kebun binatang kita.

Di C # 3.0 dan sebelumnya, ini tidak dapat dikompilasi:

List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>
fishes.Add(new Fish("Guppy"));

Di sini, kompiler dapat mengizinkan potongan kode ini, meskipun metode tersebut kembali List<Animal>hanya karena semua ikan adalah hewan, jadi jika kita mengubah tipenya menjadi ini:

List<Animal> fishes = GetAccessToFishes();
fishes.Add(new Fish("Guppy"));

Maka itu akan berhasil, tetapi kompilator tidak dapat menentukan bahwa Anda tidak mencoba melakukan ini:

List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>
Fish firstFist = fishes[0];

Karena daftar tersebut sebenarnya adalah daftar hewan, hal ini tidak diperbolehkan.

Jadi kontra dan kovarian adalah bagaimana Anda memperlakukan referensi objek dan apa yang boleh Anda lakukan dengannya.

Kata kunci indan outdalam C # 4.0 secara khusus menandai antarmuka sebagai satu atau yang lain. Dengan in, Anda diizinkan untuk menempatkan tipe generik (biasanya T) di input -positions, yang berarti argumen metode, dan properti hanya-tulis.

Dengan out, Anda diizinkan untuk menempatkan tipe generik dalam output -positions, yang merupakan nilai kembalian metode, properti hanya-baca, dan parameter metode keluar.

Ini akan memungkinkan Anda melakukan apa yang ingin dilakukan dengan kode:

IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish>
// since we can only get animals *out* of the collection, every fish is an animal
// so this is safe

List<T> memiliki arah masuk dan keluar di T, jadi ini bukan varian bersama atau kontra-varian, tetapi antarmuka yang memungkinkan Anda menambahkan objek, seperti ini:

interface IWriteOnlyList<in T>
{
    void Add(T value);
}

akan memungkinkan Anda melakukan ini:

IWriteOnlyList<Fish> fishes = GetWriteAccessToAnimals(); // still returns
                                                            IWriteOnlyList<Animal>
fishes.Add(new Fish("Guppy")); <-- this is now safe

Berikut beberapa video yang menunjukkan konsep tersebut:

Berikut contohnya:

namespace SO2719954
{
    class Base { }
    class Descendant : Base { }

    interface IBibbleOut<out T> { }
    interface IBibbleIn<in T> { }

    class Program
    {
        static void Main(string[] args)
        {
            // We can do this since every Descendant is also a Base
            // and there is no chance we can put Base objects into
            // the returned object, since T is "out"
            // We can not, however, put Base objects into b, since all
            // Base objects might not be Descendant.
            IBibbleOut<Base> b = GetOutDescendant();

            // We can do this since every Descendant is also a Base
            // and we can now put Descendant objects into Base
            // We can not, however, retrieve Descendant objects out
            // of d, since all Base objects might not be Descendant
            IBibbleIn<Descendant> d = GetInBase();
        }

        static IBibbleOut<Descendant> GetOutDescendant()
        {
            return null;
        }

        static IBibbleIn<Base> GetInBase()
        {
            return null;
        }
    }
}

Tanpa tanda ini, berikut ini dapat dikompilasi:

public List<Descendant> GetDescendants() ...
List<Base> bases = GetDescendants();
bases.Add(new Base()); <-- uh-oh, we try to add a Base to a Descendant

atau ini:

public List<Base> GetBases() ...
List<Descendant> descendants = GetBases(); <-- uh-oh, we try to treat all Bases
                                               as Descendants
Lasse V. Karlsen
sumber
Hmm, bisakah Anda menjelaskan tujuan kovariansi dan kontravarian? Mungkin membantu saya untuk lebih memahaminya.
NibblyPig
1
Lihat bit terakhir, yang dicegah oleh compiler sebelumnya, tujuan dari in dan out adalah untuk mengatakan apa yang dapat Anda lakukan dengan antarmuka (atau tipe) yang aman, sehingga compiler tidak akan menghalangi Anda untuk melakukan hal-hal yang aman. .
Lasse V. Karlsen
Jawaban luar biasa, saya menonton video mereka sangat membantu, dan dikombinasikan dengan contoh Anda, saya sudah menyortirnya sekarang. Hanya satu pertanyaan yang tersisa, dan itulah mengapa 'keluar' dan 'masuk' diperlukan, mengapa studio visual tidak secara otomatis tahu apa yang Anda coba lakukan (atau apa alasan di baliknya)?
NibblyPig
Automagic "Saya melihat apa yang Anda coba lakukan di sana" biasanya tidak disukai ketika harus mendeklarasikan hal-hal seperti kelas, lebih baik meminta programmer secara eksplisit menandai jenisnya. Anda dapat mencoba menambahkan "in" ke kelas yang memiliki metode yang mengembalikan T dan kompilator akan mengeluh. Bayangkan apa yang akan terjadi jika ia secara diam-diam menghapus "dalam" yang sebelumnya ditambahkan secara otomatis untuk Anda.
Lasse V. Karlsen
1
Jika satu kata kunci membutuhkan penjelasan panjang ini, jelas ada sesuatu yang salah. Menurut pendapat saya, C # mencoba menjadi terlalu pintar dalam kasus khusus ini. Meskipun demikian, terima kasih atas penjelasannya yang keren.
rr-
8

Posting ini adalah yang terbaik yang pernah saya baca tentang masalah ini

Singkatnya, kovarian / kontravarian / invarian berhubungan dengan pengecoran tipe otomatis (dari basis ke turunan dan sebaliknya). Jenis cetakan tersebut hanya mungkin jika beberapa jaminan dipatuhi dalam hal tindakan baca / tulis yang dilakukan pada objek yang dicor. Baca postingan untuk lebih jelasnya.

Ben G
sumber
5
Tautan tampak mati. Ini adalah versi yang diarsipkan: web.archive.org/web/20140626123445/http://adamnathan.co.uk/…
si618
1
Saya lebih menyukai penjelasan ini: codepureandsimple.com/…
volkit