Dlaczego warto korzystać z IList lub List?


82

Wiem, że pojawiło się wiele postów na ten temat, ale nadal mnie to dezorientuje, dlaczego miałbyś przekazywać interfejs taki jak IList i zwracać interfejs taki jak IList z powrotem zamiast konkretnej listy.

Czytałem wiele postów mówiących, jak to ułatwia późniejszą zmianę implementacji, ale po prostu nie do końca rozumiem, jak to działa.

Powiedz, czy mam tę metodę

  public class SomeClass
    {
        public bool IsChecked { get; set; }
    }

 public void LogAllChecked(IList<SomeClass> someClasses)
    {
        foreach (var s in someClasses)
        {
            if (s.IsChecked)
            {
                // log 
            }
        }
    }

Nie jestem pewien, jak korzystanie z IList pomoże mi w przyszłości.

A jeśli już jestem w metodzie? Czy nadal powinienem używać IList?

public void LogAllChecked(IList<SomeClass> someClasses)
    {
        //why not List<string> myStrings = new List<string>()
        IList<string> myStrings = new List<string>();

        foreach (var s in someClasses)
        {
            if (s.IsChecked)
            {
                myStrings.Add(s.IsChecked.ToString());
            }
        }
    }

Co teraz otrzymam za używanie IList?

public IList<int> onlySomeInts(IList<int> myInts)
    {
        IList<int> store = new List<int>();
        foreach (var i in myInts)
        {
            if (i % 2 == 0)
            {
                store.Add(i);
            }
        }

        return store;
    }

A teraz? Czy jest jakaś nowa implementacja listy int, którą będę musiał zmienić?

Zasadniczo muszę zobaczyć kilka rzeczywistych przykładów kodu pokazujących, jak użycie IList rozwiązałoby jakiś problem, po prostu biorąc List do wszystkiego.

Po przeczytaniu wydaje mi się, że mogłem użyć IEnumberable zamiast IList, ponieważ po prostu przeglądam rzeczy.

Edycja Więc bawiłem się kilkoma moimi metodami, jak to zrobić. Nadal nie mam pewności co do typu zwracanego (czy powinienem uczynić go bardziej konkretnym lub interfejsem).

 public class CardFrmVm
    {
        public IList<TravelFeaturesVm> TravelFeaturesVm { get; set; }
        public IList<WarrantyFeaturesVm> WarrantyFeaturesVm { get; set; }

        public CardFrmVm()
        {
            WarrantyFeaturesVm = new List<WarrantyFeaturesVm>();
            TravelFeaturesVm = new List<TravelFeaturesVm>();
        }
}

 public class WarrantyFeaturesVm : AvailableFeatureVm
    {
    }

 public class TravelFeaturesVm : AvailableFeatureVm
    {
    }

 public class AvailableFeatureVm
    {
        public Guid FeatureId { get; set; }
        public bool HasFeature { get; set; }
        public string Name { get; set; }
    }


        private IList<AvailableFeature> FillAvailableFeatures(IEnumerable<AvailableFeatureVm> avaliableFeaturesVm)
        {
            List<AvailableFeature> availableFeatures = new List<AvailableFeature>();
            foreach (var f in avaliableFeaturesVm)
            {
                if (f.HasFeature)
                {
                                                    // nhibernate call to Load<>()
                    AvailableFeature availableFeature = featureService.LoadAvaliableFeatureById(f.FeatureId);
                    availableFeatures.Add(availableFeature);
                }
            }

            return availableFeatures;
        }

Teraz zwracam IList z powodu prostego faktu, że dodam to do mojego modelu domeny, co ma taką właściwość:

public virtual IList<AvailableFeature> AvailableFeatures { get; set; }

Powyższe jest samym IListem, ponieważ wydaje się, że jest to standard używany z nhibernate. W przeciwnym razie mógłbym zwrócić IEnumberable z powrotem, ale nie jestem pewien. Mimo to nie mogę dowiedzieć się, czego użytkownik będzie potrzebował w 100% (w tym przypadku zwrot betonu ma przewagę).

