Czy zwracanie IList <T> jest gorsze niż zwracanie T [] lub List <T>?

80

Odpowiedzi na takie pytania: List <T> lub IList <T> zawsze wydają się zgadzać, że zwrócenie interfejsu jest lepsze niż zwrócenie konkretnej implementacji kolekcji. Ale walczę z tym. Utworzenie wystąpienia interfejsu jest niemożliwe, więc jeśli metoda zwraca interfejs, w rzeczywistości nadal zwraca określoną implementację. Trochę z tym eksperymentowałem, pisząc dwie małe metody:

public static IList<int> ExposeArrayIList()
{
    return new[] { 1, 2, 3 };
}

public static IList<int> ExposeListIList()
{
    return new List<int> { 1, 2, 3 };
}

I użyj ich w moim programie testowym:

static void Main(string[] args)
{
    IList<int> arrayIList = ExposeArrayIList();
    IList<int> listIList = ExposeListIList();

    //Will give a runtime error
    arrayIList.Add(10);
    //Runs perfectly
    listIList.Add(10);
}

W obu przypadkach, gdy próbuję dodać nową wartość, mój kompilator nie daje mi żadnych błędów, ale oczywiście metoda, która ujawnia moją tablicę jako a, IList<T>daje błąd w czasie wykonywania, gdy próbuję coś do niej dodać. Dlatego ludzie, którzy nie wiedzą, co dzieje się w mojej metodzie i muszą dodać do niej wartości, są zmuszeni najpierw skopiować moje IListdo a, Listaby móc dodawać wartości bez ryzyka błędów. Oczywiście mogą sprawdzić typoszereg, aby sprawdzić, czy mają do czynienia z a Listlub an Array, ale jeśli tego nie robią i chcą dodać elementy do kolekcji, nie mają innego wyboru, aby skopiować IListdo a List, nawet jeśli to już jestList . Czy tablica nigdy nie powinna być ujawniona jako IList?

Inna moja obawa opiera się na zaakceptowanej odpowiedzi na powiązane pytanie (moje wyróżnienie):

Jeśli udostępniasz swoją klasę za pośrednictwem biblioteki, z której będą korzystać inni, generalnie chcesz udostępnić ją za pośrednictwem interfejsów, a nie konkretnych implementacji. Pomoże to, jeśli później zdecydujesz się zmienić implementację swojej klasy, aby użyć innej konkretnej klasy. W takim przypadku użytkownicy Twojej biblioteki nie będą musieli aktualizować swojego kodu, ponieważ interfejs się nie zmienia.

Jeśli używasz go tylko wewnętrznie, możesz nie przejmować się tak bardzo, a korzystanie z listy może być w porządku.

Wyobraź sobie, że ktoś faktycznie użył mojej metody, IList<T>którą otrzymałem z mojej ExposeListIlist()metody, właśnie w ten sposób, aby dodać / usunąć wartości. Wszystko dziala. Ale teraz, jak sugeruje odpowiedź, ponieważ zwracanie interfejsu jest bardziej elastyczne, zwracam tablicę zamiast listy (nie ma problemu z mojej strony!), A potem czeka ich przyjemność ...

TLDR:

1) Odsłonięcie interfejsu powoduje niepotrzebne rzuty? Czy to nie ma znaczenia?

2) Czasami, jeśli użytkownicy biblioteki nie używają rzutowania, ich kod może się zepsuć, gdy zmienisz metodę, nawet jeśli metoda pozostaje w porządku.

Prawdopodobnie zbytnio się nad tym zastanawiam, ale nie mam ogólnego konsensusu, że zwracanie interfejsu jest lepsze niż zwracanie implementacji.

Alexander Derck
źródło
9
Następnie wróć IEnumerable<T>i masz czas na kompilację. Nadal można używać wszystkich metod rozszerzenia LINQ, które zwykle próbują zoptymalizować wydajność, rzutując ją na określony typ (podobnie ICollection<T>jak użycie Countwłaściwości zamiast wyliczania).
Tim Schmelter
4
Tablice implementowane IList<T>przez „hack”. Jeśli chcesz ujawnić metodę zwracającą ją, zwróć typ faktycznie ją implementujący.
CodeCaster
3
Po prostu nie składaj obietnic, których nie możesz dotrzymać. Programista klienta prawdopodobnie będzie rozczarowany, gdy przeczyta Twoją umowę „Zwrócę listę”. Tak nie jest, po prostu tego nie rób.
Hans Passant
7
Z MSDN : IList jest potomkiem interfejsu ICollection i jest interfejsem podstawowym wszystkich list nieogólnych. Implementacje IList dzielą się na trzy kategorie: tylko do odczytu, o stałym rozmiarze i o zmiennym rozmiarze. Nie można modyfikować IList tylko do odczytu. IList o stałym rozmiarze nie pozwala na dodawanie ani usuwanie elementów, ale umożliwia modyfikację istniejących elementów. IList o zmiennej wielkości umożliwia dodawanie, usuwanie i modyfikację elementów.
Hamid Pourjam
3
@AlexanderDerck: jeśli chcesz, aby konsument mógł dodawać pozycje do Twojej listy, nie rób tego IEnumerable<T>w pierwszej kolejności. Wtedy możesz wrócić IList<T>lub List<T>.
Tim Schmelter,

