Как реализовать кастомные коллекции?
Комментарии (2)
Ответ сгенерирован нейросетью и может содержать ошибки
Реализация кастомных коллекций в .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
}
}
Рекомендации по реализации
-
Выбор базового класса или интерфейса - наследуйтесь от
Collection<T>,KeyedCollection<TKey, TItem>или реализуйте интерфейсы с нуля в зависимости от требований. -
Потокобезопасность - если коллекция будет использоваться в многопоточных сценариях, реализуйте синхронизацию или сделайте коллекцию неизменяемой.
-
Производительность - оптимизируйте операции, которые будут выполняться чаще всего (например,
Add,Containsдля словарей). -
Сериализация - добавьте атрибуты
[Serializable]и реализуйте интерфейсы сериализации при необходимости. -
Валидация данных - добавляйте проверки в методы модификации коллекции.
-
Поддержка LINQ - автоматически доступна при реализации
IEnumerable<T>.
Альтернативные подходы
- Наследование от существующих коллекций - самый простой способ, но может нарушить инкапсуляцию.
- Композиция - включение стандартной коллекции как приватного поля, что обеспечивает лучший контроль.
- Использование Collection<T> как базового класса для простых сценариев.
Кастомные коллекции особенно полезны при работе с специализированными структурами данных, требующими особого управления памятью, специфических алгоритмов доступа или интеграции с неуправляемым кодом. Правильно реализованная кастомная коллекция обеспечивает баланс между производительностью, безопасностью типов и удобством использования.