Tablica kontra lista <T>: kiedy używać której?


592
MyClass[] array;
List<MyClass> list;

Jakie są scenariusze, w których jeden jest lepszy od drugiego? I dlaczego?


9
Tablice są raczej przestarzałe, jak widać w popularnej dyskusji tutaj. Wskazano również tutaj , a przez naszego gospodarza na blogu .
gimel

9
Jeśli się nie mylę, List <> ma tablicę jako wewnętrzną strukturę. Ilekroć wewnętrzna tablica jest wypełniona, po prostu skopiuj zawartość do tablicy, która jest dwa razy większa (lub inna stała razy bieżąca wielkość). en.wikipedia.org/wiki/Dynamic_array
Ykok

Ykok: To, co mówisz, wydaje się słuszne, znalazłem tutaj kod źródłowy Listy <> .
Carol

19
@gimel Argumentowanie, że tablice są przestarzałe, może być nieco odważne
awdz9nld

Odpowiedzi:


580

W rzeczywistości rzadko jest potrzebne użycie tablicy. Zdecydowanie użyć List<T>w dowolnym momencie dodawać / dane usunąć, ponieważ zmiana rozmiaru tablic jest drogie. Jeśli wiesz, że dane mają ustaloną długość i chcesz przeprowadzić mikrooptymalizację z jakiegoś bardzo konkretnego powodu (po analizie porównawczej), tablica może być przydatna.

List<T>oferuje znacznie więcej funkcji niż tablica (chociaż LINQ nieco ją wyrówna) i prawie zawsze jest właściwym wyborem. paramsOczywiście z wyjątkiem argumentów. ;-p

Jako licznik - List<T>jest jednowymiarowy; gdzie-jak masz prostokątne (itp.) tablice, takie jak int[,]lub string[,,]- ale istnieją inne sposoby modelowania takich danych (jeśli potrzebujesz) w modelu obiektowym.

Zobacz też:

To powiedziawszy, często używam tablic w mojej sieci protobuf projekcie ; całkowicie za realizację:

  • robi dużo przesuwania bitów, więc byte[] jest prawie niezbędny do kodowania;
  • Używam lokalnej walcowanie byte[] bufora który wypełniam przed przesłaniem do strumienia bazowego (i vv); szybciej niż BufferedStreamitp .;
  • wewnętrznie wykorzystuje model obiektów oparty na macierzy ( Foo[]zamiastList<Foo> ), ponieważ rozmiar jest ustalany po zbudowaniu i musi być bardzo szybki.

Ale to zdecydowanie wyjątek; w przypadku ogólnego przetwarzania biznesowego List<T>wygrywa za każdym razem.


8
Argument dotyczący zmiany rozmiaru jest całkowicie poprawny. Jednak ludzie wolą listy, nawet jeśli nie jest wymagana zmiana rozmiaru. Czy w tym ostatnim przypadku istnieje solidny, logiczny argument, czy też jest to nic innego jak „tablice nie są modne”?
Frederick The Fool

6
„Zdecydowanie używaj Listy <T> za każdym razem, gdy chcesz dodać / usunąć dane, ponieważ zmiana rozmiaru tablic jest droga.” Lista <T> używa tablicy wewnętrznie. Czy myślałeś o LinkedList <T>?
dan-gph

14
Więcej funkcji == bardziej złożony == niedobry, chyba że potrzebujesz tych funkcji. Ta odpowiedź zasadniczo wymienia powody, dla których tablice są lepsze, ale wyciąga przeciwny wniosek.
Eamon Nerbonne

12
@EamonNerbonne, jeśli nie używasz tych funkcji, mogę prawie zagwarantować, że nie będą cię skrzywdzić ... ale: liczba kolekcji, które nigdy nie wymagają mutacji, jest , moim zdaniem, znacznie mniejsza niż tych, które są zmutowany
Marc Gravell

