Gdy aplikacja ma przechowywać zmienną liczbę elementów, najczęściej sięgam po List
Lista w C# sprawdza się wtedy, gdy dane mają kolejność i mogą się zmieniać
-
List
przechowuje elementy jednego typu i pozwala dynamicznie zmieniać ich liczbę. - Elementy są indeksowane od 0, zachowują kolejność dodania i mogą się powtarzać.
- Add, Remove, Contains i Sort rozwiązują większość codziennych zadań.
- Właściwość Capacity pomaga ograniczyć koszt wielokrotnego powiększania listy.
- Do szybkiego wyszukiwania po kluczu lepszy będzie Dictionary
.
Czym jest List i kiedy warto go wybrać
List
W przeciwieństwie do zwykłej tablicy lista nie wymaga znajomości końcowego rozmiaru. Możesz zacząć od pustej kolekcji, a później dodawać rekordy pobrane z formularza, API albo bazy danych. Z mojego doświadczenia wynika, że właśnie ta elastyczność sprawia, że początkujący programiści używają jej bardzo często.
using System.Collections.Generic;
List technologie = new List
{
"C#",
"ASP.NET Core",
"SQL"
};
Lista zachowuje kolejność elementów, pozwala na duplikaty i udostępnia indeksowanie od zera. Oznacza to, że pierwszy element odczytasz przez technologie[0], a liczbę elementów sprawdzisz za pomocą właściwości Count.
Jak tworzyć listy i dodawać do nich dane
Najbardziej klasyczna składnia korzysta z konstruktora new List
List liczby = new();
liczby.Add(10);
liczby.Add(20);
liczby.Add(30);
liczby.AddRange(new[] { 40, 50 });
Console.WriteLine(liczby.Count);
Metoda Add dopisuje jeden element na końcu, a AddRange pozwala dodać całą kolekcję. Przy większych porcjach danych wybieram drugą opcję, bo kod jest czytelniejszy i często ogranicza liczbę operacji związanych z powiększaniem wewnętrznej tablicy.
W C# 12 dostępne są również wyrażenia kolekcji, dzięki którym listę można zapisać bardzo zwięźle.
List punkty = [10, 20, 30, 40];
Nie każda lista musi być tworzona bez określenia rozmiaru. Jeżeli wiem, że wczytam około 1000 elementów, mogę przekazać przewidywaną pojemność już w konstruktorze.
List logi = new List(1000);
To nie blokuje dodania większej liczby elementów. Ustawia jedynie początkową wartość Capacity, czyli miejsce zarezerwowane wewnątrz kolekcji.
Najważniejsze operacje na liście
Codzienna praca z listą zwykle sprowadza się do kilku metod. Najważniejsze jest rozróżnienie między Count, które mówi, ile elementów faktycznie znajduje się w kolekcji, a Capacity, która opisuje zarezerwowane miejsce.
| Operacja | Przykład | Zastosowanie |
|---|---|---|
| Dodanie elementu | lista.Add(element) |
Dopisanie wartości na końcu |
| Dodanie wielu elementów | lista.AddRange(dane) |
Połączenie danych z inną kolekcją |
| Odczyt po indeksie | lista[2] |
Szybki dostęp do konkretnej pozycji |
| Wyszukiwanie | lista.Contains(element) |
Sprawdzenie, czy wartość istnieje |
| Usunięcie wartości | lista.Remove(element) |
Usunięcie pierwszego pasującego elementu |
| Usunięcie po indeksie | lista.RemoveAt(2) |
Usunięcie elementu z konkretnej pozycji |
| Filtrowanie | lista.RemoveAll(x => x < 0) |
Usunięcie elementów spełniających warunek |
| Sortowanie | lista.Sort() |
Ułożenie elementów rosnąco |
Przy odczycie indeksu trzeba uważać na zakres. Próba użycia indeksu mniejszego od zera albo większego lub równego Count zakończy się wyjątkiem ArgumentOutOfRangeException. Jeżeli nie masz pewności, czy element istnieje, najpierw sprawdź liczbę elementów albo użyj bezpiecznego wyszukiwania.
if (technologie.Count > 0)
{
Console.WriteLine(technologie[0]);
}
Usuwanie elementów bez typowych błędów
Jedna z częstszych pułapek pojawia się wtedy, gdy ktoś usuwa elementy wewnątrz pętli foreach. Lista zmienia wtedy swoją strukturę w trakcie iterowania, co może doprowadzić do wyjątku albo pominięcia części danych.
List liczby = [1, 2, 3, 4, 5, 6];
liczby.RemoveAll(x => x % 2 == 0);
RemoveAll jest w takim przypadku najczytelniejszym rozwiązaniem. Gdy potrzebujesz bardziej złożonego warunku, możesz przejść po indeksach od końca do początku. Usunięcie elementu nie zmieni wtedy pozycji tych, które dopiero mają zostać sprawdzone.
for (int i = liczby.Count - 1; i >= 0; i--)
{
if (liczby[i] < 0)
{
liczby.RemoveAt(i);
}
}
Metoda Remove usuwa tylko pierwsze wystąpienie wskazanej wartości. Jeśli lista zawiera kilka takich samych elementów, użyj RemoveAll, gdy chcesz usunąć je wszystkie.
Wydajność, Capacity i sposób przechowywania danych
Lista zapewnia szybki dostęp po indeksie, ponieważ elementy znajdują się w wewnętrznej tablicy. Odczyt wartości przez indeks jest operacją stałoczasową, natomiast wyszukiwanie przez Contains sprawdza elementy po kolei i przy dużych kolekcjach może być wyraźnie wolniejsze.
| Operacja | Typowy koszt |
|---|---|
| Odczyt po indeksie | O(1) |
| Dodanie na końcu | Amortyzacyjnie O(1) |
| Wstawienie w środku | O(n) |
| Usunięcie ze środka | O(n) |
| Contains lub IndexOf | O(n) |
| Sortowanie | Zwykle O(n log n) |
Gdy zabraknie miejsca, lista tworzy większą tablicę i kopiuje do niej dotychczasowe elementy. Dokumentacja Microsoftu wskazuje, że podanie przewidywanej pojemności ogranicza koszt wielokrotnych realokacji. Nie ma sensu ustawiać ogromnej wartości na zapas, ale przy znanym rozmiarze danych jest to prosta optymalizacja.
Po zakończeniu budowania listy możesz sprawdzić jej parametry.
Console.WriteLine($"Elementy: {lista.Count}");
Console.WriteLine($"Pojemność: {lista.Capacity}");
Nie myl jednak optymalizacji z przedwczesnym komplikowaniem kodu. Dla kilkudziesięciu lub kilkuset elementów różnica zwykle będzie pomijalna. Capacity zaczyna mieć praktyczne znaczenie przy dużych kolekcjach, częstym dodawaniu danych albo kodzie wykonywanym tysiące razy.
List a tablica i inne kolekcje
Najlepsza kolekcja zależy od operacji, które wykonujesz najczęściej. Sam rozmiar danych nie wystarczy do podjęcia dobrej decyzji, bo lista, zbiór i słownik rozwiązują różne problemy.
| Kolekcja | Wybierz ją, gdy | Ograniczenie |
|---|---|---|
| List |
Potrzebujesz kolejności, indeksów i dynamicznego rozmiaru | Wyszukiwanie wartości jest liniowe |
| T[] | Rozmiar jest stały i zależy Ci na prostocie | Nie można łatwo zmieniać długości |
| HashSet |
Chcesz przechowywać unikalne wartości i szybko sprawdzać ich obecność | Nie służy do pracy z indeksami |
| Dictionary |
Szukasz wartości po kluczu | Każdy klucz musi być unikalny |
| LinkedList |
Często wstawiasz lub usuwasz elementy w znanych węzłach | Dostęp po indeksie jest niepraktyczny |
Jeżeli tworzysz listę użytkowników i chcesz wyświetlić ich w ustalonej kolejności, List
Praktyczny przykład listy obiektów
W prawdziwych aplikacjach lista rzadko przechowuje same liczby. Znacznie częściej zawiera obiekty, na przykład produkty, zadania albo użytkowników.
public class Zadanie
{
public string Tytul { get; set; } = "";
public bool Ukonczone { get; set; }
}
List zadania =
[
new Zadanie { Tytul = "Przygotować formularz", Ukonczone = false },
new Zadanie { Tytul = "Napisać testy", Ukonczone = true }
];
foreach (Zadanie zadanie in zadania)
{
Console.WriteLine($"{zadanie.Tytul}: {zadanie.Ukonczone}");
}
Taki model jest prosty, ale dobrze pokazuje najważniejszą zaletę generyków. Lista wie, że przechowuje obiekty typu Zadanie, więc edytor może podpowiadać właściwości, a kompilator wykryje pomyłki wcześniej.
Do filtrowania obiektów często wykorzystuję LINQ. Poniższy kod tworzy nową kolekcję tylko z aktywnymi zadaniami, nie zmieniając pierwotnej listy.
List otwarte =
zadania.Where(z => !z.Ukonczone).ToList();
W tym przykładzie ToList() materializuje wynik, czyli faktycznie tworzy nową listę. To ważne, gdy wynik ma być używany później albo przekazany do innej części aplikacji.
Jak podejmować dobrą decyzję przy wyborze kolekcji
Jeżeli potrzebujesz kolejności, duplikatów i dostępu po indeksie, zacznij od List
Najczęstszy błąd nie polega na tym, że ktoś wybiera złą kolekcję na początku. Problem pojawia się wtedy, gdy później ignoruje sposób użycia danych. Przejrzenie kilku operacji w kodzie i sprawdzenie, czy dominują odczyty po indeksie, wyszukiwanie, czy usuwanie ze środka, zwykle szybko pokazuje właściwy kierunek.
Dobrze dobrana lista nie wymaga skomplikowanych sztuczek. W większości aplikacji wystarczy pilnować indeksów, nie modyfikować kolekcji podczas bezpośredniej iteracji i świadomie rozróżniać Count od Capacity. To właśnie te trzy nawyki robią największą różnicę w codziennej pracy z kolekcjami C#.