Edytuj 2

Zastanawiałem się również, co się stanie, jeśli chcę przekazać przez odniesienie w mojej metodzie?

private void FillAvailableFeatures(IEnumerable<AvailableFeatureVm> avaliableFeaturesVm, IList<AvailableFeature> toFill)
            {

                foreach (var f in avaliableFeaturesVm)
                {
                    if (f.HasFeature)
                    {
                                                        // nhibernate call to Load<>()
                        AvailableFeature availableFeature = featureService.LoadAvaliableFeatureById(f.FeatureId);
                        toFill.Add(availableFeature);
                    }
                }
            }

czy miałbym z tym problemy? Ponieważ czy nie mogliby przekazać tablicy (która ma stały rozmiar)? Czy byłoby lepiej może dla konkretnej listy?

Odpowiedzi:


156

Są tutaj trzy pytania: jakiego typu powinienem użyć dla parametru formalnego? Czego powinienem użyć dla zmiennej lokalnej? i czego powinienem użyć jako zwracanego typu?

Parametry formalne:

Zasada jest taka, że nie proś o więcej, niż potrzebujesz . IEnumerable<T>komunikuje „Muszę uzyskać elementy tej sekwencji od początku do końca”. IList<T>komunikuje: „Muszę pobrać i ustawić elementy tej sekwencji w dowolnej kolejności”. List<T>komunikuje: „Muszę pobrać i ustawić elementy tej sekwencji w dowolnej kolejności i akceptuję tylko listy; nie akceptuję tablic”.

Prosząc o więcej, niż potrzebujesz, (1) zmuszasz rozmówcę do niepotrzebnej pracy, aby zaspokoić Twoje niepotrzebne żądania oraz (2) przekazujesz czytelnikowi kłamstwa. Pytaj tylko o to, czego zamierzasz użyć. W ten sposób, jeśli obiekt wywołujący ma sekwencję, nie musi wywoływać na niej ToList, aby spełnić Twoje żądanie.

Zmienne lokalne:

Używaj czego chcesz. To twoja metoda. Tylko Ty możesz zobaczyć wewnętrzne szczegóły implementacji metody.

Rodzaj zwrotu:

Ta sama zasada co poprzednio, odwrócona. Zaoferuj minimum, jakiego wymaga Twój rozmówca. Jeśli dzwoniący wymaga tylko możliwości wyliczenia sekwencji, nadaj im tylko IEnumerable<T>.


9
Skąd jednak wiesz, czego potrzebuje dzwoniący. Na przykład przełączałem jeden z moich typów zwracanych na IList <>, a potem prawdopodobnie po prostu wyliczę je i tak zwróćmy IEnumberable. Następnie spojrzałem na swój widok (mvc) i stwierdziłem, że faktycznie potrzebuję metody zliczania, ponieważ potrzebowałem użyć pętli for. Tak więc we własnej aplikacji nie oszacowałem tego, czego faktycznie potrzebowałem, jak przewidzieć, czego ktoś będzie potrzebował, a czego nie.
chobo2

@ chobo2: W twoim konkretnym przykładzie LINQ Countdziała w O (1), jeśli twój IEnumerablejest ICollection. Oczywiście możesz też po prostu użyć foreachpętli.
Brian,

6
@ chobo2: Jak przewidujesz, jakich metod będzie potrzebował dzwoniący? Wydaje się, że problem należy rozwiązać najpierw. Prawdopodobnie w jakiś sposób wiesz, jakie metody pisać dla osób, które będą do nich dzwonić. Zapytaj tych ludzi, jakie metody chcieliby zwrócić. Twoje pytanie zasadniczo brzmi: „Skąd mam wiedzieć, jakie oprogramowanie pisać?” Wiesz, poznając problemy, które musi rozwiązać Twój klient, i pisząc kod, który rozwiązuje jego problemy.
Eric Lippert,