7
@MarcGravell: to zależy od twojego stylu kodowania. Z mojego doświadczenia wynika, że ​​praktycznie żadna kolekcja nie jest nigdy mutowana. To jest; kolekcje są pobierane z bazy danych lub budowane z jakiegoś źródła, ale dalsze przetwarzanie odbywa się zawsze poprzez odtworzenie nowej kolekcji (np. mapy / filtra itp.). Nawet tam, gdzie konieczna jest mutacja pojęciowa, najprościej jest po prostu wygenerować nową kolekcję. Zawsze mutuję kolekcję jako optymalizację wydajności, a takie optymalizacje są zwykle lokalne i nie ujawniają mutacji konsumentom API.
Eamon Nerbonne

121

Naprawdę sama odpowiedź, aby dodać link, który mnie zaskakuje, nie została jeszcze wspomniana: wpis Erica Lipperta na blogu „Tablice uważane za nieco szkodliwe”.

Możesz sądzić po tytule, że sugeruje stosowanie kolekcji wszędzie tam, gdzie jest to praktyczne - ale jak słusznie zauważa Marc, istnieje wiele miejsc, w których tablica jest naprawdę jedynym praktycznym rozwiązaniem.


2
W końcu zacząłem czytać to ponad 3 lata później haha. Dobry artykuł, teraz dobry artykuł. :)
Spencer Ruport,

21

Niezależnie od innych zalecających odpowiedzi List<T>, będziesz chciał używać tablic podczas obsługi:

  • obraz mapy bitowej
  • inne struktury danych niskiego poziomu (tj. protokoły sieciowe)

1
Dlaczego dla protokołów sieciowych? Czy nie wolałbyś użyć tutaj niestandardowych struktur i nadać im specjalny serializator lub jawny układ pamięci? Co więcej, co przemawia przeciwko użyciu List<T>tablicy tutaj zamiast bajtów?
Konrad Rudolph

8
@Konrad - cóż, na początek, Stream.Read i Stream.Write pracują z bajtem [], podobnie jak Kodowanie itp. ...
Marc Gravell

12

Chyba że naprawdę interesujesz się wydajnością, a przez to mam na myśli: „Dlaczego używasz .Net zamiast C ++?” powinieneś trzymać się Listy <>. Łatwiej jest go utrzymać i wykonuje całą brudną robotę polegającą na zmianie rozmiaru tablicy za kulisami. (W razie potrzeby Lista <> jest całkiem sprytna w wyborze rozmiarów tablic, więc zwykle nie musi).


15
„Dlaczego używasz .Net zamiast C ++?” XNA
Bengt,

6

Tablice powinny być stosowane zamiast List, gdy niezmienność samej kolekcji jest częścią umowy między kodem klienta i dostawcy (niekoniecznie niezmienność elementów w kolekcji) ORAZ, gdy IEnumerable nie jest odpowiedni.

Na przykład,

var str = "This is a string";
var strChars = str.ToCharArray();  // returns array

Oczywiste jest, że modyfikacja „strChars” nie spowoduje mutacji oryginalnego obiektu „str”, niezależnie od poziomu wiedzy na temat implementacji typu „str”.

Ale załóżmy, że

var str = "This is a string";
var strChars = str.ToCharList();  // returns List<char>
strChars.Insert(0, 'X');

W takim przypadku z samego fragmentu kodu nie jest jasne, czy metoda wstawiania zmutuje oryginalny obiekt „str”. Wymagana jest znajomość ciągu znaków na poziomie implementacji, aby dokonać tego ustalenia, co przełamuje podejście Projekt na podstawie umowy. W przypadku String nie jest to wielka sprawa, ale może to być ważna sprawa w prawie każdym innym przypadku. Ustawienie listy jako tylko do odczytu pomaga, ale powoduje błędy w czasie wykonywania, a nie w czasie kompilacji.


Jestem stosunkowo nowy w języku C #, ale nie jest dla mnie jasne, dlaczego zwrócenie listy sugerowałoby zmienność oryginalnych danych w sposób, którego nie zwróciłoby tablica. Wydaje mi się, że metoda, której nazwa zaczyna się od, Toma na celu utworzenie obiektu, który nie ma możliwości modyfikacji oryginalnej instancji, w przeciwieństwie do strChars as char[]tego, jeśli poprawna sugeruje, że możesz teraz zmodyfikować oryginalny obiekt.
Tim MB

