导读:本期聚焦于创作的《C#中如何使用Queue队列?掌握这6种常用操作方式》,敬请观看详情。还在用 List 反复 RemoveAt 模拟排队吗?C# 内置的 Queue 泛型队列专门处理先进先出场景,但容量扩展、出队异常和线程安全经常被忽视。本文围绕入队 Enqueue、出队 Dequeue、查看 Peek、数组转换 ToArray、判断 Contains、清空 Clear 以及安全方法 TryDequeue 等六类操作展开,说明行为差异和适用时机,并给出可直接运行的示例。同时会对比普通 Queue 与 ConcurrentQueue 在多线程环境下的选择,提醒扩容机制对性能的影响。读完可以避免空队列调用 Dequeue 或 Peek 导致的 InvalidOperationException,也能更合理地设置初始容量。

C# 中的 Queue<T> 是处理先进先出场景最直接的数据结构。它内部基于环形数组实现,入队和出队都接近 O(1),适合任务排队、消息缓冲、打印调度、事件处理等业务。有些开发者习惯用 List<T> 加 RemoveAt(0) 来模拟队列,但这样每次删除头部都会移动整个数组,数据量一大性能会明显下降。本文将围绕 Queue<T> 的六类常用操作展开,包括初始化、入队出队、安全查看、数组转换、查找清空以及多线程环境下的 ConcurrentQueue<T> 选择,并结合可直接运行的代码说明它们的行为差异。

C#中如何使用Queue队列?掌握这6种常用操作方式

一、创建队列并合理设置初始容量

Queue<T> 位于 System.Collections.Generic 命名空间。最常用的创建方式是使用无参构造函数,然后逐个调用 Enqueue。如果已经知道元素来源,也可以直接把数组、列表等 IEnumerable<T> 对象传入构造函数,一次性完成初始化。

从性能角度看,初始容量设置经常被忽略。队列内部维护一个数组,当元素数量达到数组上限时,会分配一个更大的数组并把旧数据复制过去。这个扩容动作并不是免费的,尤其是队列中已有数万或数十万元素时。因此,如果能预估任务数量,建议在创建时指定一个足够大的容量。例如一个固定批量任务有 2000 条,用 new Queue<Order>(2000) 会比默认容量反复扩容减少多次内存分配。

using System;
using System.Collections.Generic;

var queue = new Queue<string>();
queue.Enqueue("first");
queue.Enqueue("second");

var preset = new Queue<int>(capacity: 128);

int[] source = { 3, 1, 4, 1, 5 };
var fromArray = new Queue<int>(source);

Console.WriteLine(queue.Count);

上面代码展示了三种创建方式:无参创建、指定初始容量、从数组创建。创建后可以通过 Count 属性读取当前元素个数。需要注意 Capacity 不是 Queue<T> 的直接公开属性,初始容量只在构造时传入,后续要优化内存可以调用 TrimExcess。

二、入队、出队与安全查看

入队使用 Enqueue,元素会被放到队尾;出队使用 Dequeue,它会移除并返回队首元素;只查看队首而不移除则使用 Peek。这三者是 Queue<T> 最基础的操作。需要注意,当队列为空时,直接调用 Dequeue 或 Peek 会抛出 InvalidOperationException。

为了避免异常,现代 .NET 版本提供了 TryDequeue 和 TryPeek。它们通过 out 参数返回元素,如果队列为空则返回 false,而不是中断程序。这种写法特别适合循环消费队列的场景,不需要在每次操作前都判断 Count > 0。

var queue = new Queue<int>();
queue.Enqueue(10);
queue.Enqueue(20);

if (queue.TryPeek(out int head))
{
    Console.WriteLine($"队首元素:{head}");
}

while (queue.TryDequeue(out int item))
{
    Console.WriteLine(item);
}

在上面的循环中,TryDequeue 会一直取出元素,直到队列为空。与 Dequeue 相比,它省去了手动捕获异常或检查 Count 的步骤,代码更简洁,在多线程合作消费时也更安全。

三、遍历、快照与复制到数组

Queue<T> 实现了 IEnumerable<T>,因此可以直接用 foreach 按照从队首到队尾的顺序读取元素。遍历不会移除任何元素,队列内容保持不变,这一点和 Dequeue 有本质区别。

如果要在遍历后保存一份当时的队列快照,可以调用 ToArray。它返回一个新的数组,后续再往队列中添加或删除元素都不会影响这个快照。快照适合日志记录、批量入库或者把队列状态交给其他模块处理。

var queue = new Queue<string>();
queue.Enqueue("A");
queue.Enqueue("B");
queue.Enqueue("C");

