Introduction: The Trap Many Have Fallen Into
\nImagine 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.
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.
\nIn 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!
What's Wrong with List<T> from a Design Perspective?
\nLet'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.
Take a look at its declaration in the .NET source code (simplified):
\npublic 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}\nNotice: 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.
Problem #1: Violation of the Liskov Substitution Principle (LSP)
\nThe 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.
Example: you create a class ReadOnlyLibrary : List<Book> and override Add to throw an exception:
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.\nEven 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.
Problem #2: Violation of Encapsulation
\nBy 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.
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\nUnder the Hood: How List<T> Works Internally
\nTo under