Queue<T>
是 .NET 中实现队列(先进先出)的一种泛型集合类。它基于数组实现,支持动态扩容、线程不安全,适用于大多数需要队列结构的场景。
一、类结构与字段说明
public class Queue<T> : IEnumerable<T>, ICollection, IReadOnlyCollection<T>{private T[] _array;private int _head; //指向队列中最先进入的元素位置,执行出队(Dequeue)操作时使用此索引private int _tail; // 指向下一个要插入的位置,而不是最后一个元素的位置private int _size; // 元素个数private int _version;private const int MinimumGrow = 4;private const int GrowFactor = 200; // double each time}
- 实现了
IEnumerable<T>
、ICollection
和IReadOnlyCollection<T>
接口,支持枚举、集合操作和只读访问。 - 内部使用环形数组(循环队列)实现,避免频繁复制和移动数据。
核心字段:
字段名 | 类型 | 说明 |
---|---|---|
_array | T[] | 存储元素的数组 |
_head | int | 队头索引(下一个要出队的元素) |
_tail | int | 队尾索引(下一个要入队的位置) |
_size | int | 当前队列中元素个数 |
_version | int | 用于枚举器版本控制,防止修改时遍历异常 |
MinimumGrow | int | 最小扩容数量(4) |
GrowFactor | int | 扩容因子(200%) |
二、构造函数
1. 默认构造函数
public Queue()
{_array = Array.Empty<T>();
}
- 初始化一个空队列,默认容量为 0,首次入队时自动扩容。
2. 指定初始容量的构造函数
public Queue(int capacity)
{if (capacity < 0)throw new ArgumentOutOfRangeException(...);_array = new T[capacity];
}
- 指定初始容量,避免频繁扩容。
3. 使用 IEnumerable<T>
构造
public Queue(IEnumerable<T> collection)
{_array = EnumerableHelpers.ToArray(collection, out _size);if (_size != _array.Length) _tail = _size;
}
- 将集合一次性入队,注意
_tail
的处理。
三、核心操作方法
1. 入队 Enqueue
public void Enqueue(T item)
{// 如果当前队列已满(元素数量等于数组长度),需要扩容if (_size == _array.Length){// 根据 GrowFactor(默认 200)计算新的容量,即当前容量的 200%// 例如:当前容量是 4,扩容后是 8;当前容量是 100,扩容后是 200int newcapacity = (int)((long)_array.Length * (long)GrowFactor / 100);// 如果计算出的新容量仍小于当前容量加上最小增长量(MinimumGrow = 4)// 则直接使用当前容量 + MinimumGrow,确保至少增加 4 个容量if (newcapacity < _array.Length + MinimumGrow){newcapacity = _array.Length + MinimumGrow;}// 调用 SetCapacity 方法,创建一个新的数组并复制旧数据SetCapacity(newcapacity);}// 将新元素插入到队尾位置_array[_tail] = item;// 移动队尾指针到下一个位置(考虑环形数组)// 例如:如果当前是数组末尾,则回到数组开头MoveNext(ref _tail);// 队列元素数量增加 1_size++;// 增加版本号,用于在枚举时检测集合是否被修改(防止 InvalidOperationException)_version++;
}
- 如果数组已满,则调用
SetCapacity
进行扩容。 - 插入到
_tail
位置,更新_tail
和_size
。
扩容逻辑:
- 默认扩容为原来的 200%(即翻倍),如果不够,则至少扩容 4 个单位。
2. 出队 Dequeue
public T Dequeue()
{if (_size == 0)ThrowForEmptyQueue();T removed = _array[_head];if (RuntimeHelpers.IsReferenceOrContainsReferences<T>()){_array[_head] = default!;}MoveNext(ref _head);_size--;_version++;return removed;
}
- 如果队列为空抛出异常。
- 获取
_head
处的元素并清除(引用类型时)。 - 更新
_head
和_size
。
3. 尝试出队 TryDequeue
public bool TryDequeue([MaybeNullWhen(false)] out T result)
{if (_size == 0){result = default!;return false;}result = _array[_head];if (RuntimeHelpers.IsReferenceOrContainsReferences<T>()){_array[_head] = default!;}MoveNext(ref _head);_size--;_version++;return true;
}
- 不抛出异常,返回
false
表示队列为空。
4. 查看队首 Peek
public T Peek()
{if (_size == 0)ThrowForEmptyQueue();return _array[_head];
}
- 返回队首元素,不移除。
5. 尝试查看队首 TryPeek
public bool TryPeek([MaybeNullWhen(false)] out T result)
{if (_size == 0){result = default!;return false;}result = _array[_head];return true;
}
- 类似
TryDequeue
,不抛异常。
6. 清空队列 Clear
public void Clear()
{// 如果队列中有元素(_size > 0),才需要进行清理操作if (_size != 0){// 如果 T 是引用类型或包含引用类型(如类、字符串等),需要显式清空内存// 避免内存泄漏(例如引用未被释放,GC 无法回收)if (RuntimeHelpers.IsReferenceOrContainsReferences<T>()){// 情况 1:元素是连续存储的(_head < _tail)if (_head < _tail){// 清空从 _head 开始的 _size 个元素Array.Clear(_array, _head, _size);}else{// 情况 2:元素是环形存储的(_head > _tail)// 第一步:清空从 _head 到数组末尾的部分Array.Clear(_array, _head, _array.Length - _head);// 第二步:清空从数组开头到 _tail 的部分Array.Clear(_array, 0, _tail);}}// 将元素数量置为 0_size = 0;}// 重置队头和队尾指针为 0,表示队列为空_head = 0;_tail = 0;// 增加版本号,用于防止在枚举过程中修改队列导致异常(如 InvalidOperationException)_version++;
}
情况 1:连续存储
_array = [_, _, 1, 2, 3, _]
_head = 2
_tail = 5
_size = 3
_head < _tail
,执行Array.Clear(_array, 2, 3)
,清除 1, 2, 3_size = 0
_head = 0
,_tail = 0
情况 2:环形存储
_array = [4, _, _, _, 1, 2, 3]
_head = 4
_tail = 1
_size = 4
- 执行两次
Array.Clear
:Array.Clear(_array, 4, 3)
→ 清除 1, 2, 3Array.Clear(_array, 0, 1)
→ 清除 4
_size = 0
_head = 0
,_tail = 0
7. 遍历队列 GetEnumerator
public Enumerator GetEnumerator()
{return new Enumerator(this);
}
- 返回一个
Enumerator
,支持foreach
遍历。 - 内部使用
_version
检测是否在遍历时被修改。
8. 转换为数组 ToArray
public T[] ToArray()
{// 创建一个与当前队列大小相同的新数组T[] arr = new T[_size];// 判断队列中元素的存储方式(连续 or 环形)// 情况 1:队列元素是连续存储的(_head <= _tail)if (_head < _tail){// 将数组中从 _head 开始的 _size 个元素复制到新数组的起始位置Array.Copy(_array, _head, arr, 0, _size);}else{// 情况 2:队列元素是环形存储的(_head > _tail)// 即:队列数据从 _head 到数组末尾,再从数组开头到 _tail// 第一步:复制从 _head 到数组末尾的部分到新数组的起始位置Array.Copy(_array, _head, arr, 0, _array.Length - _head);// 第二步:复制从数组开头到 _tail 的部分到新数组后续位置Array.Copy(_array, 0, arr, _array.Length - _head, _tail);}// 返回包含队列所有元素的新数组return arr;
}
情况 1:连续存储
_array = [_, _, 1, 2, 3, _]
_head = 2
_tail = 5
_size = 3
Array.Copy(_array, 2, arr, 0, 3)
→ 复制 1, 2, 3- 返回
[1, 2, 3]
情况 2:环形存储
_array = [4, _, _, _, 1, 2, 3]
_head = 4
_tail = 1
_size = 4
- 执行两次
Array.Copy
:Array.Copy(_array, 4, arr, 0, 3)
→ 复制 1, 2, 3Array.Copy(_array, 0, arr, 3, 1)
→ 复制 4 到 3 的后面
- 返回
[1, 2, 3, 4]
9. 扩容方法 SetCapacity
private void SetCapacity(int capacity)
{// 创建一个新的数组,容量为传入的 capacityT[] newarray = new T[capacity];// 如果当前队列中有元素(_size > 0),则需要复制旧数据到新数组if (_size > 0){// 情况 1:队列中元素是连续存储的(没有环绕)// 即:队头索引 < 队尾索引,元素从 _head 到 _tail - 1 连续存放if (_head < _tail){// 将旧数组中从 _head 开始的 _size 个元素复制到新数组的起始位置Array.Copy(_array, _head, newarray, 0, _size);}else{// 情况 2:队列中元素是环绕存储的(即队尾索引小于队头索引)// 例如:数组最后几个元素是队列的一部分,队头在数组末尾附近,队尾在数组开头附近// 第一步:复制从 _head 到数组末尾的部分Array.Copy(_array, _head, newarray, 0, _array.Length - _head);// 第二步:复制从数组开头到 _tail 的部分Array.Copy(_array, 0, newarray, _array.Length - _head, _tail);}}// 将旧数组替换为新数组_array = newarray;// 重置队头为新数组的起始位置(0)_head = 0;// 设置队尾位置:// 如果队列已满(_size == capacity),则队尾回到 0(表示队列已满)// 否则,队尾指向当前元素数量的位置(即下一个插入的位置)_tail = (_size == capacity) ? 0 : _size;// 增加版本号,用于防止在遍历时修改队列导致异常(如 InvalidOperationException)_version++;
}
情况 1:连续存储
_array = [_, _, 1, 2, 3, 4] (_head = 2, _tail = 6, _size = 4)
扩容到 capacity = 8复制后:
newarray = [1, 2, 3, 4, _, _, _, _]
_head = 0
_tail = 4
情况 2:环形存储
_array = [4, _, _, _, 1, 2, 3] (_head = 4, _tail = 1, _size = 4)
扩容到 capacity = 8复制后:
newarray = [1, 2, 3, 4, _, _, _, _]
_head = 0
_tail = 4
10. 指针移动 MoveNext
private void MoveNext(ref int index)
{int tmp = index + 1;if (tmp == _array.Length){tmp = 0;}index = tmp;
}
- 实现环形数组的指针移动。
四、其他辅助方法
1. Contains
public bool Contains(T item)
{if (_size == 0)return false;if (_head < _tail)return Array.IndexOf(_array, item, _head, _size) >= 0;elsereturnArray.IndexOf(_array, item, _head, _array.Length - _head) >= 0 ||Array.IndexOf(_array, item, 0, _tail) >= 0;
}
- 判断队列中是否包含某个元素。
- 分段查找环形数组中的内容。
2. CopyTo
public void CopyTo(T[] array, int arrayIndex)
{// 参数检查:目标数组不能为 nullif (array == null){throw new ArgumentNullException(nameof(array));}// 参数检查:arrayIndex 必须在 [0, array.Length] 范围内if (arrayIndex < 0 || arrayIndex > array.Length){throw new ArgumentOutOfRangeException(nameof(arrayIndex), arrayIndex, SR.ArgumentOutOfRange_Index);}// 获取目标数组的长度int arrayLen = array.Length;// 检查目标数组从 arrayIndex 开始是否有足够空间容纳 _size 个元素if (arrayLen - arrayIndex < _size){throw new ArgumentException(SR.Argument_InvalidOffLen);}// 要复制的元素数量,初始为整个队列的大小int numToCopy = _size;// 如果队列为空,直接返回if (numToCopy == 0)return;// 第一部分复制:从 _head 开始,到数组末尾为止,最多能复制多少个元素int firstPart = Math.Min(_array.Length - _head, numToCopy);// 复制第一部分:从 _head 到数组末尾Array.Copy(_array, _head, array, arrayIndex, firstPart);// 减去已复制的元素数量numToCopy -= firstPart;// 如果还有剩余元素,说明队列是环形存储,需要复制第二部分(从数组开头到 _tail)if (numToCopy > 0){// 继续复制剩余元素,从数组开头开始复制Array.Copy(_array, 0, array, arrayIndex + firstPart, numToCopy);}
}
复制原理和SetCapacity
方法相同
五、迭代器
// 实现 Queue<T> 的枚举器(迭代器),用于支持 foreach 循环。
// 枚举器使用队列内部的版本号(_version)来确保在枚举期间队列没有被修改。
public struct Enumerator : IEnumerator<T>, System.Collections.IEnumerator
{// 引用当前队列private readonly Queue<T> _q;// 保存队列的版本号,用于检测在枚举过程中队列是否被修改private readonly int _version;// 当前枚举索引:// -1 表示未开始// -2 表示已结束或已释放private int _index;// 当前枚举的元素private T? _currentElement;// 构造函数:初始化枚举器internal Enumerator(Queue<T> q){_q = q;_version = q._version; // 保存队列当前版本号_index = -1; // 初始状态为未开始_currentElement = default;}// 清理资源,标记枚举结束public void Dispose(){_index = -2; // 标记为已释放_currentElement = default;}// 移动到下一个元素public bool MoveNext(){// 检查队列是否被修改,若版本号不一致,抛出异常if (_version != _q._version)throw new InvalidOperationException(SR.InvalidOperation_EnumFailedVersion);// 如果枚举已结束,直接返回 falseif (_index == -2)return false;// 索引递增_index++;// 如果索引超出队列大小,说明已经枚举完所有元素if (_index == _q._size){_index = -2; // 标记为结束_currentElement = default;return false;}// 获取队列的内部数组和容量T[] array = _q._array;int capacity = array.Length;// 计算实际数组索引:// 队列的元素不一定从数组索引 0 开始,并且可能是环形存储的。int arrayIndex = _q._head + _index;// 如果索引越界(环形),手动回绕(比取模更快)if (arrayIndex >= capacity){arrayIndex -= capacity;}// 保存当前元素_currentElement = array[arrayIndex];return true;}// 获取当前元素(泛型版本)public T Current{get{if (_index < 0)ThrowEnumerationNotStartedOrEnded();return _currentElement!;}}// 抛出异常:枚举未开始或已结束private void ThrowEnumerationNotStartedOrEnded(){Debug.Assert(_index == -1 || _index == -2);throw new InvalidOperationException(_index == -1 ? SR.InvalidOperation_EnumNotStarted : SR.InvalidOperation_EnumEnded);}// 获取当前元素(非泛型版本)object? IEnumerator.Current{get { return Current; }}// 重置枚举器(不推荐使用,因为 IEnumerator.Reset() 是显式接口实现)void IEnumerator.Reset(){if (_version != _q._version)throw new InvalidOperationException(SR.InvalidOperation_EnumFailedVersion);_index = -1; // 重置为未开始状态_currentElement = default;}
}
设计亮点总结
功能 | 说明 |
---|---|
版本号检测 | 使用 _version 检测队列是否在枚举期间被修改,防止并发修改异常 |
环形数组支持 | 正确处理 _head 和 _index 的组合,支持非连续存储的队列 |
性能优化 | 使用 arrayIndex -= capacity 替代 % 运算符,提升性能 |
状态管理 | 使用 _index 的状态区分枚举是否开始、进行中或已结束 |
线程安全限制 | 不是线程安全的,但能检测到队列被修改 |
资源释放 | 提供 Dispose() 方法清理资源,符合 .NET 枚举器规范 |
Queue<T>.Enumerator
是一个支持环形数组结构的枚举器,通过保存队列的版本号来检测并发修改,确保枚举过程的稳定性,同时使用高效的索引计算方式处理非连续存储的数据。
七、结语
Queue<T>
是 .NET 中一个非常实用的泛型集合类,其基于环形数组的实现方式在性能和内存管理上做了很好的权衡。