Stack 应直接用 new Stack() 初始化,Push/Pop/Peek 操作需检查 Count 避免空栈异常;遍历时禁用 foreach 中 Pop,改用 while;多线程须用 ConcurrentStack 而非加锁。

Stack 的基本创建和压入/弹出操作
直接用 new Stack<t>()</t> 就能初始化,别手贱去继承它或自己写数组模拟——C# 的 Stack<t></t> 已经是泛型、线程不安全但高性能的成熟实现。压入用 Push(),弹出用 Pop(),查顶元素用 Peek()(不移除)。
常见错误:调用 Pop() 前没检查 Count == 0,直接抛 InvalidOperationException: Stack is empty。这不是设计缺陷,是明确告诉你“你忘了兜底”。
Stack<int> s = new(); s.Push(1); s.Push(2); Console.WriteLine(s.Pop()); // 输出 2</int>- 不要用
Stack<object></object>存不同类型的值再手动转型——类型擦除后运行时容易崩,改用具体泛型类型或Stack<dynamic></dynamic>(仅限极少数动态场景) -
Push()时间复杂度是 O(1),但底层扩容时会触发数组复制,频繁 Push 小量数据无感;如果已知大概容量,构造时传入初始容量更稳:new Stack<string>(1024)</string>
遍历 Stack 时为什么不能边遍历边 Pop()
因为 Stack<t></t> 的枚举器(foreach 所用)是“快照式”的——它按当前栈内元素从顶到底顺序返回副本,不反映后续 Pop() 或 Push() 的变化。但如果你在 foreach 中调用 Pop(),实际是在修改原集合,.NET 会立刻抛 InvalidOperationException: Collection was modified。
真实需求通常是“清空并处理每个元素”,这时别 foreach,改用 while:
while (s.Count > 0) { var item = s.Pop(); Process(item); }- 想保留原栈?先
ToArray()或ToList()拷贝一份再遍历(注意:ToArray()返回的是从底到顶顺序,和栈逻辑相反) - 需要从底到顶遍历?别硬绕,转成
List<t></t>后Reverse(),或者直接用s.ToList().AsEnumerable().Reverse()
Stack 和 List 在 LIFO 场景下的取舍
表面上都能实现后进先出,但语义和性能差很远。Stack<t></t> 的 API 强制你只关注栈顶,Push()/Pop() 是唯一自然入口;而 List<t></t> 要靠 Add() + RemoveAt(list.Count - 1) 模拟,代码啰嗦且易错(比如写成 RemoveAt(0) 就变成 FIFO 了)。
- 用
List<t></t>模拟栈:每次RemoveAt()是 O(n),因为要移动后面所有元素;Stack<t></t>的Pop()是稳定 O(1) -
Stack<t></t>不支持随机索引访问(没有this[int]),这是故意的——如果你频繁需要索引,说明它根本不是栈场景,该换数据结构 - 兼容性注意:.NET Framework 2.0+ 和所有 .NET Core/.NET 5+ 都有
Stack<t></t>,无需额外 NuGet
多线程环境下 Stack 的典型误用
Stack<t></t> 默认**不是线程安全的**。两个线程同时 Push() 可能导致内部数组状态错乱,甚至出现 NullReferenceException 或静默丢数据——它不会加锁,也不会报“线程不安全”警告。
- 真要跨线程共享栈?用
ConcurrentStack<t></t>,它的Push()/TryPop()是原子操作,且TryPop()返回 bool 表示是否成功,比捕异常更合理 - 别给
Stack<t></t>加lock后当并发栈用——锁粒度大,吞吐低,还容易死锁;ConcurrentStack<t></t>内部用无锁算法,性能好得多 - 常见陷阱:把
Stack<t></t>放在静态字段里供多个请求共用,没加锁也没换并发版本,压测时偶发崩溃或逻辑错乱
栈不是万能容器,它只解决“最后一次操作最优先响应”这个具体问题。用错场景比用错语法更容易埋雷——比如用它存用户会话上下文却不考虑生命周期管理,最后栈越堆越深,内存涨得无声无息。


















