List<T> — lista, która rośnie razem z programem

Do koła programistycznego dopisują się kolejne osoby, a ktoś inny rezygnuje. Tablica tego nie udźwignie, bo jej długość ustala się raz. Lista zmienia rozmiar sama.

C# List<T> Add Count 45–90 min
CEL LEKCJI

Czego się dziś nauczysz

  • Utworzysz List<T> i wyjaśnisz, co oznacza typ w nawiasach ostrych.
  • Dodasz, wstawisz i usuniesz element.
  • Odróżnisz Count od indeksu ostatniego elementu.
  • Usuniesz elementy pętlą, nie psując przy tym numeracji.
  • Wskażesz sytuacje, w których lepsza jest tablica, a w których lista.

Przygotowanie: lekcje 01–30. Przewidywany czas: 45–90 minut z zadaniami. Przykłady wymagają .NET 8 lub nowszego, z włączonymi ImplicitUsings i Nullable.

TEORIA

Tablica ma stałą długość

Do koła programistycznego zapisuje się pierwsza osoba. Potem druga. Potem ktoś rezygnuje, a na jego miejsce przychodzą dwie kolejne.

tablica: rozmiar ustalony przy tworzeniu
string[] uczestnicy = new string[3];
uczestnicy[0] = "Ala";
uczestnicy[1] = "Bartek";
uczestnicy[2] = "Cezary";

// uczestnicy[3] = "Dorota";   // IndexOutOfRangeException

Tablica z lekcji 23 ma długość ustaloną w chwili utworzenia. Dopisanie czwartej osoby wymaga utworzenia nowej tablicy i przepisania zawartości — Array.Resize robi dokładnie to, tylko w ukryciu. A usunięcie kogoś ze środka zostawia dziurę, którą trzeba zasypać ręcznie.

List<T> zajmuje się tym za ciebie:

Program.cs
List<string> uczestnicy = new List<string>();

uczestnicy.Add("Ala");
uczestnicy.Add("Bartek");
uczestnicy.Add("Cezary");
uczestnicy.Add("Dorota");      // lista sama sie powieksza

uczestnicy.Remove("Bartek");   // i sama sie zsuwa

Console.WriteLine(uczestnicy.Count);      // 3
Console.WriteLine(uczestnicy[1]);         // Cezary
string[]List<string>
Rozmiarustalony przy tworzeniuzmienia się w trakcie
Dodanie elementutrzeba tworzyć nową tablicęAdd
Usunięcie elementuręczne przesuwanieRemove, RemoveAt
Liczba elementówLengthCount
Dostęp po indeksiet[0]lista[0] — tak samo
Kiedy wybraćliczba elementów znana i stałaliczba elementów zmienia się w trakcie
TEORIA

Co oznacza T w nawiasach ostrych

Zapis List<string> czytamy: „lista napisów”. Litera w nawiasach ostrych mówi, jakiego typu elementy lista przechowuje — i kompilator tego pilnuje.

Program.cs
List<string> imiona = new List<string>();
List<int> punkty = new List<int>();
List<double> pomiary = new List<double>();

imiona.Add("Ala");
// imiona.Add(42);      // BLAD CS1503: nie mozna zamienic int na string

Dzięki temu do listy imion nikt nie doda liczby przez pomyłkę — błąd wychodzi przy kompilacji, a nie u użytkownika. Mechanizm, który za tym stoi, poznasz dokładniej w lekcji 52 przy generykach; na razie wystarczy zasada: w nawiasach ostrych stoi typ elementu.

Trzy sposoby na utworzenie listy

Program.cs
// 1. pusta lista
List<string> a = new List<string>();

// 2. lista z wartosciami poczatkowymi
List<string> b = new List<string> { "Ala", "Bartek", "Cezary" };

// 3. lista z zawartosci tablicy
string[] tablica = { "Ala", "Bartek" };
List<string> c = new List<string>(tablica);

Zapis skrócony

Od C# 9 wolno napisać List<string> lista = new(); — typ po prawej stronie jest oczywisty, więc kompilator go uzupełnia. W kursie piszemy pełną formę, żeby było widać, co powstaje, ale w cudzym kodzie spotkasz obie.

