Przywracasz poprzedni stan edytora albo obsługujesz nawiasy w wyrażeniu i potrzebujesz pobrać ostatnio dodany element. W C# zwykle sięgasz wtedy po stos typu LIFO, choć wybór metody i sposób obsługi pustej kolekcji zależą od tego, co program ma zrobić dalej.
Stos oddaje najpierw element dodany jako ostatni
- Push dodaje element na wierzch, a Pop usuwa i zwraca ten element.
- Peek pozwala podejrzeć wierzch bez zmiany kolekcji.
- TryPop zwraca informację, czy udało się pobrać element, zamiast zgłaszać wyjątek dla pustego stosu.
- Stack
nie synchronizuje dostępu między wątkami; przy współbieżnych operacjach rozważ ConcurrentStack .
Push i Pop odwracają kolejność pobierania
Wyobraź sobie historię zmian w edytorze. Na stos trafiają kolejne stany dokumentu, a cofnięcie powinno przywrócić ten zapisany jako ostatni. Właśnie tak działa zasada LIFO: „ostatni wszedł, pierwszy wyszedł”.
W .NET gotową implementacją jest Stack z przestrzeni nazw System.Collections.Generic. Push dodaje element, Pop usuwa go z wierzchu, a Peek odczytuje wierzch bez usuwania. Prawdę mówiąc, łatwo pomylić ostatnie dwie operacje, a różnica staje się widoczna dopiero przy następnym odczycie.
var history = new Stack();
history.Push("Dokument A");
history.Push("Dokument B");
string current = history.Peek(); // Dokument B, stos bez zmian
string previous = history.Pop(); // Dokument B, teraz usunięty
Operacje na wierzchu są szybkie. Dokumentacja .NET opisuje Pop jako operację O(1); Push zwykle także ma koszt stały, ale gdy zabraknie miejsca w wewnętrznej tablicy, kolekcja musi ją powiększyć i skopiować elementy. Dla typowego użycia nie trzeba temu zapobiegać, lecz przy znanej liczbie elementów można ustawić pojemność konstruktorem.
TryPop wybiera bezpieczną ścieżkę dla pustego stosu
Gdy historia zmian może być pusta, wywołanie Pop bez sprawdzenia kończy się InvalidOperationException. Można wcześniej sprawdzić Count, ale gdy celem jest po prostu pobranie elementu, TryPop łączy próbę z obsługą wyniku.
if (history.TryPop(out string? previous))
{
Console.WriteLine($"Przywracam: {previous}");
}
else
{
Console.WriteLine("Brak wcześniejszego stanu.");
}
Prosta reguła pomaga dobrać metodę: jeśli pusty stos oznacza błąd programu, użyj Pop i pozwól, by błąd był widoczny; jeśli brak elementu jest normalnym wynikiem, wybierz TryPop. Podobnie TryPeek sprawdza wierzch bez usuwania go.
| Sytuacja | Skutek | Co zrobić |
|---|---|---|
| Stos może być pusty, a brak elementu jest normalny | Pop zgłosi wyjątek |
Użyj TryPop i obsłuż wynik false |
| Brak elementu wskazuje na błąd logiki | Warunek może ukryć nieoczekiwany stan | Użyj Pop i potraktuj wyjątek jako sygnał błędu |
| Chcesz sprawdzić następny element i zachować go w kolekcji | Pop zmieni stan stosu |
Użyj Peek albo TryPeek |
Enumerowanie pokazuje elementy od wierzchu i nie opróżnia stosu
Popularne nieporozumienie brzmi: przejście pętlą po stosie pobiera z niego elementy. W Stack enumeracja nie usuwa danych, a kolejność przejścia zaczyna się od wierzchu, czyli od elementu dodanego najpóźniej.
var stack = new Stack();
stack.Push(10);
stack.Push(20);
stack.Push(30);
foreach (int item in stack)
{
Console.WriteLine(item); // 30, 20, 10
}
Console.WriteLine(stack.Count); // 3
Microsoft Learn opisuje enumerator jako nieważny po zmianie kolekcji: jeśli dodasz lub usuniesz element w trakcie pętli, kolejne przejście może zgłosić wyjątek. Gdy naprawdę chcesz opróżnić stos, pobieraj elementy przez TryPop lub Pop, zamiast polegać na samym foreach.
Queue i ConcurrentStack rozwiązują inne problemy
Stos pasuje do cofania ostatniej zmiany, śledzenia nawiasów czy przechodzenia grafu w głąb. Jeśli zadania mają być obsługiwane w kolejności nadejścia, potrzebujesz kolejki FIFO, czyli Queue: dodajesz przez Enqueue, pobierasz przez Dequeue. Wybór nie zależy od szybkości, tylko od kolejności, której wymaga logika.
Jest jeszcze przypadek wielowątkowy. Zwykły Stack nie synchronizuje równoczesnych odczytów i zapisów, więc współdzielona kolekcja wymaga blokady albo typu ConcurrentStack. Microsoft Learn wskazuje, że jego publiczne i chronione składowe są bezpieczne przy współbieżnym użyciu; to właściwy punkt wyjścia, gdy wiele wątków ma niezależnie dodawać lub pobierać elementy.
Uważam, że najczęstszy błąd przy wyborze stosu nie dotyczy samej implementacji, lecz pomylenia „ostatniego” z „najstarszym” elementem. Jeśli chcesz cofać historię lub wracać z najgłębszego miejsca algorytmu, LIFO jest naturalne; jeśli obsługujesz kolejkę żądań, wybierz FIFO.
Przed dodaniem elementu ustal, kto go później pobierze
W stosie szczegóły kolejności są częścią działania algorytmu, a nie ozdobą kolekcji. Przy cofaniu zmian zapisuj stan przed modyfikacją dokumentu, a przy przetwarzaniu nawiasów wkładaj na stos otwierające znaki i zdejmuj je po napotkaniu pasującego zamknięcia.
Jeśli elementów może być wiele, rozważ konstruktor z początkową pojemnością, by ograniczyć powiększanie tablicy. Gdy natomiast kolekcję współdzieli kilka wątków, zdecyduj o synchronizacji przed wdrożeniem operacji, bo samo zastąpienie pętli przez foreach nie czyni dostępu bezpiecznym.
