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.
źródło
Odpowiedzi:
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.źródło
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>
. JestList<string>
to trochę nie-nie, ponieważ jest ogólnie zmienne i posunę się tak daleko, aby powiedzieć, żeReadOnlyCollection<string>
jako realizatorICollection<string>
czuje się, jakby narusza zasadę substytucji Liskowa.źródło
IEnumerable<string>
jest dokładnie tym, czego tu chcemy.IReadOnlyList<T>
.IList<T>
)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
Następnie uzyskaj z tego interfejs zawierający tylko część właściwości (i dowolne metody odczytu) i zaimplementuj to w A.
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
źródło
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 wtedyIEnumerable<T>
alboReadOnlyCollection<T>
:IEnumerable<T>
robiIEnumerable
lubIReadOnlyCollect
nie 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).źródło