🔥 Zapisy zamknięte, ale możesz pobrać Roadmapę .NET i dołączyć do listy oczekujących — Pobierz i dołącz do Listy VIP →

Queue w C# — kolejka FIFO na przykładach

Queue<T> to jedna z tych kolekcji, które każdy programista C# zna z nazwy, ale niewielu potrafi wytłumaczyć, dlaczego wybrać ją zamiast List<T> albo kiedy warto sięgnąć po jej wariant bezpieczny wielowątkowo. Kolejka reprezentuje strukturę danych typu FIFO (First-In-First-Out) — pierwszy element, który trafia do kolejki, jest też pierwszym, który z niej wychodzi. Dokładnie tak jak w prawdziwej kolejce w sklepie: kto stanął pierwszy, ten pierwszy zostanie obsłużony.

Ten artykuł skupia się na gotowej, wbudowanej klasie Queue<T> z .NET — jej pełnym API, złożoności czasowej, pułapkach i realnych zastosowaniach w produkcyjnym kodzie. Jeśli interesuje Cię, jak taka kolejka działa od środka i chcesz zbudować własną implementację FIFO na tablicy krok po kroku (własny interfejs, wskaźniki początku/końca, bufor cykliczny), zobacz osobny, dedykowany temu artykuł: Implementacja kolejki w C#.

Czym jest Queue<T> i kiedy go użyć

Kolejki modelują dowolny proces, w którym kolejność obsługi musi odpowiadać kolejności zgłoszenia. Typowe przykłady z życia i z kodu:

  • ludzie czekający w kolejce do kasy,
  • samochody na jednokierunkowej ulicy,
  • zadania czekające w tle na przetworzenie przez worker service,
  • wiadomości w systemie kolejkowym (np. RabbitMQ, Azure Service Bus) — Queue<T> to ich najprostszy, lokalny odpowiednik w pamięci,
  • przechodzenie po grafie/drzewie wszerz (algorytm BFS).

Jeśli w Twojej logice liczy się kolejność napływania danych, a nie dostęp swobodny do dowolnego elementu po indeksie, Queue<T> jest naturalnym wyborem — czytelniejszym i mniej podatnym na błędy niż ręczne żonglowanie indeksami w List<T>.

Jak Queue<T> działa pod maską — w skrócie

Wewnętrznie Queue<T> z biblioteki standardowej to bufor cykliczny (circular buffer) oparty na tablicy, a nie lista połączona, jak można by się spodziewać. Gdy tablica się zapełni, .NET alokuje nową, większą (domyślnie podwaja pojemność) i kopiuje elementy w poprawnej kolejności. To dlatego, jeśli z góry znasz przybliżoną liczbę elementów, warto skorzystać z konstruktora przyjmującego pojemność początkową — oszczędzasz sobie zbędne realokacje:

// Unikamy wielokrotnych realokacji tablicy wewnętrznej
var queue = new Queue<string>(capacity: 100);

Podstawowe operacje kolejki

Trzy fundamentalne operacje w kolejce to:

  • Enqueue — dodaje element na końcu kolejki,
  • Dequeue — usuwa i zwraca element z przodu kolejki,
  • Peek — zwraca element z przodu, ale bez usuwania go.

Oprócz nich Queue<T> udostępnia też metody pomocnicze:

var queue = new Queue<string>();

// Dodaj element do kolejki
queue.Enqueue("Hello");
queue.Enqueue("World");

// Sprawdź, czy kolejka zawiera dany element — O(n)
var contains = queue.Contains("Hello");

// Usuń i zwróć element z przodu kolejki
var front = queue.Dequeue(); // "Hello"

// Podejrzyj element z przodu, nie usuwając go
var top = queue.Peek(); // "World"

// Bezpieczne warianty — nie rzucają wyjątku na pustej kolejce
if (queue.TryDequeue(out var result))
{
    Console.WriteLine(result);
}

if (queue.TryPeek(out var next))
{
    Console.WriteLine(next);
}

// Usuń wszystkie elementy z kolejki
queue.Clear();

// Uzyskaj liczbę elementów w kolejce
var count = queue.Count;

