Menu

Kolejka Queue w C#: Enqueue, Dequeue, Peek i TryDequeue

Queue<T> to kolekcja typu first in, first out: elementy wychodzą w kolejności, w jakiej przyszły. Poznaj Enqueue, Dequeue i Peek, wyjątek pustej kolejki i TryDequeue, przeszukiwanie wszerz z kolejką oraz to, kiedy sięgnąć po ConcurrentQueue albo PriorityQueue.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Queue<T> działa jak kolejka do okienka: pierwszy dodany element jest pierwszym wyjętym (FIFO, first in, first out). Dodajesz na koniec za pomocą Enqueue i zabierasz z przodu za pomocą Dequeue, obie operacje w czasie stałym.

Enqueue, Dequeue i Peek

Wynik:

Waiting: 3
Next up: invoice.pdf
Printing invoice.pdf
Next up: photo.png
Printing photo.png
Printing report.docx
Waiting: 0

Peek służy do zdecydowania, co zrobić z następnym elementem, zanim się na niego zdecydujesz; Dequeue tę decyzję wykonuje. Pętla while (queue.Count > 0), która wyjmuje elementy aż do opróżnienia, to standardowy sposób przetwarzania kolejki i działa dalej, nawet jeśli ciało pętli dodaje nowe elementy, na co foreach nie pozwala.

Wyjątek pustej kolejki i TryDequeue

Dequeue i Peek na pustej kolejce rzucają InvalidOperationException. Nie ma zachowania "null, gdy pusto", nawet dla typów referencyjnych.

Wynik:

Caught InvalidOperationException
Serving ticket 41
Serving ticket 42
False

TryDequeue i TryPeek zwracają true i ustawiają zmienną out, gdy jest jakiś element, a gdy kolejka jest pusta, zwracają false. Pojawiły się w .NET Core 2.0; w starszym kodzie .NET Framework sprawdzaj Count > 0 przed wywołaniem Dequeue.

Zaglądanie do kolejki

Kolejkę można przeglądać bez usuwania czegokolwiek. foreach, ToArray i Contains widzą elementy od przodu do końca.

Wynik:

Ana Ben Chloe 
True
Ana is first, 3 in line
3

Nie ma indeksu: line[1] się nie kompiluje i nie da się usunąć elementu ze środka. Jeśli potrzebujesz któregoś z nich, dane tak naprawdę nie są kolejką; użyj List<T> albo LinkedList<T>. Jak w innych kolekcjach, dodawanie lub wyjmowanie elementów wewnątrz foreach po tej samej kolejce rzuca InvalidOperationException.

Przeszukiwanie wszerz z kolejką

Kolejność kolejki to dokładnie to, czego potrzebuje przeszukiwanie wszerz (BFS): odwiedź wszystko, co jest o jeden krok dalej, potem wszystko o dwa kroki dalej i tak dalej. Tutaj znajduje ono, ile połączeń dzieli Anę od wszystkich innych w małej sieci:

Wynik:

Ana: 0 step(s) from Ana
Ben: 1 step(s) from Ana
Chloe: 1 step(s) from Ana
Dev: 2 step(s) from Ana
Eli: 2 step(s) from Ana
Fay: 3 step(s) from Ana

Każda osoba trafia do kolejki raz, gdy zostanie osiągnięta po raz pierwszy, a ponieważ kolejka oddaje osoby w kolejności ich znalezienia, ten pierwszy raz zawsze prowadzi najkrótszą ścieżką. Zamień kolejkę na Stack, a ta sama pętla stanie się przeszukiwaniem w głąb (DFS), które nie znajduje już najkrótszych ścieżek.

Ten sam schemat obsługuje przechodzenie drzewa poziomami, wypełnianie obszaru na siatce (flood fill) i przechodzenie po linkach: zacznij od jednego elementu w kolejce i dopóki nie jest pusta, wyjmij jeden i dodaj jego nieodwiedzonych sąsiadów.

Queue a List do pracy FIFO

Możesz używać List<T> jako kolejki z Add i RemoveAt(0), ale RemoveAt(0) przesuwa każdy pozostały element o jedno miejsce w dół. Opróżnienie w ten sposób listy 100 000 elementów to około pięciu miliardów przesunięć elementów; Queue<T> wykonuje 100 000 kroków w czasie stałym. Wewnętrznie kolejka to bufor cykliczny: śledzi indeks początku i końca w tablicy i realokuje ją tylko wtedy, gdy jest pełna.