@TimMB Niezmienność kolekcji (nie można dodawać ani zdalne elementy) i niezmienność elementów w kolekcji. Odniosłem się do tego drugiego, podczas gdy prawdopodobnie łączysz je ze sobą. Zwrócenie tablicy zapewnia klientowi, że nie może dodawać / usuwać elementów. Jeśli tak, to ponownie przydziela tablicę i zapewnia, że ​​nie wpłynie to na oryginał. Zwracając listę, nie ma takich zapewnień, a oryginalność może zostać zmieniona (zależy od implementacji). Zmiana elementów w kolekcji (tablicy lub listy) może mieć wpływ na oryginał, jeśli typ elementu nie jest strukturą.
Herman Schoenfeld,

Dziękuję za wyjaśnienie. Nadal jestem zdezorientowany (prawdopodobnie dlatego, że pochodzę ze świata C ++). Jeśli strwewnętrznie korzysta z tablicy i ToCharArrayzwraca odwołanie do tej tablicy, klient może mutować str, zmieniając elementy tej tablicy, nawet jeśli rozmiar pozostaje stały. Jednak piszesz „Oczywiste jest, że modyfikacja„ strChars ”nie spowoduje mutacji oryginalnego obiektu„ str ”. Czego tu brakuje? Z tego co widzę, w obu przypadkach klient może mieć dostęp do wewnętrznej reprezentacji i, niezależnie od rodzaju, pozwoliłoby to na jakąś mutację.
Tim MB

3

Jeśli wiem dokładnie ile elementów Idę do potrzebujących, powiedzieć muszę 5 elementów i tylko kiedykolwiek 5 elementów potem używać tablicy. W przeciwnym razie po prostu używam Listy <T>.


1
Dlaczego nie użyłbyś Listy <T> w przypadku, gdy znasz liczbę elementów?
Oliver,

3

W większości Listprzypadków wystarczyłoby użyć . A Listwykorzystuje wewnętrzną tablicę do obsługi swoich danych i automatycznie zmienia rozmiar tablicy podczas dodawania większej liczby elementów Listniż jej obecna pojemność, co sprawia, że ​​jest łatwiejsza w użyciu niż tablica, w której trzeba wcześniej poznać pojemność.

Zobacz http://msdn.microsoft.com/en-us/library/ms379570(v=vs.80).aspx#datastructures20_1_topic5, aby uzyskać więcej informacji na temat list w języku C # lub po prostu dekompilować System.Collections.Generic.List<T>.

Jeśli potrzebujesz danych wielowymiarowych (na przykład za pomocą matrycy lub w programowaniu graficznym), prawdopodobnie skorzystasz z nich array.

Jak zawsze, jeśli problemem jest pamięć lub wydajność, zmierz to! W przeciwnym razie możesz przyjmować fałszywe założenia dotyczące kodu.


1
Cześć, czy możesz wyjaśnić, dlaczego „Czas wyszukiwania listy to O (n)” jest prawdą? O ile wiem Lista <T> używa tablicy za kulisami.
ważka

1
@dragonfly masz całkowitą rację. Źródło . Wówczas założyłem, że implementacja używa wskaźników, ale od tego czasu nauczyłem się inaczej. Z linku powyżej: „Pobieranie wartości tej właściwości jest operacją O (1); ustawienie właściwości jest również operacją O (1). ”
Sune Rievers,

2

Tablice vs. Listy to klasyczny problem związany z utrzymywaniem a wydajnością. Zasadą, którą przestrzegają prawie wszyscy programiści, jest to, że powinieneś strzelać dla obu, ale gdy wejdą w konflikt, wybierz łatwość konserwacji zamiast wydajności. Wyjątkiem od tej reguły jest sytuacja, w której wydajność już okazała się problemem. Jeśli zastosujesz tę zasadę do Arrays vs. Listy, a następnie otrzymujesz to:

Używaj mocno wpisywanych list, dopóki nie napotkasz problemów z wydajnością. Jeśli napotkasz problem z wydajnością, podejmij decyzję, czy rezygnacja z macierzy przyniesie korzyści Twojemu rozwiązaniu z wydajnością bardziej niż będzie to niekorzystne dla rozwiązania pod względem konserwacji.


