Lista <T> lub IList <T> [zamknięte]


495

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>


1
wyszukaj w Internecie „kod do interfejsu, a nie implementację”, możesz przeczytać cały dzień i znaleźć kilka godzin wojny.
granadaCoder,

Odpowiedzi:


446

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.


19
Nie rozumiem (subtelnej) różnicy, czy jest jakiś przykład, na który możesz mnie wskazać?
StingyJack,

134
Powiedzmy, że pierwotnie używałeś Listy <T> i chciałeś zmienić, aby używać specjalistycznej CaseInsensitiveList <T>, z których oba implementują IList <T>. Jeśli używasz konkretnego typu, wszystkie osoby dzwoniące muszą zostać zaktualizowane. Jeśli zostanie wyświetlony jako IList <T>, dzwoniącego nie trzeba zmieniać.
tvanfosson

46
Ach OK. Rozumiem. Wybór najniższego wspólnego mianownika dla umowy. Dzięki!
StingyJack,

16
Ta zasada jest przykładem enkapsulacji, jednego z trzech filarów programowania obiektowego. Chodzi o to, że ukrywasz szczegóły implementacji przed użytkownikiem, a zamiast tego zapewniasz im stabilny interfejs. Ma to na celu zmniejszenie zależności od szczegółów, które mogą ulec zmianie w przyszłości.
jason

14
Zauważ też, że T []: IList <T>, dzięki czemu ze względu na wydajność zawsze możesz zamienić zwrot ze Listy <T> z procedury na zwrot T [], a jeśli procedura zadeklaruje, że zwraca IList <T> (a może ICollection <T> lub IEnumerable <T>) nic innego nie wymagałoby zmiany.
yfeldblum

322

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”.

Oprogramowanie astronautów

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.


65
Muszę się nie zgodzić z twoją sardoniczną odpowiedzią. Nie zgadzam się całkowicie z tym, że uczucie nadmiernej architektury jest prawdziwym problemem. Myślę jednak, że szczególnie w przypadku kolekcji, które naprawdę błyszczą. Powiedzmy, że mam funkcję, która zwraca 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”.
joshperry,

38
Rozwój oprogramowania polega na tłumaczeniu zamiarów. Jeśli uważasz, że interfejsy przydają się tylko do budowania dużych, imponujących architektur i nie mają miejsca w małych sklepach, to mam nadzieję, że osoba siedząca naprzeciwko ciebie w wywiadzie nie jest mną.
joshperry,

48
Ktoś musiał powiedzieć to Arec ... nawet jeśli w wyniku tego masz różnego rodzaju akademickie wzorce ścigające pocztę nienawiści. +1 dla wszystkich, którzy go nienawidzą, gdy mała aplikacja jest załadowana interfejsami i kliknięcie „znajdź definicję” prowadzi nas gdzieś INNE niż źródło problemu ... Czy mogę pożyczyć wyrażenie „Astronauci architektury”? Widzę, że to się przyda.
Gats

6
Jeśli to możliwe, dam ci 1000 punktów za tę odpowiedź. : D
Samuel

8
Mówiłem o szerszym wyniku pogoni za wzorami. Interfejsy dla popularnych interfejsów dobre, dodatkowe warstwy dla pogoni za wzorem, złe.
Gats

186

Interfejs to obietnica (lub umowa).

Jak zawsze z obietnicami - im mniejsze, tym lepiej .


56
Nie zawsze lepiej, po prostu łatwiej utrzymać! :)
Kirk Broadhurst,

5
Nie rozumiem tego. Taka IListjest obietnica. Która jest tutaj mniejsza i lepsza obietnica? To nie jest logiczne.
nawfal

3
W przypadku każdej innej klasy zgodziłbym się. Ale nie dla List<T>, ponieważ AddRangenie jest zdefiniowany w interfejsie.
Jesse de Wit

3
W tym konkretnym przypadku nie jest to pomocne. Najlepsza obietnica jest dotrzymywana. IList<T>składa obietnice, których nie może dotrzymać. Na przykład istnieje Addmetoda, 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>!
ZunTzu,

@ZunTzu Twierdzę, że ktoś przekazujący niezmienny zbiór jako zmienny świadomie złamał prezentowaną umowę - to nie jest problem z samą umową, np 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.
Shelby Oldfield

93

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 IsReadOnlyflagi 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. IsReadOnlynarusza 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


5
Świetna, świetna odpowiedź. Chciałbym dodać, że nawet jeśli IsReadOnlyjest trueto IListmożliwe, nadal możesz modyfikować jego obecnych członków! Być może tę właściwość należy nazwać IsFixedSize?
xofz