TEORIA

Dodawanie, usuwanie, sprawdzanie

Program.cs
List<string> lista = new List<string> { "Ala", "Bartek", "Cezary" };

lista.Add("Dorota");            // na koniec
lista.Insert(1, "Ewa");         // na pozycje 1, reszta przesuwa sie w prawo

Console.WriteLine(lista.Count);              // 5
Console.WriteLine(lista.Contains("Ala"));    // True
Console.WriteLine(lista.IndexOf("Cezary"));  // 3   (po wstawieniu Ewy!)

lista.Remove("Ewa");            // usuwa PIERWSZE wystapienie wartosci
lista.RemoveAt(0);              // usuwa element o indeksie 0

Console.WriteLine(string.Join(", ", lista)); // Bartek, Cezary, Dorota

lista.Clear();
Console.WriteLine(lista.Count);              // 0

Indeksy przesuwają się same

Po Insert(1, …) wszystko, co stało od pozycji 1 w prawo, dostaje indeks o jeden większy. Po RemoveAt — o jeden mniejszy. Dlatego numer indeksu zapisany w zmiennej przed zmianą listy przestaje być aktualny. To najczęstsza przyczyna „dziwnych” wyników przy usuwaniu.

Count a indeks ostatniego elementu

Ta sama zasada, co przy tablicach: indeksy zaczynają się od zera, więc ostatni element ma numer Count - 1.

Program.cs
List<string> lista = new List<string> { "Ala", "Bartek", "Cezary" };

Console.WriteLine(lista.Count);              // 3
Console.WriteLine(lista[lista.Count - 1]);   // Cezary
Console.WriteLine(lista[^1]);                // Cezary - zapis skrocony
// Console.WriteLine(lista[lista.Count]);    // ArgumentOutOfRangeException

Pusta lista nie ma indeksu 0

Odczyt lista[0] z pustej listy kończy się wyjątkiem ArgumentOutOfRangeException. Przed sięgnięciem po element zawsze sprawdź Count — albo użyj foreach, który po pustej liście po prostu nie wykona ani jednego obiegu.

TEORIA

Przechodzenie i modyfikowanie

Do samego wypisania wygodniejszy jest foreach. Gdy potrzebny jest numer pozycji albo zmiana zawartości — for z indeksem.

Program.cs
List<string> lista = new List<string> { "Ala", "Bartek", "Cezary" };

foreach (string imie in lista)
{
    Console.WriteLine(imie);
}

for (int i = 0; i < lista.Count; i++)
{
    Console.WriteLine($"{i + 1}. {lista[i]}");
}

Nie zmieniaj listy w trakcie foreach

Dodanie albo usunięcie elementu wewnątrz foreach po tej samej liście kończy się wyjątkiem InvalidOperationException w następnym obiegu: „kolekcja została zmodyfikowana”. Pętla trzyma w środku licznik pozycji, a zmiana listy czyni go nieaktualnym.

Usuwanie robimy pętlą for od końca. Dzięki temu przesunięcie indeksów dotyczy tylko elementów, które już sprawdziliśmy:

Program.cs
List<int> oceny = new List<int> { 5, 1, 4, 1, 3 };

for (int i = oceny.Count - 1; i >= 0; i--)
{
    if (oceny[i] == 1)
    {
        oceny.RemoveAt(i);
    }
}

Console.WriteLine(string.Join(", ", oceny));   // 5, 4, 3

Krótsza droga do tego samego

oceny.RemoveAll(o => o == 1); robi to jedną instrukcją. Zapis ze strzałką to wyrażenie lambda — poznasz je w lekcji 53. Warto jednak raz napisać wersję z pętlą, bo dopiero ona pokazuje, dlaczego idziemy od końca.

Sortowanie i odwracanie

Program.cs
List<string> imiona = new List<string> { "Cezary", "Ala", "Bartek" };

imiona.Sort();                                 // w miejscu, alfabetycznie
Console.WriteLine(string.Join(", ", imiona));  // Ala, Bartek, Cezary

imiona.Reverse();
Console.WriteLine(string.Join(", ", imiona));  // Cezary, Bartek, Ala