1

Inną sytuacją, o której jeszcze nie wspomniano, jest sytuacja, w której będziemy mieli dużą liczbę elementów, z których każdy składa się ze stałej wiązki powiązanych, ale niezależnych zmiennych, sklejonych razem (np. Współrzędne punktu lub wierzchołki trójkąta 3d). Szereg struktur pola odsłoniętego pozwoli skutecznie zmodyfikować jego elementy „na miejscu” - co jest niemożliwe w przypadku jakiegokolwiek innego typu kolekcji. Ponieważ tablica struktur utrzymuje swoje elementy kolejno w pamięci RAM, sekwencyjny dostęp do elementów tablicy może być bardzo szybki. W sytuacjach, gdy kod będzie musiał wykonać wiele kolejnych przejść przez tablicę, tablica struktur może przewyższać tablicę lub inny zbiór odwołań do obiektów klasy o współczynnik 2: 1; dalej,

Chociaż tablic nie można zmieniać rozmiaru, nie jest trudne, aby kod przechowywał odwołanie do tablicy wraz z liczbą używanych elementów i zamieniał tablicę na większą, w razie potrzeby. Alternatywnie, można łatwo napisać kod dla typu, który zachowuje się bardzo podobnie, List<T>ale ujawnia swój magazyn zaplecza, umożliwiając w ten sposób powiedzenie albo, MyPoints.Add(nextPoint);albo MyPoints.Items[23].X += 5;. Zauważ, że ten ostatni niekoniecznie rzuciłby wyjątek, gdyby kod próbował uzyskać dostęp poza końcem listy, ale użycie byłoby inaczej koncepcyjnie podobne do List<T>.


To, co opisałeś, to Lista <>. Istnieje moduł indeksujący, dzięki któremu można uzyskać bezpośredni dostęp do podstawowej tablicy, a lista <> zachowa rozmiar.
Carl