TryDequeue i TryPeek to warianty, o których wielu programistów zapomina — a to one ratują z jednego z najczęstszych błędów opisanych niżej.

Pełny przykład w praktyce

Poniżej symulacja prostego systemu obsługi zgłoszeń — klasyczne zastosowanie kolejki:

var tickets = new Queue<string>();

tickets.Enqueue("Zgłoszenie #1 — reset hasła");
tickets.Enqueue("Zgłoszenie #2 — błąd 500 na produkcji");
tickets.Enqueue("Zgłoszenie #3 — pytanie o fakturę");

Console.WriteLine($"W kolejce: {tickets.Count} zgłoszeń");

while (tickets.TryDequeue(out var ticket))
{
    Console.WriteLine($"Obsługuję: {ticket}");
}

Console.WriteLine("Wszystkie zgłoszenia obsłużone.");

Zwróć uwagę na while (tickets.TryDequeue(...)) — to idiomatyczny sposób opróżniania kolejki w C#, bez ręcznego sprawdzania Count > 0 przed każdym wywołaniem Dequeue().

Złożoność czasowa Queue<T>

  • Enqueue — O(1) zamortyzowane (sporadyczna realokacja to O(n), ale rzadka),
  • Dequeue — O(1),
  • Peek — O(1),
  • Contains — O(n), bo trzeba przeszukać całą kolekcję liniowo,
  • ToArray() — O(n), zwraca elementy w kolejności od przodu do tyłu.

To właśnie stałoczasowe Enqueue/Dequeue odróżnia Queue<T> od naiwnej implementacji na List<T>, gdzie RemoveAt(0) wymaga przesunięcia wszystkich pozostałych elementów — czyli O(n) przy każdym zdjęciu z kolejki.

Queue vs Stack vs List — kiedy co wybrać

To pytanie pada często na rozmowach rekrutacyjnych, więc warto mieć gotową, precyzyjną odpowiedź:

  • Queue<T> — kolejność FIFO. Wybierz, gdy kolejność przetwarzania musi odpowiadać kolejności napływania (zadania w tle, BFS, bufor komunikatów).
  • Stack<T> — kolejność LIFO (Last-In-First-Out), czyli “ostatni wchodzi, pierwszy wychodzi”. Wybierz do cofania operacji (undo), parsowania wyrażeń nawiasowych, przechodzenia po grafie w głąb (DFS).
  • List<T> — gdy potrzebujesz swobodnego dostępu po indeksie, sortowania czy wyszukiwania binarnego. Nie nadaje się do symulowania FIFO/LIFO — będzie działać, ale wolniej i mniej czytelnie niż dedykowana struktura.

Innymi słowy: jeśli w kodzie widzisz list.RemoveAt(0) w pętli, to niemal zawsze sygnał, że powinna tam być Queue<T>.

Zastosowania Queue<T> w realnym kodzie .NET

Przeszukiwanie grafu wszerz (BFS) — klasyczne zastosowanie, w którym kolejka gwarantuje odwiedzanie węzłów warstwa po warstwie:

static void Bfs(Dictionary<int, List<int>> graph, int start)
{
    var visited = new HashSet<int>();
    var queue = new Queue<int>();

    queue.Enqueue(start);
    visited.Add(start);

    while (queue.TryDequeue(out var node))
    {
        Console.WriteLine($"Odwiedzam: {node}");

        foreach (var neighbor in graph[node])
        {
            if (visited.Add(neighbor))
            {
                queue.Enqueue(neighbor);
            }
        }
    }
}

Kolejka zadań w tle (BackgroundService, ASP.NET Core) — jeden wątek dodaje zadania, drugi je przetwarza. Tu zamiast zwykłej Queue<T> używa się jej wariantu bezpiecznego wątkowo:

private readonly ConcurrentQueue<Func<Task>> _tasks = new();

public void QueueBackgroundTask(Func<Task> workItem)
    => _tasks.Enqueue(workItem);

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        if (_tasks.TryDequeue(out var workItem))
        {
            await workItem();
        }
    }
}

To ten sam interfejs (Enqueue/TryDequeue), ale ConcurrentQueue<T> gwarantuje bezpieczeństwo, gdy wiele wątków sięga po kolejkę jednocześnie — patrz sekcja o pułapkach niżej.

