Mengapa kovarians dan contravariance tidak mendukung tipe nilai

149

IEnumerable<T>adalah co-varian tetapi tidak mendukung tipe nilai, hanya tipe referensi saja. Kode sederhana di bawah ini berhasil dikompilasi:

IEnumerable<string> strList = new List<string>();
IEnumerable<object> objList = strList;

Tetapi mengubah dari stringmenjadi intakan mendapatkan kesalahan yang dikompilasi:

IEnumerable<int> intList = new List<int>();
IEnumerable<object> objList = intList;

Alasannya dijelaskan dalam MSDN :

Varians hanya berlaku untuk tipe referensi; jika Anda menentukan tipe nilai untuk parameter tipe varian, parameter tipe itu tidak tetap untuk tipe konstruksi yang dihasilkan.

Saya telah mencari dan menemukan bahwa beberapa pertanyaan menyebutkan alasannya adalah tinju antara tipe nilai dan tipe referensi . Tapi itu masih belum menjernihkan pikiran saya mengapa tinju adalah alasannya?

Bisakah seseorang tolong berikan penjelasan yang sederhana dan terperinci mengapa kovarians dan contravariance tidak mendukung tipe nilai dan bagaimana tinju mempengaruhi ini?

cuongle
sumber
3
lihat juga jawaban Eric untuk pertanyaan saya yang serupa: stackoverflow.com/questions/4096299/…
thorn̈
1
kemungkinan duplikat objek cant-convert-value-type-array-to-params
nawfal

Jawaban:

126

Pada dasarnya, varians berlaku ketika CLR dapat memastikan bahwa CLR tidak perlu melakukan perubahan representasional terhadap nilai-nilai tersebut. Referensi semua terlihat sama - sehingga Anda dapat menggunakan IEnumerable<string>sebagai IEnumerable<object>tanpa perubahan representasi; kode asli itu sendiri tidak perlu tahu apa yang Anda lakukan dengan nilai-nilai sama sekali, asalkan infrastruktur telah menjamin bahwa itu pasti akan valid.

Untuk tipe nilai, itu tidak berfungsi - untuk menganggapnya IEnumerable<int>sebagai IEnumerable<object>, kode yang menggunakan urutan harus tahu apakah akan melakukan konversi tinju atau tidak.

Anda mungkin ingin membaca posting blog Eric Lippert tentang representasi dan identitas untuk lebih lanjut tentang topik ini secara umum.

EDIT: Memiliki membaca ulang posting blog Eric sendiri, setidaknya tentang identitas sebagai representasi, meskipun keduanya terkait. Khususnya:

Inilah sebabnya mengapa konversi kovarian dan contravarian antarmuka dan jenis delegasi mengharuskan semua argumen jenis yang berbeda dari jenis referensi. Untuk memastikan bahwa konversi referensi varian selalu mempertahankan identitas, semua konversi yang melibatkan argumen tipe juga harus mempertahankan identitas. Cara termudah untuk memastikan bahwa semua konversi non-sepele pada argumen tipe dipertahankan identitas adalah dengan membatasi mereka untuk menjadi konversi referensi.

Jon Skeet
sumber
5
@CuongLe: Yah itu detail implementasi dalam beberapa hal, tapi itu alasan mendasar untuk pembatasan, saya percaya.
Jon Skeet
2
@ AndréCaron: Posting blog Eric penting di sini - bukan hanya representasi, tetapi juga pelestarian identitas. Tapi pelestarian representasi berarti kode yang dihasilkan tidak perlu peduli sama sekali.
Jon Skeet
1
Justru, identitas tidak dapat dilestarikan karena intbukan merupakan subtipe dari object. Fakta bahwa perubahan representasional diperlukan hanyalah konsekuensi dari ini.
André Caron
3
Bagaimana int bukan subtipe objek? Int32 mewarisi dari System.ValueType, yang mewarisi dari System.Object.
David Klempfner
1
@DavidKlempfner Saya pikir @ AndréCaron berkomentar dengan buruk. Setiap tipe nilai seperti Int32memiliki dua bentuk representasional, "kotak" dan "tanpa kotak". Compiler harus memasukkan kode untuk mengkonversi dari satu formulir ke yang lain, meskipun ini biasanya tidak terlihat pada tingkat kode sumber. Akibatnya, hanya bentuk "kotak" yang dianggap oleh sistem yang mendasari sebagai subtipe object, tetapi kompilator secara otomatis berurusan dengan ini setiap kali suatu tipe nilai ditugaskan ke antarmuka yang kompatibel atau ke sesuatu yang bertipe object.
Steve
10

Mungkin lebih mudah untuk dipahami jika Anda memikirkan representasi yang mendasarinya (meskipun ini benar-benar detail implementasi). Berikut ini adalah kumpulan string:

IEnumerable<string> strings = new[] { "A", "B", "C" };

Anda dapat menganggapnya stringssebagai memiliki representasi berikut:

[0]: referensi string -> "A"
[1]: referensi string -> "B"
[2]: referensi string -> "C"

Ini adalah kumpulan dari tiga elemen, masing-masing menjadi referensi ke string. Anda bisa melemparkan ini ke koleksi objek:

IEnumerable<object> objects = (IEnumerable<object>) strings;

Pada dasarnya itu adalah representasi yang sama kecuali sekarang referensi adalah referensi objek:

[0]: referensi objek -> "A"
[1]: referensi objek -> "B"
[2]: referensi objek -> "C"