1
@Eric - powinieneś zaktualizować swoją odpowiedź dla parametrów formalnych, aby zawierała przykład ICollection, w którym wystarczy dodać element. Również wyjaśnienie typu zwrotu powinno brzmieć w stylu „oferuj tylko minimum tego, na co zezwalasz dzwoniącemu”. Jeśli jest to lista tylko do odczytu, zwraca tylko IEnumerable, itd.
Charles Lambert

4
@kvb: Prawie. Rozważ swój pierwszy scenariusz. Możesz ulepszyć algorytm, robiąc, items as IList<T>a jeśli otrzymasz wartość różną od null, użyj ulepszonego kodu. W ten sposób skorzystasz, jeśli możesz, jednocześnie pozwalając klientowi na elastyczność w tym, co przekazuje.
Eric Lippert

33

Najbardziej praktyczny powód, jaki kiedykolwiek widziałem, podał Jeffrey Richter w CLR za pośrednictwem C #.

Wzorzec polega na przyjęciu dla argumentów możliwie najniższej klasy lub interfejsu i zwróceniu możliwie najbardziej szczegółowej klasy lub interfejsu dla typów zwracanych. Daje to wywoływaczom największą elastyczność w przekazywaniu typów do metod i największe możliwości rzutowania / ponownego wykorzystania zwracanych wartości.

Na przykład następująca metoda

public void PrintTypes(IEnumerable items) 
{ 
    foreach(var item in items) 
        Console.WriteLine(item.GetType().FullName); 
}

umożliwia wywołanie metody, przekazując dowolny typ, który można rzutować na wyliczalny . Gdybyś był bardziej szczegółowy

public void PrintTypes(List items)

wtedy, powiedzmy, gdybyś miał tablicę i chciałbyś wydrukować ich nazwy typów na konsoli, musiałbyś najpierw utworzyć nową Listę i wypełnić ją swoimi typami. A jeśli użyjesz implementacji ogólnej, będziesz mógł użyć tylko metody, która działa dla dowolnego obiektu, tylko z obiektami określonego typu.

Mówiąc o typach zwracanych, im bardziej jesteś konkretny, tym bardziej elastyczni mogą być dzwoniący.

public List<string> GetNames()

możesz użyć tego typu powrotu do iteracji nazw

foreach(var name in GetNames())

lub możesz indeksować bezpośrednio w kolekcji

Console.WriteLine(GetNames()[0])

Natomiast jeśli wracasz do mniej konkretnego typu

public IEnumerable GetNames()

musiałbyś masować zwracany typ, aby uzyskać pierwszą wartość

Console.WriteLine(GetNames().OfType<string>().First());

10
Zwróć uwagę, że ta rada jest sprzeczna z odpowiedzią Erica Lipperta zawartą w zaleceniu dotyczącym typów zwrotów. Podejście Jeffreya Richtera daje konsumentom metody największą elastyczność w używaniu zwracanego obiektu w dowolny sposób, podczas gdy Eric daje opiekunom metody największą elastyczność w zakresie zmiany implementacji bez modyfikowania publicznej powierzchni metody. Zwykle postępuję zgodnie z radą Jeffreya dotyczącą wewnętrznego kodu, ale w przypadku biblioteki publicznej prawdopodobnie byłbym bardziej skłonny postępować zgodnie z zaleceniami Erica.
phoog

3
@phoog: Biorąc pod uwagę, skąd pochodzi Eric, nie byłoby zaskoczeniem, że jest bardziej ostrożny w wprowadzaniu zmian. Ale to zdecydowanie ważna kwestia.

Bardzo mocne. Wygląda na to, że są ludzie po obu stronach (powrót nago min lub nie). Rozumiem więcej, dlaczego robisz to ze względu na parametry, które pomagają powstrzymać niepotrzebną konwersję. Nadal nie jestem pewien, co zrobić z typem zwrotu.
chobo2