Odpowiedzi:

108

Może to nie jest bezpośrednią odpowiedzią na Twoje pytanie, ale w .NET 4.5+ wolę przestrzegać następujących zasad podczas projektowania publicznych lub chronionych API:

  • zwracaj IEnumerable<T>, jeśli dostępne jest tylko wyliczenie;
  • zwracaj, IReadOnlyCollection<T>jeśli dostępne są zarówno wyliczenie, jak i liczba elementów;
  • zwracają IReadOnlyList<T>, jeśli wyliczenie, liczba elementów i dostęp indeksowany są dostępne;
  • zwracają, ICollection<T>jeśli wyliczenie, liczba elementów i modyfikacja są dostępne;
  • zwracają IList<T>, jeśli wyliczenie, liczba elementów, indeksowany dostęp i modyfikacja są dostępne.

Ostatnie dwie opcje zakładają, że metoda nie może zwracać tablicy jako IList<T>implementacji.

Dennis
źródło
2
W istocie nie jest to odpowiedź, ale dobra reguła i bardzo mi pomocna :) Ostatnio bardzo się męczyłem z wyborem tego, co zwrócić. Twoje zdrowie!
Alexander Derck
1
To jest rzeczywiście właściwa zasada +1. Tylko dla kompletności pragnę dodać IReadOnlyCollection<T>, a ICollection<T>które mają ich wykorzystanie dla niezarejestrowanych pojemników do indeksowania (na przykład później jest de facto standardem dla EF właściwości nawigacyjnych kolekcja podmiot sub)
Ivan Stoev
@IvanStoev Czy możesz edytować odpowiedź z dodanymi? Byłoby miło, gdybym w końcu dostał listę, gdzie używać jakiego interfejsu
Alexander Derck,
3
Należy zauważyć, że IReadOnlyCollection<T>i IReadOnlyList<T>mają tę samą wadę, co C ++ const. Istnieją dwie możliwości: albo kolekcja jest niezmienna, albo tylko twój sposób dostępu do kolekcji zabrania ci jej modyfikowania. I nie możesz odróżnić jednego od drugiego.
ach
1
@Panzercrisis: Oczywiście wybrałbym drugą opcję - IEnumerable<T>. Jako programista API masz pełną wiedzę na temat dozwolonych przypadków użycia. Jeśli wyniki metod mają być tylko wyliczane, nie ma powodu, aby je ujawniać IList<T>. Im mniej zależy od wyniku, tym bardziej elastyczna jest implementacja metody. Np. Wystawianie IEnumerable<T>pozwala na zmianę implementacji na yield returns, IList<T>nie pozwala na to.
Dennis
31

Nie, ponieważ konsument powinien wiedzieć, czym dokładnie jest IList :

IList jest potomkiem interfejsu ICollection i jest interfejsem podstawowym wszystkich list nieogólnych. Implementacje IList dzielą się na trzy kategorie: tylko do odczytu, o stałym rozmiarze i o zmiennym rozmiarze. Nie można modyfikować IList tylko do odczytu. IList o stałym rozmiarze nie pozwala na dodawanie ani usuwanie elementów, ale umożliwia modyfikację istniejących elementów. IList o zmiennej wielkości umożliwia dodawanie, usuwanie i modyfikację elementów.

Możesz sprawdzić IList.IsFixedSizei IList.IsReadOnlyi rób co chcesz z nim.

Myślę, że IListjest to przykład grubego interfejsu i powinien zostać podzielony na wiele mniejszych interfejsów, a także narusza zasadę podstawiania Liskova, gdy zwracasz tablicę jako plik IList.

Przeczytaj więcej, jeśli chcesz podjąć decyzję o powrocie interfejsu


AKTUALIZACJA

Kopiąc więcej i stwierdziłem, że IList<T>nie implementuje IListi IsReadOnlyjest dostępny przez interfejs podstawowy, ICollection<T>ale nie ma IsFixedSizedla IList<T>. Przeczytaj więcej o tym, dlaczego rodzajowy IList <> nie dziedziczy nieogólnego IList?