Representasinya sama. Referensi hanya diperlakukan secara berbeda; Anda tidak lagi dapat mengakses string.Lengthproperti tetapi Anda masih dapat menelepon object.GetHashCode(). Bandingkan ini dengan koleksi int:

IEnumerable<int> ints = new[] { 1, 2, 3 };
[0]: int = 1
[1]: int = 2
[2]: int = 3

Untuk mengonversikan ini ke IEnumerable<object>data, harus dikonversi dengan memasukkan int:

[0]: referensi objek -> 1
[1]: referensi objek -> 2
[2]: referensi objek -> 3

Konversi ini membutuhkan lebih dari sekadar pemeran.

Martin Liversage
sumber
2
Tinju bukan hanya "detail implementasi". Jenis nilai kotak disimpan dengan cara yang sama dengan objek kelas, dan berperilaku, sejauh yang bisa diketahui dunia luar, seperti objek kelas. Satu-satunya perbedaan adalah bahwa dalam definisi tipe nilai kotak, thismerujuk ke struct yang bidangnya overlay mereka dari objek tumpukan yang menyimpannya, daripada merujuk ke objek yang menyimpannya. Tidak ada cara bersih untuk instance tipe nilai kotak untuk mendapatkan referensi ke objek tumpukan melampirkan.
supercat
7

Saya pikir semuanya dimulai dari definisi LSP(Prinsip Pergantian Liskov), yang meliputi:

jika q (x) adalah properti yang dapat dibuktikan tentang objek x dari tipe T maka q (y) harus benar untuk objek y dari tipe S di mana S adalah subtipe dari T.

Namun tipe nilai, misalnya inttidak bisa menjadi pengganti objectin C#. Buktikan sangat sederhana:

int myInt = new int();
object obj1 = myInt ;
object obj2 = myInt ;
return ReferenceEquals(obj1, obj2);

Ini mengembalikan falsebahkan jika kita menetapkan "referensi" yang sama ke objek.

Tigran
sumber
1
Saya pikir Anda menggunakan prinsip yang benar tetapi tidak ada bukti yang harus dibuat: intbukan subtipe objectsehingga prinsip tersebut tidak berlaku. "Bukti" Anda bergantung pada representasi perantara Integer, yang merupakan subtipe dari objectdan untuk mana bahasa tersebut memiliki konversi tersirat ( object obj1=myInt;sebenarnya diperluas ke object obj1=new Integer(myInt);).
André Caron
Bahasa menangani pengecoran yang benar antara jenis, tetapi perilaku int tidak sesuai dengan yang kita harapkan dari subtipe objek.
Tigran
Seluruh poin saya justru intbukan subtipe dari object. Selain itu, LSP tidak berlaku karena myInt, obj1dan obj2merujuk ke tiga objek berbeda: satu intdan dua (tersembunyi) Integers.
André Caron
22
@ André: C # bukan Java. Kata kunci C # intadalah alias untuk BCL System.Int32, yang sebenarnya merupakan subtipe dari object(alias dari System.Object). Sebenarnya, intkelas dasar adalah System.ValueTypesiapa kelas dasar itu System.Object. Cobalah mengevaluasi ekspresi berikut dan lihat: typeof(int).BaseType.BaseType. Alasan ReferenceEqualsmengembalikan false di sini adalah bahwa intkotak itu menjadi dua kotak yang terpisah, dan identitas masing-masing kotak berbeda untuk kotak lainnya. Jadi dua operasi tinju selalu menghasilkan dua objek yang tidak pernah identik, terlepas dari nilai kotaknya.
Allon Guralnek
@ AllonGuralnek: Setiap tipe nilai (misalnya System.Int32atau List<String>.Enumerator) sebenarnya mewakili dua jenis hal: tipe lokasi penyimpanan, dan tipe objek tumpukan (kadang-kadang disebut "tipe nilai kotak"). Lokasi penyimpanan yang jenisnya berasal System.ValueTypeakan menampung bekas; tumpukan objek yang tipenya juga akan memegang yang terakhir. Dalam sebagian besar bahasa, pemeran pelebaran ada dari yang pertama, dan yang lebih kecil dari yang terakhir ke yang terakhir. Perhatikan bahwa sementara tipe nilai kotak memiliki deskriptor tipe yang sama dengan lokasi penyimpanan tipe nilai, ...
supercat
3

Itu datang ke detail implementasi: Jenis nilai diimplementasikan secara berbeda untuk jenis referensi.

Jika Anda memaksakan tipe nilai untuk diperlakukan sebagai tipe referensi (yaitu kotak mereka, misalnya dengan merujuk mereka melalui antarmuka) Anda bisa mendapatkan varians.

Cara termudah untuk melihat perbedaannya adalah dengan mempertimbangkan Array: sebuah array tipe Nilai disatukan dalam memori secara bersamaan (langsung), di mana sebagai array tipe Referensi hanya memiliki referensi (pointer) yang berdekatan dalam memori; objek yang ditunjuk dialokasikan secara terpisah.

Masalah (terkait) lainnya (*) adalah bahwa (hampir) semua tipe Referensi memiliki representasi yang sama untuk tujuan varians dan banyak kode tidak perlu mengetahui perbedaan antara jenis, sehingga co-dan kontra-varians dimungkinkan (dan mudah diimplementasikan - seringkali hanya dengan menghilangkan pemeriksaan tipe tambahan).

(*) Ini mungkin terlihat masalah yang sama ...

Mark Hurd
sumber