Praktik Terbaik Mengembalikan Objek Hanya-Baca

11

Saya memiliki pertanyaan "praktik terbaik" tentang OOP di C # (tapi itu semacam berlaku untuk semua bahasa).

Pertimbangkan memiliki kelas pustaka dengan objek yang akan diekspos ke publik, katakanlah melalui accessor properti, tetapi kami tidak ingin publik (orang yang menggunakan kelas pustaka ini) mengubahnya.

class A
{
    // Note: List is just example, I am interested in objects in general.
    private List<string> _items = new List<string>() { "hello" }

    public List<string> Items
    {
        get
        {
            // Option A (not read-only), can be modified from outside:
            return _items;

            // Option B (sort-of read-only):
            return new List<string>( _items );

            // Option C, must change return type to ReadOnlyCollection<string>
            return new ReadOnlyCollection<string>( _items );
        }
    }
}

Jelas pendekatan terbaik adalah "opsi C", tetapi sangat sedikit objek memiliki varian ReadOnly (dan tentu saja tidak ada kelas yang ditentukan pengguna memilikinya).

Jika Anda adalah pengguna kelas A, apakah Anda mengharapkan perubahan

List<string> someList = ( new A() ).Items;

merambat ke objek asli (A)? Atau tidak apa-apa mengembalikan klon asalkan ditulis begitu dalam komentar / dokumentasi? Saya pikir pendekatan klon ini mungkin menyebabkan bug yang sulit dilacak.

Saya ingat bahwa di C ++ kita bisa mengembalikan objek const dan Anda hanya bisa memanggil metode yang ditandai sebagai const di atasnya. Saya kira tidak ada fitur / pola seperti itu di C #? Jika tidak, mengapa mereka tidak memasukkannya? Saya percaya ini disebut benar const.

Tetapi sekali lagi pertanyaan utama saya adalah tentang "apa yang Anda harapkan" atau bagaimana menangani Opsi A vs B.

Jangan berhenti belajar
sumber
2
Lihat artikel ini tentang pratinjau koleksi abadi baru ini untuk .NET 4.5: blogs.msdn.com/b/bclteam/archive/2012/12/18/… Anda sudah bisa mendapatkannya melalui NuGet.
Kristof Claes

Jawaban:

10

Selain jawaban Jesse, yang merupakan solusi terbaik untuk koleksi: ide umumnya adalah merancang antarmuka publik A Anda dengan cara sehingga hanya mengembalikannya

  • tipe nilai (seperti int, dobel, struct)

  • objek yang tidak dapat diubah (seperti "string", atau kelas yang ditentukan pengguna yang hanya menawarkan metode baca-saja, seperti kelas A Anda)

  • (hanya jika Anda harus): objek salinan, seperti "Opsi B" Anda menunjukkan

IEnumerable<immutable_type_here> tidak berubah dengan sendirinya, jadi ini hanya kasus khusus di atas.

Doc Brown
sumber
19

Juga perhatikan Anda harus pemrograman ke antarmuka, dan pada umumnya, apa yang ingin Anda kembalikan dari properti itu adalah IEnumerable<string>. A List<string>sedikit tidak-tidak karena itu umumnya bisa berubah dan saya akan melangkah lebih jauh dengan mengatakan bahwa ReadOnlyCollection<string>sebagai pelaksana ICollection<string>terasa seperti itu melanggar Prinsip Pergantian Liskov.

