Najlepsze praktyki dotyczące zwracania obiektu tylko do odczytu

11

Mam pytanie dotyczące „najlepszych praktyk” na temat OOP w C # (ale w pewnym sensie dotyczy to wszystkich języków).

Zastanów się nad klasą biblioteki z obiektem, który ma być udostępniony publicznie, powiedzmy za pośrednictwem modułu dostępu do właściwości, ale nie chcemy, aby publiczność (ludzie używający tej klasy biblioteki) to zmieniła.

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 );
        }
    }
}

Oczywiście najlepszym podejściem jest „opcja C”, ale bardzo niewiele obiektów ma wariant ReadOnly (a na pewno nie ma w nim klas zdefiniowanych przez użytkownika).

Gdybyś był użytkownikiem klasy A, czy spodziewałbyś się zmian

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

propagować do oryginalnego obiektu (A)? Czy może w porządku jest zwrócić klon pod warunkiem, że został napisany w komentarzach / dokumentacji? Myślę, że takie podejście do klonowania może prowadzić do dość trudnych do wyśledzenia błędów.

Pamiętam, że w C ++ mogliśmy zwracać obiekty const i można było wywoływać tylko metody oznaczone jako const. Myślę, że nie ma takiej funkcji / wzoru w C #? Jeśli nie, dlaczego mieliby tego nie uwzględniać? Uważam, że nazywa się to stałą poprawnością.

Ale znowu moje główne pytanie dotyczy „czego byś się spodziewał” lub jak poradzić sobie z opcją A w porównaniu z B.

Nigdy nie przestawaj się uczyć
źródło
2
Zapoznaj się z tym artykułem na temat podglądu nowych niezmiennych kolekcji dla platformy .NET 4.5: blogs.msdn.com/b/bclteam/archive/2012/12/18/... Możesz je już uzyskać za pośrednictwem NuGet.
Kristof Claes

Odpowiedzi:

10

Oprócz odpowiedzi @ Jesse, która jest najlepszym rozwiązaniem dla kolekcji: ogólną ideą jest zaprojektowanie publicznego interfejsu A w taki sposób, aby zwracał tylko

  • typy wartości (takie jak int, double, struct)

  • niezmienne obiekty (takie jak „string” lub klasy zdefiniowane przez użytkownika, które oferują tylko metody tylko do odczytu, podobnie jak klasa A)

  • (tylko jeśli musisz): kopie obiektów, jak pokazuje „Opcja B”

IEnumerable<immutable_type_here> jest niezmienny sam w sobie, więc jest to tylko szczególny przypadek powyższego.

Doktor Brown
źródło
19

Pamiętaj też, że powinieneś programować interfejsy i ogólnie rzecz biorąc, to, co chcesz zwrócić z tej właściwości, to IEnumerable<string>. Jest List<string>to trochę nie-nie, ponieważ jest ogólnie zmienne i posunę się tak daleko, aby powiedzieć, że ReadOnlyCollection<string>jako realizator ICollection<string>czuje się, jakby narusza zasadę substytucji Liskowa.

Jesse C. Slicer
źródło
6
+1, użycie IEnumerable<string>jest dokładnie tym, czego tu chcemy.
Doc Brown
2
W .Net 4.5 możesz także użyć IReadOnlyList<T>.
svick
3
Uwaga dla innych zainteresowanych stron. Rozważając moduł dostępu do właściwości: jeśli zwrócisz IEnumerable <łańcuch> zamiast List <łańcuch> wykonując return _list (+ niejawne rzutowanie na IEnumerable), wówczas tę listę można przerzucić z powrotem na List <łańcuch> i zmienić, a zmiany zostaną propagować do głównego obiektu. Możesz zwrócić nową List <łańcuch> (_list) (+ kolejne niejawne rzutowanie do IEnumerable), co ochroni wewnętrzną listę. Więcej na ten temat można znaleźć tutaj: stackoverflow.com/questions/4056013/…
NeverStopLearning
1
Czy lepiej jest wymagać, aby każdy odbiorca kolekcji, który musi uzyskać do niej dostęp za pomocą indeksu, był przygotowany do skopiowania danych do typu, który to ułatwia, czy też lepiej jest wymagać, aby kod zwracający kolekcję dostarczył ją w formie, która jest dostępny według indeksu? O ile dostawca prawdopodobnie nie będzie go miał w formie, która nie ułatwia dostępu według indeksu, uważam, że wartość uwolnienia odbiorców od konieczności sporządzenia własnej zbędnej kopii przekroczy wartość zwolnienia dostawcy z obowiązku dostarczenia formularz o swobodnym dostępie (np. tylko do odczytu IList<T>)
supercat
2
Jedną rzeczą, która mi się nie podoba w IEnumerable jest to, że nigdy nie wiadomo, czy działa jako coroutine, czy jest to po prostu obiekt danych. Jeśli działa jak korupcja, może powodować subtelne błędy, więc czasami warto o tym wiedzieć. Właśnie dlatego wolę ReadOnlyCollection lub IReadOnlyList itp.
Steve Vermeulen
3

Napotkałem podobny problem jakiś czas temu i poniżej było moje rozwiązanie, ale tak naprawdę działa tylko wtedy, gdy kontrolujesz bibliotekę:


Zacznij od swojej klasy A

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

Następnie uzyskaj z tego interfejs zawierający tylko część właściwości (i dowolne metody odczytu) i zaimplementuj to w A.

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

Oczywiście, jeśli chcesz użyć wersji tylko do odczytu, wystarczy rzut prosty. Tak właśnie wygląda rozwiązanie C, ale pomyślałem, że zaproponuję metodę, dzięki której go osiągnąłem

Andy Hunt
źródło
2
Problem polega na tym, że możliwe jest przywrócenie go do wersji modyfikowalnej. Naruszenie LSP, ponieważ stan obserwowany za pomocą metod klasy bazowej (w tym przypadku interfejsu) można zmienić za pomocą nowych metod z podklasy. Ale przynajmniej komunikuje zamiar, nawet jeśli go nie wymusza.
user470365
1

Dla każdego, kto znajdzie to pytanie i nadal jest zainteresowany odpowiedzią - teraz jest inna opcja, która ujawnia ImmutableArray<T>. Są kilka punktów, dlaczego uważam, że lepiej wtedy IEnumerable<T>albo ReadOnlyCollection<T>:

  • wyraźnie stwierdza, że ​​jest niezmienny (ekspresyjny interfejs API)
  • wyraźnie stwierdza, że ​​kolekcja jest już utworzona (innymi słowy, nie jest inicjowana leniwie i można ją wielokrotnie powtarzać)
  • jeżeli liczba sztuk jest znana, to nie ukrywa, że wiedzy, pojmowania, jak IEnumerable<T>robi
  • ponieważ klient nie wie, jaka jest implementacja IEnumerablelub IReadOnlyCollectnie może założyć, że nigdy się nie zmienia (innymi słowy, nie ma gwarancji, że elementy kolekcji nie zostaną zmienione, a odniesienie do tej kolekcji pozostanie niezmienione)

Również, jeśli APi będzie musiał powiadomić klienta o zmianach w kolekcji, odsłoniłbym zdarzenie jako osobny członek.

Zauważ, że nie twierdzę, że IEnumerable<T>ogólnie jest źle. Nadal używałbym go podczas przekazywania kolekcji jako argumentu lub zwracania leniwie zainicjowanej kolekcji (w tym konkretnym przypadku sensowne jest ukrywanie liczby elementów, ponieważ nie jest jeszcze znana).

Pavels Ahmadulins
źródło