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 sprawdzajCountalbo, lepiej, użyjTryDequeue/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
foreachrzuci 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.
🚀 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.
- 🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 kroków od podstaw C# do pierwszej pracy: dev-hobby.pl
- 🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
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ę →
2 comments