(który został przetestowany z zaliczeniem jako Arrayjako IList, więc mógłbym zmodyfikować obecnych członków)
xofz

1
@SamPearson istniała właściwość IsFixedSize na IList w .NET 1, nieuwzględniona w ogólnej IList .NET 2.0 <T>, gdzie została skonsolidowana z IsReadOnly, co prowadziło do zaskakujących zachowań wokół tablic. Szczegółowy blog na temat subtelnych różnic znajduje się tutaj: enterprisecraftsmanship.com/2014/11/22/…
perfekcjonista

7
Doskonała odpowiedź! Ponadto, od .NET 4.6 mamy teraz 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>!
ZunTzu,

37

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.


Jeśli chodzi o twoje ostatnie zdanie na temat używania IEnumerable zamiast IList. Nie zawsze jest to zalecane, weźmy na przykład WPF, który po prostu utworzy opakowujący obiekt IList, dzięki czemu będzie miał wpływ perf - msdn.microsoft.com/en-us/library/bb613546.aspx
HAdes

1
Świetna odpowiedź, gwarancje wydajności to coś, czego interfejs nie może dostarczyć. W niektórych kodach może to być dość ważne, a użycie konkretnych klas komunikuje twoje zamiary, potrzebę konkretnej klasy. Z drugiej strony interfejs mówi: „Muszę tylko wywołać ten zestaw metod, nie sugerując żadnej innej umowy”.
joshperry,

1
@ joshperry Jeśli przeszkadza Ci wydajność interfejsu, jesteś na niewłaściwej platformie. Impo, to jest mikrooptymalizacja.
nawfal

@nawfal Nie komentowałem zalet / wad wydajności interfejsów. Zgadzałem się i komentowałem fakt, że kiedy zdecydujesz się ujawnić konkretną klasę jako argument, możesz przekazać zamiar wykonania. Biorąc LinkedList<T>vs biorąc List<T>vs IList<T>wszyscy przekazują coś z gwarancji wydajności wymaganych przez wywoływany kod.
joshperry

28

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.


1
Bardzo ciekawy sposób myślenia o tym. I dobry sposób myślenia podczas programowania - dzięki.
Peanut

Dobrze powiedziane! Ponieważ interfejs jest bardziej elastyczny, naprawdę powinieneś uzasadnić, co kupuje konkretna implementacja.
Cory House

9
Świetny sposób myślenia. Nadal mogę na to odpowiedzieć: mój powód nazywa się AddRange ()
PPC

4
mój powód nazywa się Add (). czasem chcesz listę, o której wiesz , że z pewnością może zawierać dodatkowe elementy? Zobacz moją odpowiedź.
perfekcjonista

19

IList <T> to interfejs, dzięki któremu można odziedziczyć inną klasę i nadal implementować IList <T>, podczas gdy dziedziczenie List <T> nie pozwala na to.

Na przykład, jeśli istnieje klasa A, a twoja klasa B ją dziedziczy, nie możesz użyć Listy <T>

class A : B, IList<T> { ... }

15

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.


3
Jeśli twoja ExtendedList <T> dziedziczy List <T>, nadal możesz zmieścić ją w umowie List <T>. Ten argument działa tylko wtedy, gdy napiszesz własną implementację IList <T> z sratch (lub przynajmniej bez dziedziczenia List <T>)
PPC

14
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>.


13

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)

  • AddRange
  • AsReadOnly
  • BinarySearch
  • Pojemność
  • Skonwertuj wszystko
  • Istnieje
  • Odnaleźć
  • Znajdź wszystko
  • FindIndex
  • FindLast
  • FindLastIndex
  • Dla każdego
  • GetRange
  • InsertRange
  • LastIndexOf
  • Usuń wszystko
  • RemoveRange
  • Odwrócić
  • Sortować
  • ToArray
  • TrimExcess
  • TrueForAll

1
Nie do końca prawda. Reversei ToArraysą metodami rozszerzenia interfejsu IEnumerable <T>, z którego wywodzi się IList <T>.
Tarec

To dlatego, że mają one sens tylko w Listsi, a nie w innych implementacjach IList.
HankCa

12

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.


7

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


7

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

  1. Dodaj
  2. Jasny
  3. Zawiera
  4. Kopiuj do
  5. GetEnumerator
  6. Indeks
  7. Wstawić
  8. Usunąć
  9. RemoveAt

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


4

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.



2

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.


2

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>


0

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.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.