@ chobo2: Naprawdę, po prostu musisz się zastanowić, czy może zajść potrzeba zmiany typu zwracanego wyniku w przyszłości, jeśli określisz coś dokładniejszego niż ogólnego. Jeśli możesz rozważyć swoją metodę, ustal, że prawdopodobnie nie będziesz zmieniać typu kolekcji zwracanej, wtedy prawdopodobnie bezpieczniej będzie zwrócić dokładniejszy typ. Jeśli nie jesteś pewien lub boisz się, że jeśli zmienisz to w przyszłości, złamiesz kod innych osób, przejdź bardziej ogólnie.

1
+1 Ciekawostki: ta odpowiedź jest ładnym, specyficznym dla CLR przykładem prawa Postela . :)
Dan J

13

IEnumerable<T>pozwala na iterację w kolekcji. ICollection<T>opiera się na tym, a także umożliwia dodawanie i usuwanie elementów. IList<T>umożliwia również dostęp do nich i modyfikowanie ich w określonym indeksie. Ujawniając tę, z którą klient będzie współpracował, możesz dowolnie zmieniać swoją implementację. List<T>zdarza się zaimplementować wszystkie trzy z tych interfejsów.

Jeśli ujawnisz swoją własność jako, a List<T>nawet IList<T>kiedy, wszystko, czego chcesz, aby konsument miał, to możliwość iteracji po kolekcji. Wtedy mogliby zacząć polegać na fakcie, że mogą modyfikować listę. Później, jeśli zdecydujesz się przekonwertować rzeczywisty magazyn danych z a List<T>na a Dictionary<T,U>i ujawnić klucze słownika jako rzeczywistą wartość właściwości (musiałem to zrobić dokładnie wcześniej). Wówczas konsumenci, którzy oczekują, że ich zmiany zostaną odzwierciedlone w Twojej klasie, nie będą już mieli takiej możliwości. To duży problem! Jeśli ujawnisz List<T>jako, IEnumerable<T>możesz łatwo przewidzieć, że Twoja kolekcja nie jest modyfikowana zewnętrznie. Jest to jedna z możliwości ujawniania, List<T>jak każdy z powyższych interfejsów.

Ten poziom abstrakcji idzie w innym kierunku, gdy należy do parametrów metody. Przekazując listę do metody, która akceptuje IEnumerable<T>, możesz być pewien, że lista nie zostanie zmodyfikowana. Kiedy jesteś osobą wdrażającą metodę i mówisz, że akceptujesz metodę, IEnumerable<T>ponieważ wszystko, co musisz zrobić, to powtórzyć tę listę. Następnie osoba wywołująca metodę może wywołać ją z dowolnym wyliczalnym typem danych. Pozwala to na użycie kodu w nieoczekiwany, ale całkowicie prawidłowy sposób.

Z tego wynika, że ​​implementacja metody może reprezentować swoje zmienne lokalne w dowolny sposób. Szczegóły implementacji nie są ujawniane. Dzięki temu możesz zmienić kod na lepszy bez wpływu na osoby dzwoniące do niego.

Nie możesz przewidzieć przyszłości. Zakładanie, że typ nieruchomości zawsze będzie korzystny, ponieważ List<T>natychmiast ogranicza twoją zdolność dostosowania się do nieprzewidzianych oczekiwań twojego kodu. Tak, możesz nigdy nie zmienić tego typu danych z a, List<T>ale możesz być pewien, że jeśli musisz. Twój kod jest na to gotowy.


9

Krótka odpowiedź:

Przekazujesz interfejs, aby bez względu na to, jakiej konkretnej implementacji interfejsu używasz, Twój kod będzie go obsługiwał.

Jeśli używasz konkretnej implementacji listy, inna implementacja tej samej listy nie będzie obsługiwana przez Twój kod.

Przeczytaj trochę o dziedziczeniu i polimorfizmie .


8

Oto przykład: Kiedyś miałem projekt, w którym nasze listy stały się bardzo duże, a wynikająca z tego fragmentacja dużej sterty obiektów obniżała wydajność. Zamieniliśmy List na LinkedList. LinkedList nie zawiera tablicy, więc nagle prawie nie używaliśmy dużego stosu obiektów.