Sortowanie zmienia listę, nie tworzy nowej

Sort i Reverse działają w miejscu — nie zwracają nowej listy, tylko przestawiają elementy w istniejącej. Jeśli oryginalna kolejność jest jeszcze potrzebna, zrób najpierw kopię: List<string> kopia = new List<string>(imiona);.

PRZYKŁAD

Przykład: zapisy do koła programistycznego

Program.cs
using System.Collections.Generic;

class Program
{
    static void Pokaz(string tytul, List<string> lista)
    {
        Console.WriteLine();
        Console.WriteLine($"{tytul} ({lista.Count}):");

        if (lista.Count == 0)
        {
            Console.WriteLine("  (nikt)");
            return;
        }

        for (int i = 0; i < lista.Count; i++)
        {
            Console.WriteLine($"  {i + 1}. {lista[i]}");
        }
    }

    static void Main()
    {
        List<string> uczestnicy = new List<string>();

        uczestnicy.Add("Ala");
        uczestnicy.Add("Bartek");
        uczestnicy.Add("Cezary");
        Pokaz("Po zapisach", uczestnicy);

        uczestnicy.Insert(0, "Dorota");
        Pokaz("Dorota z pierwszenstwem", uczestnicy);

        bool usunieto = uczestnicy.Remove("Bartek");
        Console.WriteLine($"\nUsunieto Bartka: {usunieto}");
        Console.WriteLine($"Usunieto Zenona: {uczestnicy.Remove("Zenon")}");

        uczestnicy.Sort();
        Pokaz("Po posortowaniu", uczestnicy);

        Console.WriteLine();
        Console.WriteLine($"Czy jest Ala: {uczestnicy.Contains("Ala")}");
        Console.WriteLine($"Ala jest na pozycji: {uczestnicy.IndexOf("Ala") + 1}");
        Console.WriteLine($"Ostatnia osoba: {uczestnicy[uczestnicy.Count - 1]}");
    }
}
wynik w konsoli
Po zapisach (3):
  1. Ala
  2. Bartek
  3. Cezary

Dorota z pierwszenstwem (4):
  1. Dorota
  2. Ala
  3. Bartek
  4. Cezary

Usunieto Bartka: True
Usunieto Zenona: False

Po posortowaniu (3):
  1. Ala
  2. Cezary
  3. Dorota

Czy jest Ala: True
Ala jest na pozycji: 1
Ostatnia osoba: Dorota

Co dzieje się po kolei

  • Insert(0, …) wstawia Dorotę na początek, a pozostałe trzy osoby dostają indeksy o jeden większe.
  • Remove zwraca booltrue, gdy element znaleziono i usunięto. Próba usunięcia kogoś, kogo nie ma, nie jest błędem; po prostu zwraca false.
  • Metoda Pokaz przyjmuje listę jako parametr — działa dla każdej listy napisów. To metoda z lekcji 19, tylko z innym typem parametru.
  • Wcześniejsze return w Pokaz obsługuje pustą listę i chroni pętlę przed sięganiem po nieistniejący element.
  • Numery na ekranie to i + 1, bo człowiek liczy od jednego, a lista od zera.
ELEMENTY WBUDOWANE

Zestawienie metod List<T>