foreach (var item in queue)
{
    Console.WriteLine(item);
}

string[] snapshot = queue.ToArray();
queue.Enqueue("D");
Console.WriteLine(snapshot.Length); // 输出 3

var buffer = new string[5];
queue.CopyTo(buffer, 1);

CopyTo 与 ToArray 类似,但目标数组需要事先分配。第二个参数表示从目标数组的哪个索引开始写入。如果目标数组剩余空间不足,会抛出 ArgumentException。在实际使用时,最好根据 queue.Count 来分配数组长度,避免硬编码。

四、查找元素与清空队列

当需要判断某个元素是否已经在队列中时,可以使用 Contains。它按照队列顺序做线性查找,时间复杂度为 O(n)。如果元素类型是自定义类,Contains 会根据引用相等或你重写的 Equals 方法来判断。

Clear 会移除队列中的所有元素,并把 Count 重置为 0。但这里有一个容易误解的地方:Clear 通常不会立即收缩内部数组。也就是说,如果队列曾经扩容到很大的容量,清空后内存仍然被内部数组占用。只有调用 TrimExcess 才可能释放多余空间,但 TrimExcess 本身有复制成本,不应频繁调用。

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

bool exists = queue.Contains(2);
Console.WriteLine(exists);

queue.Clear();
Console.WriteLine(queue.Count);

queue.TrimExcess();

如果你的业务逻辑是长时间运行的服务,并且队列会经历“高峰期扩容、低峰期清空”的周期,那么只在确定需要释放内存时调用一次 TrimExcess 是合理的做法。否则,频繁扩容和收缩反而会增加 GC 压力。

五、多线程场景与 ConcurrentQueue 的选择

普通的 Queue<T> 不是线程安全的。多个线程同时执行 Enqueue 或 Dequeue,可能破坏内部数组状态,导致元素丢失或出现异常。如果并发量不大,可以用 lock 包住所有队列操作,保证一次只有一个线程进入临界区。

如果系统存在多个生产者或多个消费者,更推荐使用 System.Collections.Concurrent 命名空间下的 ConcurrentQueue<T>。它内部使用无锁或细粒度锁实现,提供了 Enqueue、TryDequeue、TryPeek 等方法,不需要额外加锁。

using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;

var safeQueue = new ConcurrentQueue<int>();

Parallel.For(0, 1000, i =>
{
    safeQueue.Enqueue(i);
});

int consumed = 0;
while (safeQueue.TryDequeue(out int value))
{
    consumed++;
}

Console.WriteLine(consumed);

使用 ConcurrentQueue 时要注意,它的 Count 属性并不是 O(1),因为在无锁数据结构中统计元素数量可能需要遍历内部段。判断队列是否为空时,优先使用 IsEmpty 属性。如果只是简单的高并发入队和出队,ConcurrentQueue 比手动加锁更加省心。

六、用初始容量与 TrimExcess 控制性能

容量变化带来的性能波动常常被忽略。假设一个服务每秒处理 500 条消息,高峰期可能积压 10 万条。如果创建队列时使用默认容量,那么从第一条消息入队到达到 10 万条,内部数组可能经历十几次扩容和复制。虽然整体复杂度仍然是 O(n),但复制旧数组、触发 GC 这些动作会拉高延迟。

反过来,如果一开始就创建 new Queue<Message>(100000),可以完全避免高峰期的扩容。缺点是可能占用超过实际需要的内存。因此合理设置初始容量是代码和业务场景之间的权衡。如果无法预估,也可以采用分段策略:先设置一个中等容量,观察监控数据,再在配置中调整。

using System;
using System.Collections.Generic;
using System.Diagnostics;

var sw = Stopwatch.StartNew();
var queueWithoutCapacity = new Queue<int>();
for (int i = 0; i < 200000; i++)
{
    queueWithoutCapacity.Enqueue(i);
}
sw.Stop();
Console.WriteLine($"默认容量耗时:{sw.ElapsedMilliseconds} ms");

sw.Restart();
var queueWithCapacity = new Queue<int>(200000);
for (int i = 0; i < 200000; i++)
{
    queueWithCapacity.Enqueue(i);
}
sw.Stop();
Console.WriteLine($"预设容量耗时:{sw.ElapsedMilliseconds} ms");

上面的测试结果会受到机器性能和 .NET 版本影响,但预设容量的耗时通常会低于反复扩容的版本。对于日志缓冲、消息队列等高频入队场景,把容量调优做好,往往比纠结单个 API 的微小差异更有效。

C# Queue队列Queue队列用法先进先出队列修改时间:2026-09-18 22:31:14

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0918/58989.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。