DYNAMIC EXPLAINER / ARTICLE FLOW

把章节变成可单步观察的过程

01 / 11
STEP 01 / CODE / LOGIC

先说结论

Queue 和 Stack 保存的可以是同一批数据,区别只在“下一次取谁”:Queue 先进先出,Stack 后进先出。选择它们是在选择处理顺序,而不仅是在选择 API。

依次放入:A、B、C
Queue.Dequeue() -> A,剩余 [B, C]
Stack.Pop()     -> C,剩余 [A, B]

分类:CSharp 基础数据结构

先说结论

Queue 和 Stack 保存的可以是同一批数据,区别只在“下一次取谁”:Queue 先进先出,Stack 后进先出。选择它们是在选择处理顺序,而不仅是在选择 API。

同一组数据的两种结果

依次放入:A、B、C
Queue.Dequeue() -> A,剩余 [B, C]
Stack.Pop()     -> C,剩余 [A, B]

任务排队通常希望 A 先执行,所以用 Queue;撤销操作通常希望最后执行的 C 先撤销,所以用 Stack。若业务需要从中间任意删除,两者都不是天然合适的结构。

Queue 是什么

Queue<T> 是队列:

先进先出。

也叫 FIFO:

First In, First Out

就像排队买东西:

先来的人先处理。

声明:

Queue<string> queue = new();

入队:

queue.Enqueue("Alice");
queue.Enqueue("Bob");
queue.Enqueue("Cindy");

出队:

string first = queue.Dequeue();

拿到的是:

Alice

Queue 常用操作

入队:

queue.Enqueue("Alice");

出队并移除:

string name = queue.Dequeue();

查看队首但不移除:

string next = queue.Peek();

数量:

int count = queue.Count;

清空:

queue.Clear();

空队列时直接 Dequeue()Peek() 会报错。

所以通常先判断:

if (queue.Count > 0)
{
    string name = queue.Dequeue();
}

Queue 适合什么场景

Queue 适合按顺序处理任务:

伤害数字排队播放
系统消息按顺序弹出
下载任务队列
AI 命令队列
网络包按顺序处理
BFS 广度优先搜索

例子:

Queue<string> messages = new();

messages.Enqueue("获得金币 +10");
messages.Enqueue("获得经验 +20");
messages.Enqueue("任务完成");

while (messages.Count > 0)
{
    string message = messages.Dequeue();
    Debug.Log(message);
}

输出顺序:

获得金币 +10
获得经验 +20
任务完成

Stack 是什么

Stack<T> 是栈:

后进先出。

也叫 LIFO:

Last In, First Out

就像一摞盘子:

最后放上去的盘子,最先被拿走。

声明:

Stack<string> stack = new();

入栈:

stack.Push("PageA");
stack.Push("PageB");
stack.Push("PageC");

出栈:

string top = stack.Pop();

拿到的是:

PageC

Stack 常用操作

入栈:

stack.Push("PageA");

出栈并移除:

string page = stack.Pop();

查看栈顶但不移除:

string top = stack.Peek();

数量:

int count = stack.Count;

清空:

stack.Clear();

空栈时直接 Pop()Peek() 也会报错。

所以通常:

if (stack.Count > 0)
{
    string page = stack.Pop();
}

Stack 适合什么场景

Stack 适合回退、撤销、嵌套结构:

UI 页面返回栈
撤销操作 Undo
递归改成非递归
括号匹配
DFS 深度优先搜索
状态压栈和恢复

例子:UI 页面返回。

Stack<string> pageStack = new();

pageStack.Push("Home");
pageStack.Push("Bag");
pageStack.Push("ItemDetail");

string current = pageStack.Pop();   // ItemDetail
string previous = pageStack.Peek(); // Bag

含义:

当前关闭 ItemDetail,回到 Bag。

Queue 和 Stack 对比

同样放入:

1, 2, 3

Queue 取出:

1, 2, 3

Stack 取出:

3, 2, 1

示例:

Queue<int> queue = new();
queue.Enqueue(1);
queue.Enqueue(2);
queue.Enqueue(3);

Debug.Log(queue.Dequeue()); // 1
Stack<int> stack = new();
stack.Push(1);
stack.Push(2);
stack.Push(3);

Debug.Log(stack.Pop()); // 3

和 List 的区别

可以用 List 模拟栈:

list.Add(item); // push
var item = list[^1];
list.RemoveAt(list.Count - 1); // pop

但队列用 List 从头删很差:

var item = list[0];
list.RemoveAt(0);

RemoveAt(0) 会导致后面所有元素前移,成本是 O(n)

Queue.Dequeue() 更适合队列场景。

完整运行:任务排队与操作撤销

为什么 Queue 适合任务处理:先到的请求必须先执行。

Queue<string> jobs = new();
jobs.Enqueue("加载配置");
jobs.Enqueue("创建角色");
jobs.Enqueue("进入场景");

while (jobs.TryDequeue(out string? job))
    Console.WriteLine(job);

输出严格保持入队顺序:

加载配置
创建角色
进入场景

为什么 Stack 适合撤销:最后完成的操作必须最先回退。

Stack<string> undo = new();
undo.Push("放置节点 A");
undo.Push("移动节点 A");
undo.Push("删除节点 A");

while (undo.TryPop(out string? operation))
    Console.WriteLine($"撤销: {operation}");

输出顺序相反:

撤销: 删除节点 A
撤销: 移动节点 A
撤销: 放置节点 A

边界:空容器不能直接取值

队列为空时调用 Dequeue()、栈为空时调用 Pop() 都会抛出异常。输入是否可能为空不确定时,使用 TryDequeueTryPop

if (!jobs.TryDequeue(out string? nextJob))
    Console.WriteLine("当前没有待处理任务");

if (!undo.TryPop(out string? lastOperation))
    Console.WriteLine("当前没有可撤销操作");

另一个边界是容量无限增长:生产者持续入队而消费者处理不过来时,Queue 会占用越来越多内存。工程系统要监控队列长度,并明确限流、丢弃、合并或背压策略。

最重要的收获

Queue:适合排队处理,先来的先处理。
Stack:适合回退撤销,后来的先处理。

最常记的 API:

queue.Enqueue(item);
queue.Dequeue();
queue.Peek();

stack.Push(item);
stack.Pop();
stack.Peek();