Bagaimana array di C # menerapkan sebagian IList <T>?

99

Jadi seperti yang Anda ketahui, array dalam C # mengimplementasikan IList<T>, di antara antarmuka lainnya. Namun, entah bagaimana, mereka melakukan ini tanpa menerapkan properti Count secara publik IList<T>! Array hanya memiliki properti Panjang.

Apakah ini contoh terang-terangan dari C # / .NET yang melanggar aturannya sendiri tentang implementasi antarmuka atau apakah saya melewatkan sesuatu?

MgSam
sumber
2
Tidak ada yang mengatakan bahwa Arraykelas harus ditulis dalam C #!
pengguna541686
Arrayadalah kelas "ajaib", yang tidak dapat diterapkan di C # atau bahasa lain yang menargetkan .net. Tetapi fitur khusus ini tersedia di C #.
CodesInChaos

Jawaban:

81

Jawaban baru dalam terang jawaban Hans

Berkat jawaban yang diberikan oleh Hans, kita bisa melihat implementasinya agak lebih rumit dari yang kita kira. Baik compiler dan CLR berusaha keras untuk memberikan kesan bahwa sebuah tipe array diimplementasikan IList<T>- tetapi varian array membuat ini lebih rumit. Bertentangan dengan jawaban dari Hans, tipe array (berdimensi tunggal, tetap saja berbasis nol) mengimplementasikan koleksi generik secara langsung, karena tipe dari array tertentu tidak System.Array - itu hanya tipe dasar dari array. Jika Anda menanyakan tipe array apa antarmuka yang didukungnya, itu termasuk tipe generik:

foreach (var type in typeof(int[]).GetInterfaces())
{
    Console.WriteLine(type);
}

Keluaran:

