ReadOnlyCollection atau IEnumerable untuk mengekspos koleksi anggota?

124

Apakah ada alasan untuk mengekspos koleksi internal sebagai ReadOnlyCollection daripada IEnumerable jika kode panggilan hanya mengulang koleksi?

class Bar
{
    private ICollection<Foo> foos;

    // Which one is to be preferred?
    public IEnumerable<Foo> Foos { ... }
    public ReadOnlyCollection<Foo> Foos { ... }
}


// Calling code:

foreach (var f in bar.Foos)
    DoSomething(f);

Seperti yang saya lihat, IEnumerable adalah bagian dari antarmuka ReadOnlyCollection dan tidak memungkinkan pengguna untuk mengubah koleksi. Jadi jika antarmuka IEnumberable sudah cukup maka itulah yang akan digunakan. Apakah itu cara yang tepat untuk menalarinya atau apakah saya melewatkan sesuatu?

Terima kasih / Erik

Erik Öjebo
sumber
6
Jika Anda menggunakan .NET 4.5, Anda mungkin ingin mencoba antarmuka koleksi hanya-baca yang baru . Anda masih ingin membungkus koleksi yang dikembalikan dengan a ReadOnlyCollectionjika Anda paranoid, tetapi sekarang Anda tidak terikat pada implementasi tertentu.
StriplingWarrior

Jawaban:

96

Solusi yang lebih modern

Kecuali jika Anda memerlukan koleksi internal yang dapat diubah, Anda dapat menggunakan System.Collections.Immutablepaket, mengubah jenis bidang Anda menjadi koleksi yang tidak dapat diubah, dan kemudian mengeksposnya secara langsung - dengan asumsi Fooitu sendiri tidak dapat diubah, tentu saja.

Jawaban yang diperbarui untuk menjawab pertanyaan secara lebih langsung

Apakah ada alasan untuk mengekspos koleksi internal sebagai ReadOnlyCollection daripada IEnumerable jika kode panggilan hanya mengulang koleksi?

Itu tergantung pada seberapa besar Anda mempercayai kode panggilan. Jika Anda memegang kendali penuh atas semua yang akan memanggil anggota ini dan Anda menjamin bahwa kode tidak akan pernah digunakan:

ICollection<Foo> evil = (ICollection<Foo>) bar.Foos;
evil.Add(...);

Maka pasti, tidak ada salahnya jika Anda langsung mengembalikan koleksinya. Saya biasanya mencoba menjadi sedikit lebih paranoid dari itu.

Demikian juga, seperti yang Anda katakan: jika Anda hanya perlu IEnumerable<T> , lalu mengapa mengikatkan diri Anda pada sesuatu yang lebih kuat?

Jawaban asli

Jika Anda menggunakan .NET 3.5, Anda dapat menghindari membuat salinan dan menghindari transmisi sederhana dengan menggunakan panggilan sederhana ke Lewati:

public IEnumerable<Foo> Foos {
    get { return foos.Skip(0); }
}

(Ada banyak opsi lain untuk membungkus secara sepele - hal yang menyenangkan tentang Skip over Select / Where adalah bahwa tidak ada delegasi yang dieksekusi tanpa tujuan untuk setiap iterasi.)

Jika Anda tidak menggunakan .NET 3.5 Anda dapat menulis pembungkus yang sangat sederhana untuk melakukan hal yang sama:

public static IEnumerable<T> Wrapper<T>(IEnumerable<T> source)
{
    foreach (T element in source)
    {
        yield return element;
    }
}
Jon Skeet
sumber
15
Perhatikan bahwa ada penalti kinerja untuk ini: AFAIK metode Enumerable.Count dioptimalkan untuk Koleksi yang dicor ke dalam IEnumerables, tetapi tidak untuk rentang yang diproduksi oleh Skip (0)
shojtsy
6
-1 Apakah hanya saya atau apakah ini jawaban untuk pertanyaan yang berbeda? Ini berguna untuk mengetahui "solusi" ini, tetapi operasi meminta perbandingan antara mengembalikan ReadOnlyCollection dan IEnumerable. Jawaban ini sudah mengasumsikan Anda ingin mengembalikan IEnumerable tanpa alasan apa pun untuk mendukung keputusan itu.
Zaid Masud
1
Jawabannya menunjukkan casting a ReadOnlyCollectionke ICollectiondan kemudian berjalan dengan Addsukses. Sejauh yang saya tahu, itu tidak mungkin. The evil.Add(...)harus melempar kesalahan. dotnetfiddle.net/GII2jW
Shaun Luttin
1
@ ShaunLuttin: Ya, tepatnya - itu melempar. Tapi dalam jawaban saya, saya tidak melemparkan ReadOnlyCollection- saya melemparkan IEnumerable<T>yang sebenarnya bukan hanya-baca ... itu alih - alih mengeksposReadOnlyCollection
Jon Skeet
1
Aha. Paranoia Anda akan membuat Anda menggunakan a ReadOnlyCollectionalih - alih sebuah IEnumerable, untuk melindungi dari casting jahat.
Shaun Luttin
43

Jika Anda hanya perlu mengulang melalui koleksi:

foreach (Foo f in bar.Foos)

maka mengembalikan IEnumerable sudah cukup.

Jika Anda membutuhkan akses acak ke item:

Foo f = bar.Foos[17];

lalu bungkus dalam ReadOnlyCollection .