Hamid Pourjam
źródło
8
Te właściwości są dobre, ale moim zdaniem nadal nie spełniają one funkcji interfejsu. Dla mnie interfejs to umowa, która nie powinna polegać na żadnych czekach ani czymkolwiek
Alexander Derck
1
Naprawdę byłoby milsze dla programisty, gdyby dostarczyli dodatkowy interfejs z List<T>semantyką (o stałym rozmiarze / nie tylko do odczytu) , ale prawdopodobnie istnieje rozsądne uzasadnienie, dlaczego nie zostało to uwzględnione ... ale tak, myślę, że twój pomysł na kilka interfejsów obejmujących różne funkcje byłby znacznie rozsądniejszym wyborem;
wydający
1
IList<T>to gruby interfejs, ale to najwięcej, co możesz uzyskać z tablicy i listy. Więc jeśli spowodowałoby to rozbicie na wiele interfejsów, możesz nie być w stanie użyć niektórych metod z listami i tablicami, ale tylko z jedną z nich, nawet jeśli wszystkie metody, których chcesz użyć, są obsługiwane (takie jak IList.Countlub indeksator). Więc moim zdaniem była to dobra decyzja. Jeśli powoduje to NotSupportedExceptionw bardzo niewielu przypadkach, w których chce się użyć Addna tablicy, cóż, taka jest umowa.
Tim Schmelter
1
@dotctor Co za zbieg okoliczności, wczoraj czytałem o zasadzie substytucji Liskova i to sprawiło, że pomyślałem :) dzięki za odpowiedź
Alexander Derck
6
Cóż, ta odpowiedź w połączeniu z zasadami Dennisa z pewnością może poprawić zrozumienie, jak radzić sobie z tym dylematem. Nie trzeba dodawać, że zasada substytucji Liskova jest tu i ówdzie naruszana w platformie .NET i nie ma się czym martwić.
Fabjan
13

Podobnie jak w przypadku wszystkich pytań „interfejs kontra implementacja”, musisz zdać sobie sprawę, co oznacza ujawnienie publicznego członka: definiuje publiczne API tej klasy.

Jeśli wystawisz a List<T>jako element członkowski (pole, właściwość, metoda, ...), poinformujesz konsumenta o tym elemencie: typ uzyskany przez dostęp do tej metody to a List<T>lub coś pochodnego.

Teraz, jeśli ujawnisz interfejs, ukryjesz „szczegóły implementacji” swojej klasy używając konkretnego typu. Oczywiście instancji nie można IList<T>, ale można użyć Collection<T>, List<T>, ich pochodne lub własne typ wykonawczych IList<T>.

Rzeczywiste pytanie „Dlaczego Arraywdrożenie IList<T> , lub „Dlaczego ma IList<T>interfejs, tak wielu członków” .

Zależy to również od tego, czego oczekujesz od konsumentów tego członka. Jeśli faktycznie zwrócisz członka wewnętrznego za pośrednictwem swojego Expose...członka, zechcesz new List<T>(internalMember)mimo to zwrócić członka , ponieważ w przeciwnym razie konsument może spróbować przesłać go IList<T>i zmodyfikować za jego pomocą członka wewnętrznego.

Jeśli po prostu oczekujesz, że konsumenci będą powtarzać wyniki, ujawnij IEnumerable<T>lub IReadOnlyCollection<T>zamiast tego.

CodeCaster
źródło
3
„Właściwe pytanie brzmi:„ Dlaczego Array implementuje IList <T> ”lub„ Dlaczego interfejs IList <T> ma tak wielu członków ”.”, To jest rzeczywiście coś, z czym się zmagałem.
Alexander Derck
2
@Fabjan - IList<T>ma metody dodawania i usuwania elementów, dlatego implementacja tablic jest niekorzystna. Ta IsReadOnlywłaściwość jest sztuczką mającą na celu zatuszowanie różnic, gdy powinna zostać podzielona na (co najmniej) dwa interfejsy.
Lee
@Lee Whoops, masz rację, usuwając mój niedokładny komentarz
Fabjan
1
@AlexanderDerck to coś, o co pytałem wcześniej stackoverflow.com/q/5968708/706456
oleksii
@oleksii jest rzeczywiście podobny, ale nie nazwałbym tego duplikatem
Alexander Derck
8

Uważaj na ogólne cytaty wyrwane z kontekstu.

Zwracanie interfejsu jest lepsze niż zwracanie konkretnej implementacji

Ten cytat ma sens tylko wtedy, gdy jest używany w kontekście zasad SOLID . Istnieje 5 zasad, ale na potrzeby tej dyskusji omówimy tylko ostatnie 3.

Zasada inwersji zależności

należy „Zależnie od abstrakcji. Nie polegaj na konkrecjach. ”

Moim zdaniem ta zasada jest najtrudniejsza do zrozumienia. Ale jeśli przyjrzysz się uważnie cytatowi, wygląda on bardzo podobnie do oryginalnego cytatu.

Zależy od interfejsów (abstrakcji). Nie zależą od konkretnych realizacji (konkrecji).

