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>ICollectionIReadOnlyCollection<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,扩容后是 200
        int 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, 3
    • Array.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, 3
    • Array.Copy(_array, 0, arr, 3, 1) → 复制 4 到 3 的后面
  • 返回 [1, 2, 3, 4]


9. 扩容方法 SetCapacity

private void SetCapacity(int capacity)
{
    // 创建一个新的数组,容量为传入的 capacity
    T[] 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;
    else
        return
            Array.IndexOf(_array, item, _head, _array.Length - _head) >= 0 ||
            Array.IndexOf(_array, item, 0, _tail) >= 0;
}
  • 判断队列中是否包含某个元素。
  • 分段查找环形数组中的内容。

2. CopyTo

public void CopyTo(T[] array, int arrayIndex)
{
    // 参数检查:目标数组不能为 null
    if (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);

        // 如果枚举已结束,直接返回 false
        if (_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 中一个非常实用的泛型集合类,其基于环形数组的实现方式在性能和内存管理上做了很好的权衡。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