Przede wszystkim używaliśmy list jako IEnumerable<T>, więc nie było potrzeby dalszej zmiany. (I tak, polecałbym zadeklarowanie odwołań jako IEnumerable, jeśli wszystko, co robisz, to je wyliczanie). W kilku miejscach potrzebowaliśmy indeksatora list, więc napisaliśmy nieefektywne IList<T>opakowanie wokół połączonych list. Rzadko potrzebowaliśmy indeksatora list, więc nieefektywność nie była problemem. Gdyby tak było, moglibyśmy dostarczyć inną implementację IList, być może jako zbiór wystarczająco małych tablic, która byłaby bardziej efektywnie indeksowana, jednocześnie unikając dużych obiektów.

W końcu może być konieczna wymiana implementacji z dowolnego powodu; wydajność to tylko jedna możliwość. Bez względu na przyczynę użycie możliwie najmniej pochodnego typu zmniejszy potrzebę zmian w kodzie po zmianie określonego typu obiektów w czasie wykonywania.


3

Wewnątrz metody należy używać varzamiast IListlub List. Gdy źródło danych zmieni się i onlySomeIntszacznie pochodzić z metody, metoda przetrwa.

Powodem używania IListzamiast Listjako parametrów jest to, że wiele rzeczy implementuje IList(List i [], jako dwa przykłady), ale tylko jedna rzecz implementuje List. Bardziej elastyczne jest kodowanie w interfejsie.

Jeśli wyliczasz tylko wartości, powinieneś używać IEnumerable. Każdy typ typu danych, który może zawierać więcej niż jedną wartość, implementuje IEnumerable(lub powinien) i sprawia, że ​​metoda jest niezwykle elastyczna.


13
Powiedzenie, że powinien użyć „var”, jest całkowicie błędne. Nie ma znaczenia, czego tam używa - to kwestia stylu. Nie wpływa na sygnaturę metody i jest ustawiana w kamieniu w czasie kompilacji. Zamiast tego powinieneś pomóc mu uporać się z dezorientacją związaną z deklarowaniem swojego lokalnego, takiego jak IList foo = new List - w tym miejscu wyraźnie leży jego zmieszanie.
x0n

Więc w zasadzie mówisz, że chcesz wziąć IList tylko dla tego, że jeśli chcą wysłać tablicę, której nie muszą robić najpierw .ToList? Co powiesz na zwrot? Nie rozumiem, co masz na myśli, mówiąc „metoda przetrwa”, gdy używasz vars? Wiem, że będzie to trochę więcej pracy, ale czy nie będziesz musiał po prostu zmienić tego na nowy typ? Więc może IList <int> do IList <String>?
chobo2

To prawda, że ​​celem „use var” była raczej sugestia, aby nie martwić się o to w samej metodzie i bardziej skupić się na tym, jak wygląda ona dla konsumentów.
Bryan Boettcher,

Zgadzam się z @ x0n: var jest poważnie nadużywany i nie pomoże w wyjaśnieniu niczego.
Ten Chuck Guy,

1

Używanie IList zamiast List znacznie ułatwia pisanie testów jednostkowych. Umożliwia wykorzystanie biblioteki „Mocking” do przekazywania i zwracania danych.

Innym ogólnym powodem używania interfejsów jest ujawnienie minimalnej ilości wiedzy niezbędnej użytkownikowi obiektu.

Rozważmy (wymyślony) przypadek, w którym mam obiekt danych, który implementuje IList.

public class MyDataObject : IList<int>
{
    public void Method1()
    {
       ...
    }
    // etc
}

Powyższe funkcje dotyczą tylko możliwości iteracji po liście. Idealnie byłoby, gdyby nie musieli wiedzieć, kto wdraża tę listę lub jak ją wdrażają.

W twoim przykładzie IEnumerable jest lepszym wyborem, jak myślisz.


1

Zawsze dobrym pomysłem jest maksymalne zmniejszenie zależności między kodem.