To wciąż jest trochę zagmatwane, ale jeśli zaczniemy stosować inne zasady razem, zacznie to mieć dużo więcej sensu.

Zasada substytucji Liskova

„Obiekty w programie powinny być zastępowalne instancjami ich podtypów bez zmiany poprawności tego programu”.

Jak zauważyłeś, zwracanie a Arrayjest wyraźnie innym zachowaniem niż zwracanie a, List<T>mimo że obie implementują IList<T>. Z całą pewnością jest to naruszenie LSP.

Ważne jest, aby zdać sobie sprawę, że interfejsy dotyczą konsumenta . Jeśli zwracasz interfejs, utworzyłeś kontrakt, w którym można używać dowolnych metod lub właściwości w tym interfejsie bez zmiany zachowania programu.

Zasada segregacji interfejsów

„Wiele interfejsów specyficznych dla klienta jest lepszych niż jeden interfejs ogólnego przeznaczenia”.

Jeśli zwracasz interfejs, powinieneś zwrócić najbardziej specyficzny dla klienta interfejs obsługiwany przez Twoją implementację. Innymi słowy, jeśli nie oczekujesz, że klient wywoła Addmetodę, nie powinieneś zwracać interfejsu z rozszerzeniemAdd metodę.

Niestety, interfejsy w .NET Framework (szczególnie wczesne wersje) nie zawsze są idealnymi interfejsami specyficznymi dla klienta. Chociaż, jak zauważył @Dennis w swojej odpowiedzi, w .NET 4.5+ jest znacznie więcej możliwości wyboru.

gry rzemieślnicze
źródło
Odczytanie zasady IList<T>
Liskova
W tym konkretnym przypadku to Array narusza LSP. Jeśli zwracasz IList, nigdy nie powinieneś zwracać tablicy, ponieważ jest to naruszenie. Zawarłeś już umowę z dzwoniącym, że otrzyma listę, na której można dodawać i usuwać elementy.
craftworkgames
5

Zwracanie interfejsu niekoniecznie jest lepsze niż zwrócenie konkretnej implementacji kolekcji. Zawsze powinieneś mieć dobry powód, aby używać interfejsu zamiast konkretnego typu. W twoim przykładzie wydaje się to bezcelowe.

Prawidłowe powody korzystania z interfejsu to:

  1. Nie wiesz, jak będzie wyglądać implementacja metod zwracających interfejs i może być ich wiele, rozwijanych w czasie. Mogą je pisać inni ludzie, z innych firm. Więc chcesz po prostu zgodzić się na podstawowe potrzeby i pozostawić im, jak wdrożyć tę funkcjonalność.

  2. Chcesz udostępnić niektóre typowe funkcje niezależne od hierarchii klas w sposób bezpieczny dla typów. Obiekty o różnych typach podstawowych, które powinny oferować te same metody, implementowałyby Twój interfejs.

Można by argumentować, że 1 i 2 to w zasadzie ten sam powód. Są to dwa różne scenariusze, które ostatecznie prowadzą do tej samej potrzeby.

„To umowa”. Jeśli umowa dotyczy Ciebie, a Twoja aplikacja jest zamknięta zarówno pod względem funkcjonalności, jak i czasu, często nie ma sensu używać interfejsu.

Martin Maat
źródło
1
Nie zgadzam się, jeśli można zwrócić interfejs, zwróciłbym interfejs, ponieważ jest to jedyna potrzebna rzecz. Jeśli metoda oczekuje interfejsu, mogę przekazać klasę, która implementuje tylko ten interfejs do niej, ale także klasę, która implementuje interfejs + mnóstwo innych rzeczy. Dzięki interfejsowi łatwiej jest rozszerzyć aplikację bez przepisywania istniejących rzeczy
Alexander Derck,
2
Tworzenie całego oprogramowania (a zwłaszcza klas i bibliotek klas) z nastawieniem „A co by było, gdyby ktoś inny używał tej klasy (biblioteki)” może być naprawdę pouczające ? i „A jeśli chcę dodać funkcjonalność do konkretnego typu, który chcę zwrócić?” . Typy BCL można uszczelniać, więc nie można ich przedłużyć. Wtedy utkniesz, jeśli chcesz zwrócić coś innego, ponieważ twoje API obiecuje ujawnić ten dokładny typ, w przeciwieństwie do interfejsu. Zwracanie interfejsu jest prawie zawsze lepsze niż zwracanie konkretnej implementacji.
CodeCaster
1
Zgadzam się (naprawdę), powinienem był dodać trzeci powód: „ograniczyć funkcjonalność zwracanego obiektu do jego istoty, aby wyjaśnić wiadomość dzwoniącemu”. Ja też zwracam interfejsy IReadOnlyXxx, gdy tylko jest to możliwe, zamiast pełnoprawnego obiektu używanego do składania kolekcji.
Martin Maat