← Назад к вопросам

Как реализовать кастомные коллекции?

1.0 Junior🔥 122 комментариев
#Автоматизация тестирования

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Реализация кастомных коллекций в .NET

Кастомные коллекции реализуются путем создания классов, которые реализуют стандартные интерфейсы коллекций из пространства имен System.Collections или System.Collections.Generic. Это позволяет создавать специализированные структуры данных с уникальным поведением, сохраняя при этом совместимость с существующими API .NET.

Ключевые интерфейсы для реализации

Основные интерфейсы для создания кастомных коллекций:

  • IEnumerable<T> и IEnumerable - обеспечивают поддержку перечисления через foreach
  • ICollection<T> - базовый интерфейс для всех обобщенных коллекций
  • IList<T> - для коллекций с доступом по индексу
  • IDictionary<TKey, TValue> - для коллекций типа ключ-значение
  • IReadOnlyCollection<T>, IReadOnlyList<T> - для неизменяемых коллекций

Пример реализации кастомной коллекции FixedSizeQueue<T>

Рассмотрим реализацию коллекции с фиксированным размером, которая автоматически удаляет старые элементы при добавлении новых (кольцевой буфер).

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

public class FixedSizeQueue<T> : ICollection<T>, IReadOnlyCollection<T>
{
    private readonly T[] _buffer;
    private int _head;
    private int _tail;
    private int _count;
    private readonly int _capacity;

    public FixedSizeQueue(int capacity)
    {
        if (capacity <= 0)
            throw new ArgumentException("Capacity must be greater than zero", nameof(capacity));
        
        _capacity = capacity;
        _buffer = new T[capacity];
        _head = 0;
        _tail = 0;
        _count = 0;
    }

    public int Count => _count;
    public bool IsReadOnly => false;
    
    public void Enqueue(T item)
    {
        _buffer[_tail] = item;
        _tail = (_tail + 1) % _capacity;
        
        if (_count == _capacity)
            _head = (_head + 1) % _capacity;
        else
            _count++;
    }

    public bool TryDequeue(out T result)
    {
        if (_count == 0)
        {
            result = default;
            return false;
        }
        
        result = _buffer[_head];
        _buffer[_head] = default;
        _head = (_head + 1) % _capacity;
        _count--;
        return true;
    }

    public void Add(T item) => Enqueue(item);
    
    public void Clear()
    {
        Array.Clear(_buffer, 0, _capacity);
        _head = 0;
        _tail = 0;
        _count = 0;
    }
    
    public bool Contains(T item)
    {
        EqualityComparer<T> comparer = EqualityComparer<T>.Default;
        for (int i = 0; i < _count; i++)
        {
            int index = (_head + i) % _capacity;
            if (comparer.Equals(_buffer[index], item))
                return true;
        }
        return false;
    }
    
    public void CopyTo(T[] array, int arrayIndex)
    {
        if (array == null)
            throw new ArgumentNullException(nameof(array));
        if (arrayIndex < 0)
            throw new ArgumentOutOfRangeException(nameof(arrayIndex));
        if (array.Length - arrayIndex < _count)
            throw new ArgumentException("Destination array is too small");
        
        for (int i = 0; i < _count; i++)
        {
            int index = (_head + i) % _capacity;
            array[arrayIndex + i] = _buffer[index];
        }
    }
    
    public bool Remove(T item)
    {
        // Для очереди операция Remove не типична
        // В данном случае реализуем упрощенную версию
        throw new NotSupportedException("Remove operation is not supported for FixedSizeQueue");
    }
    
    public IEnumerator<T> GetEnumerator()
    {
        for (int i = 0; i < _count; i++)
        {
            int index = (_head + i) % _capacity;
            yield return _buffer[index];
        }
    }
    
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

Расширенный пример с поддержкой индексатора

public class IndexedFixedSizeQueue<T> : FixedSizeQueue<T>, IList<T>
{
    public IndexedFixedSizeQueue(int capacity) : base(capacity) { }
    
    public T this[int index]
    {
        get
        {
            if (index < 0 || index >= Count)
                throw new ArgumentOutOfRangeException(nameof(index));
            
            int actualIndex = (_head + index) % _capacity;
            return _buffer[actualIndex];
        }
        set
        {
            if (index < 0 || index >= Count)
                throw new ArgumentOutOfRangeException(nameof(index));
            
            int actualIndex = (_head + index) % _capacity;
            _buffer[actualIndex] = value;
        }
    }
    
    public int IndexOf(T item)
    {
        EqualityComparer<T> comparer = EqualityComparer<T>.Default;
        for (int i = 0; i < Count; i++)
        {
            int index = (_head + i) % _capacity;
            if (comparer.Equals(_buffer[index], item))
                return i;
        }
        return -1;
    }
    
    public void Insert(int index, T item)
    {
        throw new NotSupportedException("Insert operation is not supported for IndexedFixedSizeQueue");
    }
    
    public void RemoveAt(int index)
    {
        throw new NotSupportedException("RemoveAt operation is not supported for IndexedFixedSizeQueue");
    }
}

Практическое использование

// Пример использования кастомной коллекции
public class CollectionUsageExample
{
    public void DemonstrateUsage()
    {
        // Создаем кольцевой буфер на 5 элементов
        var buffer = new FixedSizeQueue<string>(5);
        
        // Добавляем элементы
        buffer.Enqueue("First");
        buffer.Enqueue("Second");
        buffer.Enqueue("Third");
        
        Console.WriteLine($"Count: {buffer.Count}"); // Count: 3
        
        // Перебор элементов
        foreach (var item in buffer)
        {
            Console.WriteLine(item);
        }
        
        // Проверка наличия элемента
        bool containsSecond = buffer.Contains("Second"); // true
        
        // Копирование в массив
        string[] array = new string[buffer.Count];
        buffer.CopyTo(array, 0);
        
        // Когда добавляем больше элементов, чем capacity
        buffer.Enqueue("Fourth");
        buffer.Enqueue("Fifth");
        buffer.Enqueue("Sixth"); // Вытесняет "First"
        
        Console.WriteLine($"New count: {buffer.Count}"); // Все еще 5
    }
}

Рекомендации по реализации

  1. Выбор базового класса или интерфейса - наследуйтесь от Collection<T>, KeyedCollection<TKey, TItem> или реализуйте интерфейсы с нуля в зависимости от требований.

  2. Потокобезопасность - если коллекция будет использоваться в многопоточных сценариях, реализуйте синхронизацию или сделайте коллекцию неизменяемой.

  3. Производительность - оптимизируйте операции, которые будут выполняться чаще всего (например, Add, Contains для словарей).

  4. Сериализация - добавьте атрибуты [Serializable] и реализуйте интерфейсы сериализации при необходимости.

  5. Валидация данных - добавляйте проверки в методы модификации коллекции.

  6. Поддержка LINQ - автоматически доступна при реализации IEnumerable<T>.

Альтернативные подходы

  • Наследование от существующих коллекций - самый простой способ, но может нарушить инкапсуляцию.
  • Композиция - включение стандартной коллекции как приватного поля, что обеспечивает лучший контроль.
  • Использование Collection<T> как базового класса для простых сценариев.

Кастомные коллекции особенно полезны при работе с специализированными структурами данных, требующими особого управления памятью, специфических алгоритмов доступа или интеграции с неуправляемым кодом. Правильно реализованная кастомная коллекция обеспечивает баланс между производительностью, безопасностью типов и удобством использования.

Как реализовать кастомные коллекции? | PrepBro