MetodaZwracaDziałanie i ograniczenia
Add(x)Dopisuje na końcu. Lista powiększa się sama.
Insert(i, x)Wstawia na pozycji i; reszta przesuwa się w prawo. Indeks większy od Count daje wyjątek.
Remove(x)boolUsuwa pierwsze wystąpienie wartości. false, gdy nie znaleziono.
RemoveAt(i)Usuwa element o podanym indeksie. Poza zakresem — ArgumentOutOfRangeException.
RemoveAll(warunek)intUsuwa wszystkie pasujące i zwraca ich liczbę. Warunek zapisujemy lambdą (lekcja 53).
Clear()Usuwa wszystko; Count spada do zera.
CountintLiczba elementów. Właściwość — bez nawiasów. W tablicy odpowiednikiem jest Length.
lista[i]TOdczyt i zapis elementu, tak jak w tablicy. Indeksy od 0 do Count - 1.
Contains(x)boolCzy wartość jest na liście. Dla napisów rozróżnia wielkość liter.
IndexOf(x)intPozycja pierwszego wystąpienia albo -1. Tej wartości nie podawaj do indeksatora.
Sort() / Reverse()Porządkują listę w miejscu. Sortowanie własnych typów wymaga IComparable (lekcja 51).
ToArray()T[]Kopia zawartości jako tablica — przydatne, gdy metoda oczekuje tablicy.
string.Join(sep, lista)stringSkleja elementy separatorem; działa dla list tak samo jak dla tablic.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
lista.LengthLista ma Count, tablica ma Length. To najczęstsza pomyłka po lekcji o tablicach.
lista[lista.Count]Zawsze poza zakresem. Ostatni element to lista[lista.Count - 1].
Dodawanie lub usuwanie w foreachInvalidOperationException: kolekcja została zmodyfikowana. Usuwaj pętlą for od końca.
Usuwanie pętlą for od początkuPo RemoveAt(i) następny element wskakuje na miejsce i i zostaje pominięty. Idź od końca.
lista.Sort() zapisane jako lista = lista.Sort();CS0029 — Sort nic nie zwraca, bo sortuje w miejscu.
new List<string>(5) i odczyt lista[0]Liczba w nawiasie to zapas miejsca, a nie liczba elementów. Lista nadal jest pusta i Count wynosi 0.
lista.IndexOf(x) bez sprawdzenia wynikuGdy elementu nie ma, wynikiem jest -1, a lista[-1] kończy się wyjątkiem.
ZADANIA

Zadania

ZAD 1Lista obecności★☆☆

Utwórz List<string>, dodaj pięć imion, wypisz je z numeracją od jednego i podaj liczbę osób. Nie wpisuj liczby na sztywno.

ZAD 2Dodaj i usuń★☆☆

Do listy z poprzedniego zadania dopisz osobę na drugą pozycję, usuń jedną po nazwie i jedną po indeksie. Po każdej zmianie wypisz całą listę przez string.Join.

ZAD 3Czy jest na liście★☆☆

Wczytaj imię od użytkownika i sprawdź, czy jest na liście. Jeśli tak — podaj jego pozycję liczoną od jednego; jeśli nie — wypisz komunikat. Skorzystaj z IndexOf i pamiętaj o wartości -1.

ZAD 4Oceny bez jedynek★★☆

Utwórz List<int> z ocenami, w tym kilkoma jedynkami. Usuń wszystkie jedynki pętlą for od końca, a potem policz i wypisz średnią z pozostałych. Zadbaj o to, żeby pusta lista nie spowodowała dzielenia przez zero.

ZAD 5Kolejka do konsoli★★☆

Napisz program z menu: dodaj osobę na koniec, obsłuż pierwszą osobę (usuń ją z listy i wypisz jej imię), pokaż całą kolejkę, zakończ. Obsłuż próbę obsłużenia kogoś z pustej kolejki. Każdą operację zamknij w osobnej metodzie przyjmującej listę jako parametr.

ZAD 6Ranking bez powtórzeń★★★

Wczytuj imiona, dopóki użytkownik nie wpisze pustego wiersza. Imiona już obecne na liście odrzucaj z komunikatem, zamiast dodawać po raz drugi. Na koniec wypisz listę posortowaną alfabetycznie, a pod nią tę samą listę w kolejności zapisów. Podpowiedź: żeby mieć obie kolejności, potrzebne są dwie listy albo kopia przed sortowaniem.

PODSUMOWANIE

Co trzeba zapamiętać

  • List<T> zmienia rozmiar w trakcie działania programu, tablica nie.
  • W nawiasach ostrych stoi typ elementu — kompilator pilnuje, żeby nic innego nie trafiło na listę.
  • Liczbę elementów podaje Count, a ostatni element ma indeks Count - 1.
  • Remove usuwa pierwsze wystąpienie wartości i zwraca bool; RemoveAt działa po indeksie.
  • Listy nie wolno zmieniać w trakcie foreach; usuwaj pętlą for od końca.
  • Sort i Reverse porządkują listę w miejscu — nie zwracają nowej.

Dokumentacja: Microsoft Learn — temat tej lekcji.