Vojislav Stojkovic
sumber
@Vojislav Stojkovic, +1. Apakah nasihat ini masih berlaku sampai sekarang?
w0051977
1
@ w0051977 Itu tergantung pada niat Anda. Menggunakan System.Collections.Immutableseperti yang direkomendasikan Jon Skeet sangat valid ketika Anda mengekspos sesuatu yang tidak akan pernah berubah setelah Anda mendapatkan referensi untuk itu. Di sisi lain, jika Anda mengekspos tampilan read-only dari koleksi yang bisa berubah, maka saran ini masih berlaku, meskipun saya mungkin akan mengeksposnya sebagai IReadOnlyCollection(yang tidak ada saat saya menulis ini).
Vojislav Stojkovic
@VojislavStojkovic tidak dapat Anda gunakan bar.Foos[17]dengan IReadOnlyCollection. Satu-satunya perbedaan adalah Countproperti tambahan . public interface IReadOnlyCollection<out T> : IEnumerable<T>, IEnumerable
Konrad
@Konrad Benar, jika perlu bar.Foos[17], Anda harus mengeksposnya sebagai ReadOnlyCollection<Foo>. Jika Anda ingin mengetahui ukuran dan mengulanginya, tunjukkan sebagai IReadOnlyCollection<Foo>. Jika Anda tidak perlu mengetahui ukurannya, gunakan IEnumerable<Foo>. Ada solusi lain dan detail lainnya, tetapi itu di luar cakupan pembahasan jawaban khusus ini.
Vojislav Stojkovic
31

Jika Anda melakukan ini maka tidak ada yang menghentikan pemanggil Anda mentransmisikan IEnumerable kembali ke ICollection dan kemudian memodifikasinya. ReadOnlyCollection menghilangkan kemungkinan ini, meskipun masih memungkinkan untuk mengakses koleksi yang dapat ditulis melalui refleksi. Jika koleksinya kecil maka cara yang aman dan mudah untuk mengatasi masalah ini adalah dengan mengembalikan salinannya.

Stu Mackellar
sumber
14
Itulah jenis "desain paranoid" yang sangat tidak saya setujui. Saya pikir itu praktik yang buruk untuk memilih semantik yang tidak memadai untuk menegakkan kebijakan.
Vojislav Stojkovic
24
Anda tidak setuju tidak membuatnya salah. Jika saya merancang pustaka untuk distribusi eksternal, saya lebih suka memiliki antarmuka paranoid daripada menangani bug yang ditimbulkan oleh pengguna yang mencoba menyalahgunakan API. Lihat pedoman desain C # di msdn.microsoft.com/en-us/library/k2604h5s(VS.71).aspx
Stu Mackellar
5
Ya, dalam kasus khusus mendesain pustaka untuk distribusi eksternal, masuk akal untuk memiliki antarmuka paranoid untuk apa pun yang Anda ungkapkan. Meski begitu, jika semantik properti Foos adalah akses berurutan, gunakan ReadOnlyCollection lalu kembalikan IEnumerable. Tidak perlu menggunakan semantik yang salah;)
Vojislav Stojkovic
9
Anda tidak perlu melakukan keduanya. Anda dapat mengembalikan iterator hanya-baca tanpa menyalin ...
Jon Skeet
1
@shojtsy Baris kode Anda sepertinya tidak berfungsi. sebagai menghasilkan null . Pemeran membuat pengecualian.
Nick Alexeev
3

Saya menghindari penggunaan ReadOnlyCollection sebanyak mungkin, ini sebenarnya jauh lebih lambat daripada hanya menggunakan Daftar biasa. Lihat contoh ini:

List<int> intList = new List<int>();
        //Use a ReadOnlyCollection around the List
        System.Collections.ObjectModel.ReadOnlyCollection<int> mValue = new System.Collections.ObjectModel.ReadOnlyCollection<int>(intList);

        for (int i = 0; i < 100000000; i++)
        {
            intList.Add(i);
        }
        long result = 0;

        //Use normal foreach on the ReadOnlyCollection
        TimeSpan lStart = new TimeSpan(System.DateTime.Now.Ticks);
        foreach (int i in mValue)
            result += i;
        TimeSpan lEnd = new TimeSpan(System.DateTime.Now.Ticks);
        MessageBox.Show("Speed(ms): " + (lEnd.TotalMilliseconds - lStart.TotalMilliseconds).ToString());
        MessageBox.Show("Result: " + result.ToString());

        //use <list>.ForEach
        lStart = new TimeSpan(System.DateTime.Now.Ticks);
        result = 0;
        intList.ForEach(delegate(int i) { result += i; });
        lEnd = new TimeSpan(System.DateTime.Now.Ticks);
        MessageBox.Show("Speed(ms): " + (lEnd.TotalMilliseconds - lStart.TotalMilliseconds).ToString());
        MessageBox.Show("Result: " + result.ToString());
James Madison
sumber
15
Optimasi prematur. Dengan pengukuran saya, iterasi pada ReadOnlyCollection membutuhkan waktu sekitar 36% lebih lama dari daftar biasa, dengan asumsi Anda tidak melakukan apa pun di dalam for loop. Kemungkinannya adalah, Anda tidak akan pernah melihat perbedaan ini, tetapi jika ini adalah hot-spot dalam kode Anda dan membutuhkan setiap kinerja terakhir, Anda dapat memerasnya, mengapa tidak menggunakan Array sebagai gantinya? Itu akan lebih cepat lagi.
StriplingWarrior
Tolok ukur Anda mengandung kekurangan, Anda sepenuhnya mengabaikan prediksi cabang.
Trojaner
0

Terkadang Anda mungkin ingin menggunakan antarmuka, mungkin karena Anda ingin mengejek koleksi selama pengujian unit. Silakan lihat entri blog saya untuk menambahkan antarmuka Anda sendiri ke ReadonlyCollection dengan menggunakan adaptor.


sumber