Почему не стоит наследовать от List<T> в C#? Альтернативы и лучшие практики
Почему наследование от List — это антипаттерн
Ваш пример отлично иллюстрирует типичную ошибку начинающих разработчиков. На первый взгляд, наследование от List кажется логичным: вы получаете готовую функциональность списка (индексация, сортировка, поиск) и просто добавляете свою логику. Однако на практике это приводит к нескольким серьезным проблемам.
Основные проблемы наследования от List
- Нарушение принципа подстановки Лисков (LSP). Вы не можете просто заменить
Listна ваш классOrderListв любом месте кода. Например, если кто-то вызоветorderList.AddRange(orders), то ваш пересчет суммы не сработает, так какAddRangeне переопределен (вы переопределили толькоAddиRemove). - Неполное переопределение.
Listимеет множество методов:Insert,RemoveAt,Clear,AddRange,RemoveAll, индексаторthis[]и другие. Чтобы ваш класс работал корректно, вам пришлось бы переопределить все методы, изменяющие коллекцию. Это огромный объем работы, и вы гарантированно что-то упустите. - Проблемы с полиморфизмом. Если метод принимает
List, а вы передаете емуOrderList, то внутри метода будут вызываться методы базового класса (List), а не ваши переопределенные. Использованиеnew(как в вашем примере) не решает проблему — это просто скрытие метода, а не переопределение. - Нарушение инкапсуляции.
Listпредоставляет доступ к внутренним данным через индексатор и множество методов. Вы не можете контролировать, как именно будут изменяться данные. Пользователь вашего класса может напрямую изменить элемент через индекс:orderList[0] = new Order()— и вашTotalSumстанет неактуальным.
Правильное решение: композиция и Collection
Вместо наследования от List используйте композицию — включите список как приватное поле. А если вам нужна коллекция с дополнительной логикой, наследуйте от Collection или BindingList. Эти классы специально спроектированы для расширения и предоставляют виртуальные методы для переопределения.
Вот как должен выглядеть ваш класс правильно:
public class OrderCollection : Collection<Order>
{
public decimal TotalSum { get; private set; }
protected override void InsertItem(int index, Order item)
{
base.InsertItem(index, item);
TotalSum += item.Price;
}
protected override void RemoveItem(int index)
{
TotalSum -= this[index].Price;
base.RemoveItem(index);
}
protected override void SetItem(int index, Order item)
{
TotalSum -= this[index].Price; // вычитаем старый элемент
TotalSum += item.Price; // добавляем новый
base.SetItem(index, item);
}
protected override void ClearItems()
{
TotalSum = 0;
base.ClearItems();
}
}
Почему это лучше?
- Полный контроль. Все методы, изменяющие коллекцию, проходят через защищенные виртуальные методы
InsertItem,RemoveItem,SetItemиClearItems. Вы переопределяете их один раз и гарантируете, что логика пересчета суммы сработает для любого изменения. - Соблюдение LSP.
CollectionреализуетIList,ICollectionиIEnumerable. Вы можете передавать ваш класс в любой метод, ожидающий эти интерфейсы, и все будет работать корректно. - Инкапсуляция. Вы контролируете, как изменяются данные. Никто не сможет "обойти" вашу логику, так как все пути изменения коллекции ведут к вашим переопределенным методам.
Альтернатива: если вам нужен именно List
Если вам критически важна производительность List (например, для очень больших коллекций) или вам нужны методы вроде Sort() и BinarySearch(), то используйте композицию с явной реализацией интерфейса:
public class OrderListWrapper : IList<Order>
{
private readonly List<Order> _list = new List<Order>();
public decimal TotalSum { get; private set; }
// Явная реализация IList<T>
public void Add(Order item)
{
_list.Add(item);
TotalSum += item.Price;
}
public bool Remove(Order item)
{
if (_list.Remove(item))
{
TotalSum -= item.Price;
return true;
}
return false;
}
// Остальные методы IList<T> реализуются аналогично
// ...
}
Этот подход дает вам полный контроль, но требует больше кода. Для большинства сценариев Collection — идеальный выбор.
Дополнительный совет: используйте ObservableCollection для UI
Если вы разрабатываете WPF, MAUI или другое приложение с привязкой данных (data binding), то лучшим выбором будет ObservableCollection. Она уже предоставляет события об изменении коллекции (CollectionChanged), которые автоматически обновляют UI.
Вы можете просто подписаться на это событие и обновлять свою сумму:
public class OrderViewModel
{
public ObservableCollection<Order> Orders { get; } = new();
public decimal TotalSum => Orders.Sum(o => o.Price);
public OrderViewModel()
{
Orders.CollectionChanged += OnOrdersChanged;
}
private void OnOrdersChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// Просто уведомляем UI об изменении свойства TotalSum
OnPropertyChanged(nameof(TotalSum));
}
}
Это самый простой и правильный способ для UI-приложений. Не нужно изобретать велосипед — используйте готовые механизмы .NET.
Краткое резюме
- Не наследуйте от
List— это нарушает ООП-принципы и ведет к багам. - Используйте
Collection, если вам нужна собственная логика при изменении коллекции. - Используйте
ObservableCollection, если работаете с UI и привязкой данных. - Используйте композицию (обертку вокруг
List), если вам критична производительность и нужны специфические методы.
Ответов пока нет.
Войдите чтобы оставить ответ