Częste pułapki

  • Dequeue()/Peek() na pustej kolejce rzuca InvalidOperationException — zawsze sprawdzaj Count albo, lepiej, użyj TryDequeue/TryPeek.
  • Queue<T> nie jest bezpieczna wątkowo — jeśli wiele wątków ma dodawać/usuwać elementy jednocześnie, użyj System.Collections.Concurrent.ConcurrentQueue<T>, a nie ręcznego blokowania zwykłej kolejki.
  • Modyfikacja podczas iteracji — dodanie lub usunięcie elementu w trakcie pętli foreach rzuci wyjątkiem informującym, że kolekcja została zmieniona.
  • Contains() to O(n), nie O(1) — jeśli często sprawdzasz przynależność elementu, kolejka nie jest do tego stworzona; rozważ dodatkowo HashSet<T> obok kolejki.
  • Queue<T> to typ referencyjny — przekazanie jej do metody przekazuje referencję do tej samej kolekcji, nie kopię.

Queue<T> w nowszych wersjach C#

W .NET 9 (C# 13) Queue<T> otrzymała wsparcie dla collection expressions wprowadzonych w C# 12 — dzięki temu można ją zainicjalizować równie zwięźle jak tablicę czy listę:

// Skrócona inicjalizacja kolejki (C# 12/13, .NET 9+)
Queue<int> queue = [1, 2, 3, 4, 5];

To czysto składniowe ułatwienie — pod spodem nadal działa ten sam bufor cykliczny, ale kod inicjalizujący kolejkę stałymi wartościami (np. w testach) staje się krótszy i czytelniejszy.

Chcesz zobaczyć, jak zbudować własną kolejkę od podstaw, zamiast korzystać z gotowej klasy? Zobacz Implementacja własnej kolejki (Queue) w C#.

FAQ — najczęstsze pytania o Queue<T>

Czym różni się Queue<T> od List<T>?
List<T> daje swobodny dostęp po indeksie i wsparcie dla sortowania/wyszukiwania. Queue<T> wymusza dostęp wyłącznie od przodu (Dequeue/Peek) i od tyłu (Enqueue), za to robi to w stałym czasie O(1), podczas gdy usuwanie z początku List<T> to O(n).

Czy Queue<T> jest bezpieczna wątkowo?
Nie. Do scenariuszy wielowątkowych (producer-consumer) użyj ConcurrentQueue<T> z przestrzeni System.Collections.Concurrent.

Jak zaimplementować kolejkę priorytetową?
.NET dostarcza gotową PriorityQueue<TElement, TPriority> — nie trzeba budować jej ręcznie na zwykłej Queue<T>.

Jaka jest złożoność Dequeue()?
O(1) — stały czas, niezależnie od liczby elementów w kolejce.

Kiedy użyć ConcurrentQueue zamiast Queue?
Zawsze, gdy więcej niż jeden wątek jednocześnie dodaje lub usuwa elementy — np. w klasycznym wzorcu producer-consumer albo kolejce zadań przetwarzanych przez kilka workerów równolegle.

BFS to tylko jedna z dwóch podstawowych strategii przechodzenia po grafie. Zobacz przeciwną — przeszukiwanie w głąb — w artykule o Stack w C#, na tym samym grafie, tylko ze stosem zamiast kolejki.

👨‍💻
Mariusz Jurczenko
Senior .NET Developer · 10+ lat doświadczenia komercyjnego

Programista .NET z doświadczeniem komercyjnym w firmach takich jak NFZ, Kamsoft, Diagnostyka, Hermes Reply Polska czy Etisoft Smart Solutions. Twórca kursów, z których skorzystało już ponad 11 000 osób w Strefie Kursów i ponad 1 000 kursantów na dev-hobby.pl.

Specjalizacja: Clean Code, Clean Architecture i uczenie programowania tak, żeby dało się je naprawdę zrozumieć — nie wykuć.

🚀 Co dalej?

Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.

2 comments

Dodaj komentarz

czytanie to początek

Zamień wiedzę w umiejętności

Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.

Pobieram roadmapę →