@Carl: Biorąc pod uwagę np Point[] arr;. Kod może powiedzieć np arr[3].x+=q;. Używając np List<Point> list. Należałoby zamiast tego powiedzieć Point temp=list[3]; temp.x+=q; list[3]=temp;. Byłoby pomocne, gdyby List<T>miał metodę Update<TP>(int index, ActionByRefRef<T,TP> proc, ref TP params). kompilatory i może okazać list[3].x+=q;się {list.Update(3, (ref int value, ref int param)=>value+=param, ref q);, ale nie ma takiego funkcja istnieje.
supercat

Dobre wieści. To działa. list[0].X += 3;doda 3 do właściwości X pierwszego elementu listy. I listto List<Point>i Pointto klasa z X i Y właściwości
Carl

1

Listy w .NET są opakowaniami w tablice i używają tablicy wewnętrznie. Złożoność czasowa operacji na listach jest taka sama, jak w przypadku tablic, jednak jest nieco więcej narzutu przy całej dodatkowej funkcjonalności / łatwości korzystania z list (takich jak automatyczne zmienianie rozmiaru i metody, które są dostarczane z klasą listy). W zasadzie polecam używanie list we wszystkich przypadkach, chyba że istnieje ważny powód, aby tego nie robić, na przykład jeśli musisz napisać bardzo zoptymalizowany kod lub pracujesz z innym kodem zbudowanym wokół tablic.


0

Myślę, że zamiast pragnienia porównania funkcji każdego typu danych, najbardziej pragmatyczną odpowiedzią jest to, że „różnice prawdopodobnie nie są tak ważne z punktu widzenia tego, co należy osiągnąć, zwłaszcza że oba programy implementują IEnumerable, więc przestrzegaj popularnej konwencji i używaj Listdopóki nie masz powodu, aby tego nie robić, w którym to momencie prawdopodobnie będziesz miał powód do używania tablicy nad List”.

Przez większość czasu w zarządzanym kodzie będziesz chciał preferować kolekcje, które są tak łatwe w obsłudze, jak to możliwe, zamiast martwić się mikrooptymalizacjami.


0

Mogą być niepopularne, ale jestem fanem tablic w projektach gier. - Szybkość iteracji może być ważna w niektórych przypadkach, foreach na tablicy ma znacznie mniejszy narzut, jeśli nie robisz wiele na element - Dodawanie i usuwanie nie jest trudne z funkcjami pomocniczymi - Jest wolniejsze, ale w przypadkach, gdy budujesz go tylko raz to może nie mieć znaczenia - W większości przypadków marnuje się mniej dodatkowej pamięci (ma to znaczenie tylko w przypadku tablic struktur) - Nieco mniej śmieci i wskaźników i pogoni za wskaźnikami

To powiedziawszy, w praktyce korzystam z Listy znacznie częściej niż z Tablic, ale każde z nich ma swoje miejsce.

Byłoby miło, gdyby List zawierał wbudowany typ, aby mogli zoptymalizować koszty opakowania i wyliczenia.


0

Wypełnianie listy jest łatwiejsze niż tablica. W przypadku tablic musisz znać dokładną długość danych, ale w przypadku list wielkość danych może być dowolna. I możesz przekonwertować listę na tablicę.

List<URLDTO> urls = new List<URLDTO>();

urls.Add(new URLDTO() {
    key = "wiki",
    url = "https://...",
});

urls.Add(new URLDTO()
{
    key = "url",
    url = "http://...",
});

urls.Add(new URLDTO()
{
    key = "dir",
    url = "https://...",
});

// convert a list into an array: URLDTO[]
return urls.ToArray();

0

Ponieważ nikt nie wspomina: w języku C # tablica jest listą. MyClass[]i List<MyClass>oba wdrażają IList<MyClass>. (np. void Foo(IList<int> foo)można nazwać jak Foo(new[] { 1, 2, 3 })lub Foo(new List<int> { 1, 2, 3 }))

Tak więc, jeśli piszesz metodę, która akceptuje List<MyClass>argument jako argument, ale używa tylko podzbioru funkcji, możesz zadeklarować jako IList<MyClass>dla wygody osoby dzwoniącej.

Detale:


„W języku C # tablica jest listą” To nieprawda; tablica nie jest a List, tylko implementuje IListinterfejs.
Rufus L

-1

Zależy to całkowicie od kontekstów, w których potrzebna jest struktura danych. Na przykład, jeśli tworzysz elementy, które mają być używane przez inne funkcje lub usługi za pomocą Listy, to idealny sposób na osiągnięcie tego.

Teraz, jeśli masz listę przedmiotów i chcesz je tylko wyświetlić, powiedzmy, że na tablicy strony internetowej jest pojemnik, którego musisz użyć.


1
Jeśli masz listę elementów i chcesz je tylko wyświetlić, to co jest złego w korzystaniu z listy, którą już masz? Co oferuje tutaj tablica?
Marc Gravell

1
A jeśli chodzi o „tworzenie elementów, które mają być używane przez inne funkcje lub usługi”, tak naprawdę wolałbym blok iteracyjny z IEnumerable<T>- wtedy mogę przesyłać strumieniowo obiekty niż je buforować.
Marc Gravell

-1

Warto wspomnieć o zdolności do rzucania na miejscu.

interface IWork { }
class Foo : IWork { }

void Test( )
{
    List<Foo> bb = new List<Foo>( );
    // Error: CS0029 Cannot implicitly convert type 'System.Collections.Generic.List<Foo>' to 'System.Collections.Generic.List<IWork>'
    List<IWork> cc = bb; 

    Foo[] bbb = new Foo[4];
    // Fine
    IWork[] ccc = bbb;
}

Tak więc tablica oferuje nieco większą elastyczność, gdy jest używana jako typ zwracany lub argument funkcji.

IWork[] GetAllWorks( )
{
    List<Foo> fooWorks = new List<Foo>( );
    return fooWorks.ToArray( ); // Fine
}

void ExecuteWorks( IWork[] works ) { } // Also accept Foo[]

Należy zauważyć, że wariancja tablicy jest zepsuta .
mcarton
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.