OperacjaQueue<T>List<T> użyta jako kolejka
Dodanie na koniecEnqueue, O(1)Add, O(1)
Usunięcie z przoduDequeue, O(1)RemoveAt(0), O(n)
Podgląd przoduPeeklist[0]
Dostęp po indeksieNiedostępnylist[i]

ConcurrentQueue dla wielu wątków

Queue<T> nie jest bezpieczna wątkowo. Gdy kilka wątków dodaje lub zabiera elementy, użyj ConcurrentQueue<T> z System.Collections.Concurrent. Ma Enqueue, TryDequeue i TryPeek, ale nie ma Dequeue, bo przy innych wątkach wokół schemat "sprawdź Count, potem Dequeue" mógłby zawieść między tymi dwoma wywołaniami.

Wynik:

4000
4000 processed

Ze zwykłą Queue<T> w miejscu ConcurrentQueue<T> licznik wyszedłby błędny albo program rzuciłby wyjątek, zależnie od kolejności wykonania. Dla wątków producenta i konsumenta, które powinny czekać na pracę zamiast kręcić się w pętli, BlockingCollection<T> (domyślnie opakowujące ConcurrentQueue<T>) albo System.Threading.Channels dodają blokowanie i sygnał zakończenia. O ręcznej ochronie zwykłej kolekcji przeczytasz na stronie o lock.

PriorityQueue

Gdy elementy powinny wychodzić według priorytetu, a nie kolejności przybycia (najpilniejsze zgłoszenie najpierw, dotychczas najkrótsza ścieżka w algorytmie Dijkstry), .NET 6 i nowsze udostępniają PriorityQueue<TElement, TPriority>. Najpierw wychodzi element o najniższej wartości priorytetu:

var triage = new PriorityQueue<string, int>();
triage.Enqueue("sprained ankle", 3);
triage.Enqueue("chest pain", 1);
triage.Enqueue("headache", 5);

while (triage.TryDequeue(out string patient, out int priority))
{
    Console.WriteLine($"{priority}: {patient}");
}
// 1: chest pain
// 3: sprained ankle
// 5: headache

Elementy o równym priorytecie mogą wychodzić w dowolnej kolejności; dodaj do priorytetu numer kolejny, jeśli remisy ma rozstrzygać kolejność przybycia.

Typowe błędy

  • Wyjmowanie bez sprawdzenia. Pusta kolejka rzuca InvalidOperationException; zapętlaj na Count > 0 albo użyj TryDequeue.
  • Wywołanie Peek w przekonaniu, że element zniknie. Usuwa tylko Dequeue.
  • Dodawanie elementów wewnątrz foreach po kolejce. Rzuca wyjątek; przetwarzaj pętlą while.
  • Współdzielenie Queue<T> między wątkami. Użyj ConcurrentQueue<T> albo blokady.
  • Używanie List.RemoveAt(0) jako kolejki na dużych danych. Każde wywołanie to O(n).

Najczęściej zadawane pytania

Czym jest Queue w C#?

Queue<T> z System.Collections.Generic to kolekcja typu first in, first out (FIFO). Enqueue dodaje element na koniec, Dequeue usuwa i zwraca element z przodu, a Peek zwraca element z przodu bez usuwania go. Wszystkie trzy działają w czasie stałym.

Co się dzieje po wywołaniu Dequeue na pustej kolejce w C#?

Dequeue i Peek na pustej kolejce rzucają InvalidOperationException. Najpierw sprawdź queue.Count > 0 albo użyj TryDequeue(out var item) i TryPeek(out var item), które zamiast rzucać wyjątek zwracają false (dostępne od .NET Core 2.0).

Jaka jest różnica między Peek a Dequeue?

Peek zwraca element z przodu i zostawia go w kolejce, więc dwukrotne wywołanie zwraca ten sam element. Dequeue zwraca element z przodu i go usuwa, więc następne wywołanie zwraca element stojący za nim.

Czy Queue<T> jest bezpieczna wątkowo w C#?

Nie. Dwa wątki wywołujące jednocześnie Enqueue lub Dequeue na tej samej Queue<T> mogą ją uszkodzić. Użyj ConcurrentQueue<T> z System.Collections.Concurrent, której Enqueue i TryDequeue można bezpiecznie wywoływać z wielu wątków, albo otocz każdy dostęp do zwykłej kolejki instrukcją lock.

Dlaczego używać Queue zamiast List?

Usunięcie pierwszego elementu z List<T> przez RemoveAt(0) przesuwa wszystkie pozostałe elementy, więc działa coraz wolniej w miarę wzrostu listy. Queue<T> usuwa z przodu w czasie stałym, a jej API wyraża intencję: elementy są przetwarzane w kolejności przybycia.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