Czy ktoś może mi wyjaśnić, dlaczego chciałbym użyć IList zamiast listy w C #?
Powiązane pytanie: Dlaczego ujawnianie jest uważane za złeList<T>
Czy ktoś może mi wyjaśnić, dlaczego chciałbym użyć IList zamiast listy w C #?
Powiązane pytanie: Dlaczego ujawnianie jest uważane za złeList<T>
Odpowiedzi:
Jeśli udostępniasz swoją klasę za pomocą biblioteki, z której będą korzystać inni, zazwyczaj chcesz ją udostępnić za pomocą interfejsów, a nie konkretnych implementacji. Pomoże to, jeśli zdecydujesz się później 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 używanie List<T>
może być w porządku.
Mniej popularną odpowiedzią jest to, że programiści lubią udawać, że ich oprogramowanie będzie ponownie używane na całym świecie. W rzeczywistości większość projektów będzie utrzymywana przez niewielką liczbę ludzi, a niezależnie od tego, jak fajne są interfejsy dźwiękowe, to łudzisz siebie.
Astronauci architektury . Szanse, że kiedykolwiek napiszesz własną IListę, która dodaje coś do tych, które są już w środowisku .NET, są tak odległe, że to teoretyczne żelki zarezerwowane dla „najlepszych praktyk”.
Oczywiście, jeśli zostaniesz zapytany o to, którego używasz w wywiadzie, powiesz: IList, uśmiechnij się i oboje wyglądają na zadowolonych z siebie za to, że jesteś tak mądry. Lub dla publicznego API, IList. Mam nadzieję, że rozumiesz.
IEnumerable<string>
, wewnątrz funkcji mogę użyć a List<string>
dla wewnętrznego magazynu kopii zapasowych do wygenerowania mojej kolekcji, ale chcę tylko, aby osoby dzwoniące wyliczyły jej zawartość, a nie dodawały ani nie usuwały. Zaakceptowanie interfejsu jako parametru przekazuje podobny komunikat „Potrzebuję kolekcji ciągów, nie martw się, nie zmienię tego”.
Interfejs to obietnica (lub umowa).
Jak zawsze z obietnicami - im mniejsze, tym lepiej .
IList
jest obietnica. Która jest tutaj mniejsza i lepsza obietnica? To nie jest logiczne.
List<T>
, ponieważ AddRange
nie jest zdefiniowany w interfejsie.
IList<T>
składa obietnice, których nie może dotrzymać. Na przykład istnieje Add
metoda, którą można rzucić, jeśli IList<T>
akurat jest tylko do odczytu. List<T>
z drugiej strony jest zawsze do zapisu i dlatego zawsze dotrzymuje obietnic. Ponadto, od .NET 4.6, teraz mamy IReadOnlyCollection<T>
i IReadOnlyList<T>
które prawie zawsze lepiej pasują niż IList<T>
. I są kowariantne. Unikaj IList<T>
!
IList<T>
. Ktoś mógłby równie dobrze zaimplementować IReadOnlyList<T>
i sprawić, by każda metoda rzucała wyjątek. To coś w rodzaju przeciwieństwa pisania na kaczce - nazywasz ją kaczką, ale nie może tak kwakać. Jest to jednak część kontraktu, nawet jeśli kompilator nie może go egzekwować. Zgadzam się jednak w 100% z tym, że powinienem IReadOnlyList<T>
lub IReadOnlyCollection<T>
powinienem z niego korzystać tylko do odczytu.
Niektórzy mówią „zawsze używaj IList<T>
zamiast List<T>
”.
Chcą, abyś zmienił sygnatury metod z void Foo(List<T> input)
na void Foo(IList<T> input)
.
Ci ludzie się mylą.
Jest bardziej dopracowany niż to. Jeśli wracasz IList<T>
jako część publicznego interfejsu do swojej biblioteki, pozostawiasz sobie ciekawe opcje, aby być może stworzyć własną listę w przyszłości. Być może nigdy nie potrzebujesz tej opcji, ale jest to argument. Myślę, że to cały argument za zwróceniem interfejsu zamiast konkretnego typu. Warto wspomnieć, ale w tym przypadku ma poważną wadę.
Jako drobny kontrargument, może się okazać, że każdy dzwoniący potrzebuje i List<T>
tak, a kod wywołujący jest zaśmiecony.ToList()
Ale co ważniejsze, jeśli akceptujesz IList jako parametr, lepiej bądź ostrożny, ponieważ IList<T>
i List<T>
nie zachowujesz się w ten sam sposób. Mimo podobieństwa nazwy, a mimo dzielenie interfejs oni nie narazić taką samą umowę .
Załóżmy, że masz tę metodę:
public Foo(List<int> a)
{
a.Add(someNumber);
}
Pomocny kolega „refaktoryzuje” metodę akceptacji IList<int>
.
Twój kod jest teraz uszkodzony, ponieważ int[]
implementuje IList<int>
, ale ma stały rozmiar. Umowa ICollection<T>
(podstawa IList<T>
) wymaga kodu, który używa jej do sprawdzenia IsReadOnly
flagi przed próbą dodania lub usunięcia elementów z kolekcji. Umowa List<T>
nie.
Zasada podstawienia Liskowa (uproszczona) stanowi, że typ pochodny powinien być stosowany zamiast typu podstawowego, bez dodatkowych warunków wstępnych i dodatkowych.
Wydaje się, że to łamie zasadę substytucji Liskowa.
int[] array = new[] {1, 2, 3};
IList<int> ilist = array;
ilist.Add(4); // throws System.NotSupportedException
ilist.Insert(0, 0); // throws System.NotSupportedException
ilist.Remove(3); // throws System.NotSupportedException
ilist.RemoveAt(0); // throws System.NotSupportedException
Ale tak nie jest. Odpowiedź na to jest taka, że w przykładzie użyto IList <T> / ICollection <T> niepoprawnie. Jeśli używasz ICollection <T>, musisz sprawdzić flagę IsReadOnly.
if (!ilist.IsReadOnly)
{
ilist.Add(4);
ilist.Insert(0, 0);
ilist.Remove(3);
ilist.RemoveAt(0);
}
else
{
// what were you planning to do if you were given a read only list anyway?
}
Jeśli ktoś przekaże Ci tablicę lub listę, Twój kod będzie działał dobrze, jeśli za każdym razem sprawdzisz flagę i cofniesz się ... Ale naprawdę; kto tak robi? Czy nie wiesz z góry, czy twoja metoda potrzebuje listy, która może przyjąć dodatkowych członków; nie podajesz tego w podpisie metody? Co dokładnie zrobiłbyś, gdybyś otrzymał listę tylko do odczytu int[]
?
Możesz zastąpić List<T>
kod, który używa IList<T>
/ ICollection<T>
poprawnie . Nie możesz zagwarantować, że możesz zastąpić używany kod IList<T>
/ .ICollection<T>
List<T>
Istnieje wiele odwołań do zasady pojedynczej odpowiedzialności / zasady segregacji interfejsu w wielu argumentach, aby używać abstrakcji zamiast konkretnych typów - zależą od najwęższego możliwego interfejsu. W większości przypadków, jeśli używasz a List<T>
i myślisz, że możesz zamiast tego użyć węższego interfejsu - dlaczego nie IEnumerable<T>
? Jest to często lepsze dopasowanie, jeśli nie trzeba dodawać elementów. Jeśli chcesz dodać do kolekcji, użyć typu beton, List<T>
.
Dla mnie IList<T>
(i ICollection<T>
) jest najgorszą częścią frameworku .NET. IsReadOnly
narusza zasadę najmniejszego zaskoczenia. Klasa taka jak Array
, która nigdy nie pozwala na dodawanie, wstawianie lub usuwanie elementów, nie powinna implementować interfejsu za pomocą metod dodawania, wstawiania i usuwania. (patrz także /software/306105/implementing-an-interface-when-you-dont-need-one-of-the-properties )
Czy IList<T>
pasuje do twojej organizacji? Jeśli kolega poprosi cię o zmianę podpisu metody, którego chcesz użyć IList<T>
zamiast List<T>
, zapytaj go, jak dodaliby element do IList<T>
. Jeśli nie wiedzą IsReadOnly
(a większość ludzi nie wie ), nie używaj IList<T>
. Zawsze.
Zauważ, że flaga IsReadOnly pochodzi z ICollection <T> i wskazuje, czy elementy można dodać lub usunąć z kolekcji; ale tak naprawdę, aby wprowadzić w błąd rzeczy, nie wskazuje, czy można je zastąpić, co może być w przypadku tablic (które zwracają IsReadOnlys == true).
Aby uzyskać więcej informacji na temat IsReadOnly, zobacz definicję msdn dla ICollection <T> .IsReadOnly
IsReadOnly
jest true
to IList
możliwe, nadal możesz modyfikować jego obecnych członków! Być może tę właściwość należy nazwać IsFixedSize
?
Array
jako IList
, więc mógłbym zmodyfikować obecnych członków)
IReadOnlyCollection<T>
i IReadOnlyList<T>
które prawie zawsze lepiej pasują niż, IList<T>
ale nie mają niebezpiecznej, leniwej semantyki IEnumerable<T>
. I są kowariantne. Unikaj IList<T>
!
List<T>
jest specyficzną implementacją IList<T>
, która jest kontenerem, który można adresować w taki sam sposób jak tablicę liniową z T[]
wykorzystaniem indeksu liczb całkowitych. Podając IList<T>
jako typ argumentu metody, określasz tylko, że potrzebujesz pewnych możliwości kontenera.
Na przykład specyfikacja interfejsu nie wymusza zastosowania określonej struktury danych. Implementacja List<T>
dzieje się z tą samą wydajnością w zakresie uzyskiwania dostępu, usuwania i dodawania elementów jako tablicy liniowej. Można jednak wyobrazić sobie implementację, która jest poparta połączoną listą, dla której dodawanie elementów na końcu jest tańsze (czas stały), ale dostęp losowy jest znacznie droższy. (Należy pamiętać, że .NET LinkedList<T>
ma nie wdrożenia IList<T>
.)
W tym przykładzie pokazano również, że mogą wystąpić sytuacje, w których konieczne jest określenie implementacji, a nie interfejsu, na liście argumentów: W tym przykładzie, ilekroć potrzebujesz konkretnej charakterystyki wydajności dostępu. Jest to zwykle gwarantowane dla konkretnej implementacji kontenera ( List<T>
dokumentacja: „Implementuje IList<T>
ogólny interfejs za pomocą tablicy, której rozmiar jest dynamicznie zwiększany zgodnie z wymaganiami.”).
Ponadto możesz rozważyć udostępnienie najmniejszej potrzebnej funkcjonalności. Na przykład. jeśli nie musisz zmieniać zawartości listy, prawdopodobnie powinieneś rozważyć użycie rozszerzenia IEnumerable<T>
, które się IList<T>
rozszerza.
LinkedList<T>
vs biorąc List<T>
vs IList<T>
wszyscy przekazują coś z gwarancji wydajności wymaganych przez wywoływany kod.
Chciałbym nieco odwrócić to pytanie, zamiast uzasadniać, dlaczego powinieneś używać interfejsu zamiast konkretnej implementacji, spróbuj uzasadnić, dlaczego miałbyś użyć konkretnej implementacji zamiast interfejsu. Jeśli nie możesz tego uzasadnić, skorzystaj z interfejsu.
Zasadą TDD i OOP jest na ogół programowanie interfejsu, a nie implementacja.
W tym konkretnym przypadku, ponieważ zasadniczo mówisz o konstrukcji językowej, a nie niestandardowej, to na ogół nie będzie miało znaczenia, ale powiedz na przykład, że okazało się, że Lista nie obsługuje czegoś, czego potrzebujesz. Jeśli używałeś IList w pozostałej części aplikacji, możesz rozszerzyć Listę o własną klasę niestandardową i nadal być w stanie przekazywać ją bez refaktoryzacji.
Koszt tego jest minimalny, dlaczego nie zaoszczędzić sobie później bólu głowy? Na tym polega zasada interfejsu.
public void Foo(IList<Bar> list)
{
// Do Something with the list here.
}
W takim przypadku można przekazać dowolną klasę, która implementuje interfejs IList <Bar>. Jeśli zamiast tego użyto List <Bar>, można przekazać tylko instancję List <Bar>.
Sposób IList <Bar> jest luźniej sprzężony niż sposób List <Bar>.
Zaskakujące, że żadne z tych pytań (lub odpowiedzi) nie wspomina o różnicy podpisu. (Dlatego szukałem tego pytania na SO!)
Oto metody zawarte w List, których nie ma w IList, przynajmniej od .NET 4.5 (około 2015)
Reverse
i ToArray
są metodami rozszerzenia interfejsu IEnumerable <T>, z którego wywodzi się IList <T>.
List
si, a nie w innych implementacjach IList
.
Najważniejszym przypadkiem użycia interfejsów zamiast implementacji są parametry w interfejsie API. Jeśli interfejs API przyjmuje parametr List, każdy, kto go używa, musi użyć List. Jeśli typ parametru to IList, osoba wywołująca ma znacznie większą swobodę i może korzystać z klas, o których nigdy nie słyszałeś, a które nawet nie istniałyby podczas pisania kodu.
Co jeśli .NET 5.0 zastępuje System.Collections.Generic.List<T>
się System.Collection.Generics.LinearList<T>
. .NET zawsze jest właścicielem nazwy, List<T>
ale gwarantują toIList<T>
jest to umowa. Więc IMHO my (przynajmniej ja) nie powinniśmy używać czyichś nazwisk (choć w tym przypadku jest to .NET) i wpakować się później.
W przypadku użycia IList<T>
, osoba dzwoniąca zawsze gwarantuje rzeczy do pracy, a osoba wdrażająca może zmienić kolekcję bazową na dowolną alternatywną konkretną implementacjęIList
Wszystkie koncepcje są w zasadzie zawarte w większości powyższych odpowiedzi dotyczących tego, dlaczego należy używać interfejsu zamiast konkretnych implementacji.
IList<T> defines those methods (not including extension methods)
IList<T>
Łącze MSDN
List<T>
implementuje te dziewięć metod (nie wliczając metod rozszerzania), ponadto ma około 41 metod publicznych, co waży, biorąc pod uwagę, którą z nich należy zastosować w aplikacji.
List<T>
Łącze MSDN
Zrobiłbyś to, ponieważ zdefiniowanie IList lub ICollection otworzyłoby się na inne implementacje twoich interfejsów.
Możesz chcieć mieć IOrderRepository, które definiuje zbiór zamówień w IList lub ICollection. Możesz wtedy zastosować różne rodzaje implementacji, aby uzyskać listę zamówień, o ile są one zgodne z „regułami” zdefiniowanymi przez IList lub ICollection.
IList <> jest prawie zawsze preferowana zgodnie z zaleceniami drugiego autora, jednak zauważ, że w .NET 3.5 sp 1 występuje błąd podczas uruchamiania IList <> przez więcej niż jeden cykl serializacji / deserializacji z WCF DataContractSerializer.
Dostępny jest teraz dodatek SP do naprawy tego błędu: KB 971030
Interfejs zapewnia, że uzyskasz przynajmniej oczekiwane metody ; mając świadomość definicji interfejsu, tj. wszystkie metody abstrakcyjne, które mają zostać zaimplementowane przez dowolną klasę dziedziczącą interfejs. więc jeśli ktoś tworzy własną klasę za pomocą kilku metod oprócz metod odziedziczonych po interfejsie dla dodatkowej funkcjonalności, a te nie są dla ciebie przydatne, lepiej użyć odwołania do podklasy (w tym przypadku interfejs) i przypisz mu konkretny obiekt klasy.
dodatkową zaletą jest to, że Twój kod jest bezpieczny przed wszelkimi zmianami w konkretnej klasie, ponieważ subskrybujesz tylko kilka metod konkretnej klasy i te będą tam, dopóki konkretna klasa odziedziczy po interfejsie, którym jesteś za pomocą. więc jego bezpieczeństwo dla ciebie i wolność dla programisty piszącego konkretną implementację, aby zmienić lub dodać więcej funkcjonalności do swojej konkretnej klasy.
Możesz spojrzeć na ten argument z kilku punktów widzenia, w tym podejścia opartego wyłącznie na OO, które mówi o programowaniu przeciwko interfejsowi, a nie implementacji. Z tą myślą korzystanie z IList odbywa się według tej samej zasady, co przekazywanie i używanie interfejsów zdefiniowanych od zera. Wierzę również w czynniki skalowalności i elastyczności zapewniane przez interfejs w ogóle. Jeśli klasa implementująca IList <T> wymaga rozszerzenia lub zmiany, kod zużywający nie musi się zmieniać; wie, czego przestrzega umowa IList Interface. Jednak użycie konkretnej implementacji i listy <T> w klasie, która się zmienia, może również wymagać zmiany kodu wywołującego. Wynika to z faktu, że klasa przylegająca do IList <T> gwarantuje pewne zachowanie, które nie jest gwarantowane przez konkretny typ używający List <T>.
Również posiadanie uprawnień do robienia czegoś takiego jak modyfikowanie domyślnej implementacji List <T> w klasie Implementing IList <T>, na przykład .Add, .Remove lub dowolna inna metoda IList daje programistom dużą elastyczność i moc, w innym przypadku wstępnie zdefiniowaną według listy <T>
Zazwyczaj dobrym podejściem jest użycie IList w publicznym interfejsie API (w razie potrzeby i semantyka listy są potrzebne), a następnie Lista wewnętrznie w celu wdrożenia interfejsu API. Umożliwia to zmianę na inną implementację IList bez łamania kodu używającego twojej klasy.
Lista nazw klas może zostać zmieniona w następnym frameworku .net, ale interfejs nigdy się nie zmieni, ponieważ interfejs jest kontraktowy.
Zauważ, że jeśli twój interfejs API będzie używany tylko w pętlach foreach itp., Możesz rozważyć wystawienie IEnumerable.