System.ICloneable
System.Collections.IList
System.Collections.ICollection
System.Collections.IEnumerable
System.Collections.IStructuralComparable
System.Collections.IStructuralEquatable
System.Collections.Generic.IList`1[System.Int32]
System.Collections.Generic.ICollection`1[System.Int32]
System.Collections.Generic.IEnumerable`1[System.Int32]

Untuk larik berdimensi tunggal dan berbasis nol, sejauh menyangkut bahasanya , larik juga benar-benar diimplementasikan IList<T>. Bagian 12.1.2 dari spesifikasi C # mengatakan demikian. Jadi, apa pun implementasi yang mendasarinya, bahasa tersebut harus berperilaku seolah-olah jenis T[]implementasinya IList<T>sama dengan antarmuka lainnya. Dari perspektif ini, antarmuka yang diimplementasikan dengan beberapa anggota yang secara eksplisit dilaksanakan (seperti Count). Itulah penjelasan terbaik di tingkat bahasa untuk apa yang sedang terjadi.

Perhatikan bahwa ini hanya berlaku untuk larik berdimensi tunggal (dan larik berbasis nol, bukan karena C # sebagai bahasa mengatakan apa pun tentang larik berbasis non-nol). T[,] tidak diimplementasikan IList<T>.

Dari perspektif CLR, sesuatu yang lebih lucu sedang terjadi. Anda tidak bisa mendapatkan pemetaan antarmuka untuk tipe antarmuka generik. Sebagai contoh:

typeof(int[]).GetInterfaceMap(typeof(ICollection<int>))

Memberikan pengecualian untuk:

Unhandled Exception: System.ArgumentException: Interface maps for generic
interfaces on arrays cannot be retrived.

Jadi kenapa anehnya? Yah, saya percaya itu benar-benar karena kovarians array, yang merupakan kutil dalam sistem tipe, IMO. Meskipun IList<T>ini tidak kovarian (dan tidak dapat dengan aman), array kovarians memungkinkan ini bekerja:

string[] strings = { "a", "b", "c" };
IList<object> objects = strings;

... yang membuatnya terlihat seperti typeof(string[])alat IList<object>, padahal sebenarnya tidak.

Spesifikasi CLI (ECMA-335) partisi 1, bagian 8.7.1, memiliki ini:

Jenis tanda tangan T kompatibel dengan jenis tanda tangan U jika dan hanya jika setidaknya salah satu dari yang berikut ini berlaku

...

T adalah array peringkat-1 berbasis nol V[], dan Uadalah IList<W>, dan V kompatibel dengan elemen array-dengan W.

(Itu tidak benar-benar menyebutkan ICollection<W>atau IEnumerable<W>yang saya percaya adalah bug dalam spesifikasi.)

Untuk non-varians, spesifikasi CLI sejalan dengan spesifikasi bahasa secara langsung. Dari bagian 8.9.1 partisi 1:

Selain itu, vektor yang dibuat dengan tipe elemen T, mengimplementasikan antarmuka System.Collections.Generic.IList<U>, di mana U: = T. (§8.7)

( Vektor adalah larik berdimensi tunggal dengan basis nol.)

Sekarang dalam hal detail implementasi , jelas CLR melakukan beberapa pemetaan yang funky untuk menjaga kompatibilitas penugasan di sini: ketika a string[]diminta untuk implementasi ICollection<object>.Count, CLR tidak dapat menanganinya dengan cara yang cukup normal. Apakah ini dihitung sebagai implementasi antarmuka eksplisit? Saya pikir masuk akal untuk memperlakukannya seperti itu, karena kecuali Anda meminta pemetaan antarmuka secara langsung, itu selalu berperilaku seperti itu dari perspektif bahasa.

Tentang apa ICollection.Count?

Sejauh ini saya telah berbicara tentang antarmuka generik, tetapi kemudian ada antarmuka non-generik ICollectiondengan Countpropertinya. Kali ini kita bisa mendapatkan pemetaan antarmuka, dan ternyata antarmuka diimplementasikan langsung oleh System.Array. Dokumentasi untuk ICollection.Countimplementasi properti dalam Arraystatus yang diimplementasikan dengan implementasi antarmuka eksplisit.

Jika ada yang dapat memikirkan cara di mana penerapan antarmuka eksplisit semacam ini berbeda dari penerapan antarmuka eksplisit "normal", saya akan dengan senang hati memeriksanya lebih jauh.

Jawaban lama seputar implementasi antarmuka eksplisit

Terlepas dari hal di atas, yang lebih rumit karena pengetahuan tentang array, Anda masih dapat melakukan sesuatu dengan efek terlihat yang sama melalui implementasi antarmuka eksplisit .

Berikut contoh mandiri sederhana:

public interface IFoo
{
    void M1();
    void M2();
}

public class Foo : IFoo
{
    // Explicit interface implementation
    void IFoo.M1() {}

    // Implicit interface implementation
    public void M2() {}
}

class Test    
{
    static void Main()
    {
        Foo foo = new Foo();

        foo.M1(); // Compile-time failure
        foo.M2(); // Fine

        IFoo ifoo = foo;
        ifoo.M1(); // Fine
        ifoo.M2(); // Fine
    }
}
Jon Skeet
sumber
5
Saya pikir Anda akan mendapatkan kegagalan waktu kompilasi pada foo.M1 (); bukan foo.M2 ();
Kevin Aenmey
Tantangannya di sini adalah memiliki kelas non-generik, seperti array, mengimplementasikan tipe antarmuka generik, seperti IList <>. Cuplikan Anda tidak melakukan itu.
Hans Passant
@HansPassant: Sangat mudah untuk membuat kelas non-generik mengimplementasikan tipe antarmuka generik. Sepele. Saya tidak melihat indikasi apa pun bahwa itulah yang ditanyakan OP.
Jon Skeet
4
@JohnSaunders: Sebenarnya, saya tidak percaya semua itu tidak akurat sebelumnya. Saya telah mengembangkannya banyak, dan menjelaskan mengapa CLR memperlakukan array dengan aneh - tetapi saya yakin jawaban saya tentang implementasi antarmuka eksplisit cukup benar sebelumnya. Dalam hal apa Anda tidak setuju? Sekali lagi, detail akan berguna (mungkin dalam jawaban Anda sendiri, jika sesuai).
Jon Skeet
1
@RBT: Ya, meskipun ada perbedaan dalam penggunaan Countitu baik-baik saja - tetapi Addakan selalu melempar, karena array berukuran tetap.
Jon Skeet
86

Jadi seperti yang Anda ketahui, array dalam C # mengimplementasikan IList<T>, di antara antarmuka lainnya

Ya, erm tidak, tidak juga. Ini adalah deklarasi untuk kelas Array dalam kerangka .NET 4:

[Serializable, ComVisible(true)]
public abstract class Array : ICloneable, IList, ICollection, IEnumerable, 
                              IStructuralComparable, IStructuralEquatable
{
    // etc..
}

Ini mengimplementasikan System.Collections.IList, bukan System.Collections.Generic.IList <>. Itu tidak bisa, Array tidak generik. Hal yang sama berlaku untuk antarmuka IEnumerable <> dan ICollection <> generik.

Tapi CLR menciptakan tipe array konkret dengan cepat, sehingga secara teknis bisa membuat satu yang mengimplementasikan antarmuka ini. Tetapi, ini bukan perkaranya. Coba kode ini misalnya:

using System;
using System.Collections.Generic;

class Program {
    static void Main(string[] args) {
        var goodmap = typeof(Derived).GetInterfaceMap(typeof(IEnumerable<int>));
        var badmap = typeof(int[]).GetInterfaceMap(typeof(IEnumerable<int>));  // Kaboom
    }
}
abstract class Base { }
class Derived : Base, IEnumerable<int> {
    public IEnumerator<int> GetEnumerator() { return null; }
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); }
}

Panggilan GetInterfaceMap () gagal untuk tipe array konkret dengan "Interface not found". Namun cast ke IEnumerable <> bekerja tanpa masalah.

Ini adalah cara mengetik dukun seperti bebek. Ini adalah jenis pengetikan yang sama yang menciptakan ilusi bahwa setiap jenis nilai berasal dari ValueType yang berasal dari Object. Baik compiler dan CLR memiliki pengetahuan khusus tentang tipe array, seperti halnya tipe nilai. Kompilator melihat upaya Anda untuk mentransmisikan ke IList <> dan berkata "oke, saya tahu bagaimana melakukan itu!". Dan memancarkan instruksi IL castclass. CLR tidak memiliki masalah dengannya, ia tahu bagaimana menyediakan implementasi IList <> yang bekerja pada objek array yang mendasarinya. Ini memiliki pengetahuan built-in tentang kelas System.SZArrayHelper yang tersembunyi, pembungkus yang benar-benar mengimplementasikan antarmuka ini.

Yang tidak dilakukan secara eksplisit seperti klaim semua orang, properti Hitung yang Anda tanyakan terlihat seperti ini:

    internal int get_Count<T>() {
        //! Warning: "this" is an array, not an SZArrayHelper. See comments above
        //! or you may introduce a security hole!
        T[] _this = JitHelpers.UnsafeCast<T[]>(this);
        return _this.Length;
    }

Ya, Anda pasti bisa menyebut komentar itu "melanggar aturan" :) Jika tidak, komentar itu sangat berguna. Dan tersembunyi dengan sangat baik, Anda dapat memeriksanya di SSCLI20, distribusi sumber bersama untuk CLR. Telusuri "IList" untuk melihat di mana substitusi tipe berlangsung. Tempat terbaik untuk melihatnya beraksi adalah metode clr / src / vm / array.cpp, GetActualImplementationForArrayGenericIListMethod ().

Substitusi semacam ini di CLR cukup ringan dibandingkan dengan apa yang terjadi dalam proyeksi bahasa di CLR yang memungkinkan penulisan kode terkelola untuk WinRT (alias Metro). Hampir semua jenis inti .NET diganti di sana. IList <> memetakan ke IVector <> misalnya, tipe yang sepenuhnya tidak terkelola. Itu sendiri merupakan substitusi, COM tidak mendukung tipe generik.

Nah, itulah tadi sekilas tentang apa yang terjadi di balik tirai. Ini bisa menjadi laut yang sangat tidak nyaman, aneh dan asing dengan naga yang hidup di ujung peta. Ini bisa sangat berguna untuk membuat Earth datar dan membuat model gambar berbeda dari apa yang sebenarnya terjadi dalam kode terkelola. Memetakannya ke jawaban favorit semua orang nyaman seperti itu. Yang tidak berfungsi dengan baik untuk tipe nilai (jangan mutasi struct!) Tapi yang ini sangat tersembunyi. Kegagalan metode GetInterfaceMap () adalah satu-satunya kebocoran dalam abstraksi yang dapat saya pikirkan.

Hans Passant
sumber
1
Itu adalah deklarasi untuk Arraykelas tersebut, yang bukan merupakan tipe dari sebuah array. Ini adalah tipe dasar untuk sebuah array. Sebuah array berdimensi tunggal di C # bisa diterapkan IList<T>. Dan tipe non-generik pasti dapat mengimplementasikan antarmuka generik ... yang berfungsi karena ada banyak tipe yang berbeda - typeof(int[])! = Typeof (string []) , so typeof (int []) `implement IList<int>, dan typeof(string[])implement IList<string>.
Jon Skeet
2
@HansPassant: Tolong jangan berasumsi bahwa saya akan downvote sesuatu hanya karena itu mengganggu . Faktanya tetap bahwa kedua penalaran Anda melalui Array(yang seperti yang Anda tunjukkan, adalah kelas abstrak, jadi tidak mungkin menjadi tipe sebenarnya dari objek array) dan kesimpulan (yang tidak diimplementasikan IList<T>) adalah IMO yang salah. The cara di mana ia menerapkan IList<T>tidak biasa dan menarik, saya akan setuju - tapi itu murni sebuah implementasi rinci. Untuk mengklaim bahwa T[]tidak menerapkan IList<T>adalah IMO yang menyesatkan. Ini bertentangan dengan spesifikasi dan semua perilaku yang diamati.
Jon Skeet
6
Nah, yakin menurut Anda itu tidak benar. Anda tidak dapat membuatnya cocok dengan apa yang Anda baca di spesifikasi. Silakan melihatnya dengan cara Anda sendiri, tetapi Anda tidak akan pernah mendapatkan penjelasan yang baik mengapa GetInterfaceMap () gagal. "Sesuatu yang funky" bukanlah sebuah wawasan. Saya memakai kacamata implementasi: tentu saja gagal, ini adalah pengetikan quack-like-a-duck, tipe array konkret tidak benar-benar mengimplementasikan ICollection <>. Tidak ada yang funky tentang itu. Mari kita simpan di sini, kita tidak akan pernah setuju.
Hans Passant
4
Bagaimana dengan setidaknya menghapus logika palsu yang tidak dapat diterapkan oleh array klaim IList<T> karena Array tidak? Logika itu adalah sebagian besar dari apa yang tidak saya setujui. Di luar itu, saya pikir kita harus menyetujui definisi tentang apa artinya suatu tipe untuk mengimplementasikan antarmuka: menurut pendapat saya, tipe array menampilkan semua fitur yang dapat diamati dari tipe yang diimplementasikan IList<T>, selain GetInterfaceMapping. Sekali lagi, bagaimana hal itu dicapai kurang penting bagi saya, sama seperti saya baik-baik saja dengan mengatakan bahwa System.Stringitu tidak dapat diubah, meskipun detail penerapannya berbeda.
Jon Skeet
1
Bagaimana dengan compiler C ++ CLI? Yang itu jelas mengatakan, "Saya tidak tahu bagaimana melakukan itu!" dan mengeluarkan kesalahan. Ini membutuhkan pemeran eksplisit IList<T>agar berfungsi.
Tobias Knauss
21