Mając to na uwadze, najbardziej sensowne jest przekazywanie typów z najmniejszą możliwą liczbą zależności zewnętrznych i zwracanie tych samych. Jednak może się to różnić w zależności od widoczności metod i ich sygnatur.

Jeśli metody stanowią część interfejsu, należy je zdefiniować przy użyciu typów dostępnych dla tego interfejsu. Typy konkretne prawdopodobnie nie będą dostępne dla interfejsów, więc musiałyby zwrócić typy inne niż konkretne. Chciałbyś to zrobić, jeśli na przykład tworzysz framework.

Jeśli jednak nie piszesz frameworka, korzystne może być przekazanie parametru z najsłabszymi możliwymi typami (tj. Klasami bazowymi, interfejsami lub nawet delegatami) i zwrócenie konkretnych typów. Daje to wywołującemu możliwość zrobienia jak największej ilości zwracanego obiektu, nawet jeśli jest on rzutowany jako interfejs. Jednak sprawia to, że metoda jest bardziej delikatna, ponieważ każda zmiana zwracanego typu obiektu może spowodować uszkodzenie kodu wywołującego. Jednak w praktyce generalnie nie jest to poważny problem.


1

Akceptujesz Interface jako parametr metody, ponieważ umożliwia to obiektowi wywołującemu przesyłanie różnych konkretnych typów jako argumentów. Biorąc pod uwagę twoją przykładową metodę LogAllChecked, parametr someClasses może mieć różne typy, a dla osoby piszącej metodę wszystkie mogą być równoważne (tj. Napisałbyś dokładnie ten sam kod niezależnie od typu parametru). Ale dla osoby wywołującej metodę może to mieć ogromne znaczenie - jeśli ma tablicę i pytasz o listę, musi zmienić tablicę na listę lub vv przy każdym wywołaniu metody, całkowita strata czas z punktu widzenia programisty i wydajności.

To, czy zwrócisz interfejs, czy konkretny typ, zależy od tego, co chcesz, aby wywołujący zrobili z utworzonym obiektem - jest to decyzja projektowa interfejsu API i nie ma sztywnej i szybkiej reguły. Musisz porównać ich zdolność do pełnego wykorzystania obiektu z ich zdolnością do łatwego wykorzystania części funkcjonalności obiektów (i oczywiście czy CHCESZ, aby w pełni wykorzystywały obiekt). Na przykład, jeśli zwrócisz IEnumerable, ograniczysz je do iteracji - nie mogą dodawać ani usuwać elementów z obiektu, mogą działać tylko na obiektach. Jeśli chcesz udostępnić kolekcję poza klasą, ale nie chcesz pozwolić wywołującemu na zmianę kolekcji, jest to jeden ze sposobów zrobienia tego. Z drugiej strony, jeśli zwracasz pustą kolekcję, której oczekujesz / chcesz, aby się zapełniła,


0

Oto moja odpowiedź w świecie .NET 4.5+ .

Zastosowanie IList <T> i IReadonlyList <T> ,
              zamiast List <T> , ponieważ ReadonlyList <T> nie istnieje.

IList <T> wygląda tak spójnie z IReadonlyList <T>

  • Użyj IEnumerable <T> dla minimalnej ekspozycji (właściwości) lub wymagania (parametru), jeśli foreach jest jedynym sposobem na użycie tego.
  • Użyj IReadonlyList <T>, jeśli musisz również udostępnić / użyć Count i [] indeksatora.
  • Użyj IList <T>, jeśli zezwalasz również wywołującym na dodawanie / aktualizowanie / usuwanie elementów

ponieważ List <T> implementuje IReadonlyList <T> , nie wymaga żadnego jawnego rzutowania.

Przykładowe zajęcia:

// manipulate the list within the class
private List<int> _numbers;

// callers can add/update/remove elements, but cannot reassign a new list to this property
public IList<int> Numbers { get { return _numbers; } }

// callers can use: .Count and .ReadonlyNumbers[idx], but cannot add/update/remove elements
public IReadOnlyList<int> ReadonlyNumbers { get { return _numbers; } }
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.