Why Inheriting from List<T> in C# Is a Bad Idea: Analysis

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: The Trap Many Have Fallen Into

\n

Imagine a scenario: you're writing an application for an online library. You need your own Library class that stores books, can sort them, search, and display statistics. The first thing that comes to mind is to inherit from List<Book>. After all, it already has everything: Add, Remove, indexing, searching. Logical? It seems so.

\n

But experienced C# developers will tell you: "Never inherit from List<T>". This question on Stack Overflow has garnered thousands of views, and for good reason. Behind the apparent simplicity lie serious problems with architecture, performance, and most importantly, violation of object-oriented design principles.

\n

In this article, we'll look under the hood of List<T>, understand why inheriting from it is an antipattern, and what to use instead. Ready? Let's go!

\n\n

What's Wrong with List<T> from a Design Perspective?

\n

Let's start with the main point: List<T> was designed not as a base for a hierarchy, but as a concrete, high-performance implementation of a dynamic array. Its public API is a set of methods for working with a collection, not a contract for extension.

\n

Take a look at its declaration in the .NET source code (simplified):

\n
public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection, IEnumerable\n{\n    private T[] _items;\n    private int _size;\n    \n    public void Add(T item) { /* ... */ }\n    public bool Remove(T item) { /* ... */ }\n    // ... dozens of other methods\n}
\n

Notice: List<T> is not marked as abstract. It does not provide virtual methods for overriding (except for a few, which we'll discuss later). This means you cannot safely change its behavior. If you override Add, you risk breaking the internal logic that depends on this method.

\n\n

Problem #1: Violation of the Liskov Substitution Principle (LSP)

\n

The Liskov Substitution Principle states: "A derived class should supplement, not replace, the behavior of the base class." When you inherit from List<T>, you promise that your class can be used anywhere a List<T> is expected. But this is rarely fulfilled.

\n

Example: you create a class ReadOnlyLibrary : List<Book> and override Add to throw an exception:

\n
public class ReadOnlyLibrary : List<Book>\n{\n    public new void Add(Book book)\n    {\n        throw new InvalidOperationException("Collection is read-only!");\n    }\n}\n\n// Somewhere in the code:\nList<Book> list = new ReadOnlyLibrary();\nlist.Add(new Book()); // Oops! No exception will be thrown!\n// Because new does not override, it hides the method.\n// List.Add will be called, not ours.
\n

Even if you use override (and List.Add is not virtual), you cannot prevent adding — the IList interface does not provide for this. You simply break the contract.

\n\n

Problem #2: Violation of Encapsulation

\n

By inheriting from List<T>, you expose its entire public API. Your Library class will get methods like Sort, BinarySearch, Reverse, Insert, etc. But what if you want books to always be sorted by author? A user of your class can call library.Reverse() and break the invariant.

\n

You cannot prevent this without rewriting all the methods — and there are dozens of them. This is a violation of encapsulation: you expose internal implementation that you cannot control.

\n\n

Under the Hood: How List<T> Works Internally

\n

To under

Blogs

Book Recommendations