IList<T>.Countdiimplementasikan secara eksplisit :

int[] intArray = new int[10];
IList<int> intArrayAsList = (IList<int>)intArray;
Debug.Assert(intArrayAsList.Count == 10);

Ini dilakukan agar ketika Anda memiliki variabel array sederhana, Anda tidak memiliki keduanya Countdan Lengthlangsung tersedia.

Secara umum, implementasi antarmuka eksplisit digunakan ketika Anda ingin memastikan bahwa suatu tipe dapat digunakan dengan cara tertentu, tanpa memaksa semua konsumen tipe untuk memikirkannya seperti itu.

Edit : Ups, ingatan buruk di sana. ICollection.Countditerapkan secara eksplisit. Generik IList<T>ditangani seperti yang dijelaskan Hans di bawah ini .

dlev
sumber
4
Namun, membuat saya bertanya-tanya, mengapa mereka tidak menyebut properti Hitung saja, bukan Panjang? Larik adalah satu-satunya koleksi umum yang memiliki properti seperti itu (kecuali Anda menghitungnya string).
Tim S.
5
@TimS Pertanyaan yang bagus (dan yang jawabannya saya tidak tahu.) Saya akan berspekulasi bahwa alasannya adalah karena "hitung" menyiratkan sejumlah item, sedangkan array memiliki "panjang" yang tidak dapat diubah segera setelah dialokasikan ( terlepas dari elemen mana yang memiliki nilai.)
dlev
1
@TimS Saya pikir itu dilakukan karena ICollectionmendeklarasikan Count, dan akan lebih membingungkan jika tipe dengan kata "collection" di dalamnya tidak menggunakan Count:). Selalu ada trade-off dalam membuat keputusan ini.
dlev
4
@JohnSaunders: Dan lagi ... hanya suara negatif tanpa informasi berguna.
Jon Skeet
5
@ JohnSaunders: Saya masih belum yakin. Hans telah merujuk pada implementasi SSCLI, tetapi juga mengklaim bahwa tipe array bahkan tidak diimplementasikan IList<T>, meskipun spesifikasi bahasa dan CLI tampak sebaliknya. Saya berani mengatakan cara implementasi antarmuka bekerja di bawah sampul mungkin rumit, tetapi itulah yang terjadi dalam banyak situasi. Apakah Anda juga akan memberikan suara negatif kepada seseorang yang mengatakan bahwa System.Stringitu tidak dapat diubah, hanya karena cara kerja internal dapat berubah? Untuk semua tujuan praktis - dan tentunya sejauh menyangkut bahasa C # - ini adalah impl.
Jon Skeet
10