Jesse C. Slicer
sumber
6
+1, menggunakan IEnumerable<string>persis apa yang diinginkan seseorang di sini.
Doc Brown
2
Di .Net 4.5, Anda juga bisa menggunakan IReadOnlyList<T>.
svick
3
Catatan untuk pihak lain yang berkepentingan. Ketika mempertimbangkan accessor properti: jika Anda hanya mengembalikan IEnumerable <string> bukan List <string> dengan melakukan return _list (+ implisit cast ke IEnumerable) maka daftar ini dapat dilemparkan kembali ke List <string> dan diubah dan perubahan akan merambat ke objek utama. Anda dapat mengembalikan Daftar <string> (_list) baru (+ pemeran implisit berikutnya ke IEnumerable) yang akan melindungi daftar internal. Lebih lanjut tentang ini dapat ditemukan di sini: stackoverflow.com/questions/4056013/…
NeverStopLearning
1
Apakah lebih baik untuk mensyaratkan bahwa setiap penerima koleksi yang perlu mengaksesnya dengan indeks harus siap untuk menyalin data ke dalam jenis yang memfasilitasi itu, atau lebih baik mensyaratkan bahwa kode mengembalikan koleksi pasokan itu dalam bentuk yang dapat diakses dengan indeks? Kecuali pemasok kemungkinan memilikinya dalam bentuk yang tidak memfasilitasi akses dengan indeks, saya akan mempertimbangkan bahwa nilai membebaskan penerima dari kebutuhan untuk membuat salinan berlebihan mereka sendiri akan melebihi nilai membebaskan pemasok dari keharusan memasok formulir akses-acak (mis. hanya-baca IList<T>)
supercat
2
Satu hal yang saya tidak suka tentang IEnumerable adalah bahwa Anda tidak pernah tahu apakah itu berjalan sebagai coroutine atau apakah itu hanya objek data. Jika dijalankan sebagai coroutine, itu bisa menghasilkan bug yang halus jadi ini baik untuk diketahui kadang-kadang. Inilah sebabnya saya cenderung lebih suka ReadOnlyCollection atau IReadOnlyList dll.
Steve Vermeulen
3

Saya mengalami masalah yang sama beberapa waktu lalu dan di bawah ini adalah solusi saya, tetapi itu benar-benar hanya berfungsi jika Anda mengontrol perpustakaan juga:


Mulailah dengan kelas Anda A

public class A
{
    private int _n;
    public int N
    {
        get { return _n; }
        set { _n = value; }
    }
}

Kemudian dapatkan antarmuka dari ini dengan hanya mendapatkan sebagian dari properti (dan metode membaca apa pun) dan memiliki A mengimplementasikannya

public interface IReadOnlyA
{
    public int N { get; }
}
public class A : IReadOnlyA
{
    private int _n;
    public int N
    {
        get { return _n; }
        set { _n = value; }
    }
}

Maka tentu saja ketika Anda ingin menggunakan versi read only, gips sederhana sudah cukup. Ini adalah apa yang Anda solusi C, sungguh, tapi saya pikir saya akan menawarkan metode yang saya capai

Andy Hunt
sumber
2
Masalah dengan ini adalah, bahwa itu mungkin untuk melemparkannya kembali ke versi yang bisa diubah. Dan pelarutannya terhadap LSP, bacause state yang dapat diamati melalui metode kelas dasar (dalam hal ini antarmuka) dapat diubah dengan metode baru dari subkelas. Tapi itu setidaknya mengomunikasikan niat, bahkan jika itu tidak menegakkannya.
user470365
1

Bagi siapa pun yang menemukan pertanyaan ini dan masih tertarik dengan jawabannya - sekarang ada opsi lain yang terbuka ImmutableArray<T>. Inilah beberapa poin mengapa saya pikir lebih baik dari itu IEnumerable<T>atau ReadOnlyCollection<T>:

  • itu dengan jelas menyatakan bahwa itu tidak dapat diubah (API ekspresif)
  • itu jelas menyatakan bahwa koleksi sudah terbentuk (dengan kata lain itu tidak diinisialisasi malas dan tidak apa-apa untuk mengulanginya berkali-kali)
  • jika jumlah item diketahui, itu tidak menyembunyikan pengetahuan seperti IEnumerable<T>itu
  • karena klien tidak tahu apa implementasi IEnumerableatau IReadOnlyCollectdia tidak bisa berasumsi bahwa itu tidak pernah berubah (dengan kata lain tidak ada jaminan bahwa item koleksi tidak berubah sementara referensi ke koleksi itu tetap tidak berubah)

Juga jika APi perlu memberi tahu klien tentang perubahan dalam koleksi saya akan mengekspos suatu peristiwa sebagai anggota yang terpisah.

Perhatikan bahwa saya tidak mengatakan itu IEnumerable<T>buruk secara umum. Saya masih menggunakannya ketika meneruskan koleksi sebagai argumen atau mengembalikan koleksi yang diinisialisasi dengan malas (dalam kasus khusus ini masuk akal untuk menyembunyikan jumlah item karena belum diketahui).

Pavels Ahmadulins
sumber