Why not inherit from ListT?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
When designing software using object-oriented principles, inheritance is often utilized to extend the functionality of existing classes. However, not all inheritance scenarios provide the benefits we might expect. One often-recommended piece of advice in the development community, particularly when working with C# or similar .NET languages, is to avoid inheriting from List<T>. This article explores the technical reasons behind this guidance, supported by examples and additional commentary.
Understanding List<T>
List<T> is a generic collection integrated into the .NET framework, designed to provide developers with a flexible list implementation that handles items of any data type. It offers a plethora of functions, including adding, removing, searching, and iterating over elements. Crucially, List<T> is designed to be a robust and versatile component for handling collections, but it comes with its set of limitations when inheritance is considered.
Why Not to Inherit from List<T>
Encapsulation Principle
When inheriting from List<T>, the derived class automatically inherits all List<T> members which are not intended to be modified. By doing so, you inadvertently break the encapsulation principle because your derived class exposes and thus binds itself to the implementation details of List<T>. This results in tighter coupling and potentially exposes internal mechanisms that should remain hidden.
Risk of Misleading API
Enhancing an inherited class with additional functionality can lead to an API that misleads or confuses as it might not align intuitively with the standard behavior expected from a List<T> class. This is due to the behavior of shadowing or overriding methods, which might alter fundamental list operations (like Add, Remove), producing unexpected side effects.
Inflexibility of Enhanced Data Structures
By inheriting from List<T>, you lock your class into its underlying design and performance characteristics. This means that if you need to switch to a different collection type, you can face considerable refactoring costs, which may be avoided if an aggregation approach (composition over inheritance) was used initially.
Limited Control Over Base Implementation
As List<T> is a sealed and non-abstract class, you cannot override its methods unless they are explicitly virtual. Another implication is that if the base implementation updates or deprecates certain methods, your inherited implementations may become unstable or obsolete.
Performance and Behavior Considerations
Inheriting from List<T> doesn't allow for fine-tuned optimization strategies that might be possible with alternative approaches. For instance, indexing strategies or storage optimizations cannot be customized in an inherited List<T>. In addition, any performance bugs in the List<T> implementation directly propagate to your subclass.
Use Composition Instead
One of the fundamental guidelines in object-oriented design is "favor composition over inheritance." By embedding a List<T> instead of inheriting it, you gain better control over the object's interface:
- You can restrict access to specific functions.
- Modifications and enhancements are abstracted in a more controlled manner.
- Independent type behavior can be maintained.
Example: Encapsulation Violation
In this example, the Stack<T> class violates its conceptual integrity since the inherited methods like Insert and RemoveAt can be directly accessed, undermining its "stack" behavior.
Alternative Approaches
- Use Composition: Define an internal
List<T>and expose only desired functionalities.
- Extend Existing Base Interfaces: Implement custom collections from scratch or by using base interfaces such as
IEnumerable<T>orIList<T>for more control over the collection behavior and structure.
Summary Table
| Aspect | List<T> Inheritance | Composition Approach |
| Encapsulation | Weak (exposes internals) | Strong (controls access) |
| API Design | Potentially confusing | Clear & defined by you |
| Refactoring Flexibility | Limited (type-locked) | High (independent of type) |
| Performance Optimization | Constrained by List<T> | Customizable |
| Control Over Methods | Limited (non-overrideable) | Full (defined by you) |
| Violation of Abstractions | High | Low |
Conclusion
Inheriting from List<T> often leads to complications and design issues that can be resolved through careful application of composition. By leveraging composition, developers ensure that applications remain maintainable, flexible, and aligned with object-oriented design principles. This practice not only extends behavior without unforeseen consequences but also accommodates performance optimizations, providing a more robust software architecture.