Implementasi antarmuka eksplisit . Singkatnya, Anda menyatakan suka void IControl.Paint() { }atau int IList<T>.Count { get { return 0; } }.

Tim S.
sumber
2

Ini tidak berbeda dengan implementasi antarmuka eksplisit dari IList. Hanya karena Anda mengimplementasikan antarmuka tidak berarti anggotanya perlu muncul sebagai anggota kelas. Ini tidak menerapkan properti Count, hal itu tidak mengekspos pada X [].

nitzmahone.dll
sumber
1

Dengan sumber referensi yang tersedia:

//----------------------------------------------------------------------------------------
// ! READ THIS BEFORE YOU WORK ON THIS CLASS.
// 
// The methods on this class must be written VERY carefully to avoid introducing security holes.
// That's because they are invoked with special "this"! The "this" object
// for all of these methods are not SZArrayHelper objects. Rather, they are of type U[]
// where U[] is castable to T[]. No actual SZArrayHelper object is ever instantiated. Thus, you will
// see a lot of expressions that cast "this" "T[]". 
//
// This class is needed to allow an SZ array of type T[] to expose IList<T>,
// IList<T.BaseType>, etc., etc. all the way up to IList<Object>. When the following call is
// made:
//
//   ((IList<T>) (new U[n])).SomeIListMethod()
//
// the interface stub dispatcher treats this as a special case, loads up SZArrayHelper,
// finds the corresponding generic method (matched simply by method name), instantiates
// it for type <T> and executes it. 
//
// The "T" will reflect the interface used to invoke the method. The actual runtime "this" will be
// array that is castable to "T[]" (i.e. for primitivs and valuetypes, it will be exactly
// "T[]" - for orefs, it may be a "U[]" where U derives from T.)
//----------------------------------------------------------------------------------------
sealed class SZArrayHelper {
    // It is never legal to instantiate this class.
    private SZArrayHelper() {
        Contract.Assert(false, "Hey! How'd I get here?");
    }

    /* ... snip ... */
}

Khususnya bagian ini:

antarmuka stub dispatcher memperlakukan ini sebagai kasus khusus , memuat SZArrayHelper, menemukan metode generik yang sesuai (hanya cocok dengan nama metode) , membuat instance untuk jenis dan menjalankannya.

(Penekanan saya)

Sumber (gulir ke atas).

AnorZaken
sumber