C#
inheritance
List<T>
object-oriented programming
design principles

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

csharp
1public class Stack<T> : List<T>
2{
3    public void Push(T item)
4    {
5        this.Add(item);
6    }
7
8    public T Pop()
9    {
10        if (this.Count == 0)
11        {
12            throw new InvalidOperationException("Stack is empty");
13        }
14        
15        T item = this[this.Count - 1];
16        this.RemoveAt(this.Count - 1);
17        return item;
18    }
19}

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

  1. Use Composition: Define an internal List<T> and expose only desired functionalities.
csharp
1   public class Stack<T>
2   {
3       private List<T> _list = new List<T>();
4
5       public void Push(T item)
6       {
7           _list.Add(item);
8       }
9
10       public T Pop()
11       {
12           if (_list.Count == 0)
13           {
14               throw new InvalidOperationException("Stack is empty");
15           }
16
17           T item = _list[^1]; // Using C# 8.0 index syntax
18           _list.RemoveAt(_list.Count - 1);
19           return item;
20       }
21   }
  1. Extend Existing Base Interfaces: Implement custom collections from scratch or by using base interfaces such as IEnumerable<T> or IList<T> for more control over the collection behavior and structure.

Summary Table

AspectList<T> InheritanceComposition Approach
EncapsulationWeak (exposes internals)Strong (controls access)
API DesignPotentially confusingClear & defined by you
Refactoring FlexibilityLimited (type-locked)High (independent of type)
Performance OptimizationConstrained by List<T>Customizable
Control Over MethodsLimited (non-overrideable)Full (defined by you)
Violation of AbstractionsHighLow

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.


Course illustration
Course illustration

All Rights Reserved.