Why Not Inherit from List<T>: Choosing Between Composition and Inheritance in OOP

Nov 23, 2025 · Programming · 9 views · 7.8

Keywords: C# | Object-Oriented Design | Inheritance vs Composition | List<T> | Domain Modeling

Abstract: This article explores the design pitfalls of inheriting from List<T> in C#, covering performance impacts, API compatibility, and domain modeling. Using a football team case study, it distinguishes business objects from mechanisms and provides alternative implementations with composition, Collection<T>, and IList<T>, aiding developers in making informed design decisions.

Introduction

In object-oriented programming, inheritance is a key tool for code reuse, but misuse can lead to rigid designs and maintenance challenges. This article uses football team modeling to explain why inheriting from List<T> is often suboptimal and presents better alternatives.

Problem Context: Modeling a Football Team

Developers often intuitively view a football team as a list of players, leading to inheritance from List<FootballPlayer>:

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal; 
}

However, this design overlooks the team's essence: it is not just a player collection but includes business attributes like name and historical data. Modeling a team as a derived list blurs the line between mechanisms and business objects.

Why Inheriting from List<T> Is Discouraged

Performance and Optimization Issues

List<T> is highly optimized, using arrays internally for efficient add, remove, and access operations. Inheritance can undermine these optimizations, for instance:

// Example: Inheritance may introduce virtual method call overhead
class CustomList<T> : List<T>
{
    public override void Add(T item)
    {
        // Custom logic
        base.Add(item); // Virtual call could impact performance
    }
}

On large datasets with frequent operations, this overhead can accumulate into significant performance degradation.

API Stability and Lack of Control

As a .NET framework class, List<T>'s API is controlled by Microsoft. Inheriting from it risks incompatibility if framework updates change List<T> behavior. For example:

// Suppose List<T> adds a new method in the future
// Derived classes without overrides may exhibit unintended behavior
class FootballTeam : List<FootballPlayer> { }
// After an update, FootballTeam automatically gains the new method, potentially breaking existing logic

This is critical for public APIs (e.g., libraries or service interfaces), where changes affect all client code.

Misapplication in Domain Modeling

From a domain-driven design perspective, a football team is a business object, while List<T> is a generic mechanism. Inheritance implies "a team is a kind of list," which contradicts real-world understanding: a team is an organization, and the player list is a component. Proper modeling uses composition:

class FootballTeam
{
    public string TeamName { get; set; }
    public int RunningTotal { get; set; }
    public List<FootballPlayer> Players { get; } = new List<FootballPlayer>();
}

This accurately reflects the "team has a player list" relationship, avoiding semantic confusion.

Alternative Approaches and Implementations

Encapsulating List<T> with Composition

Exposing the list via properties allows controlled access and business logic integration:

class FootballTeam
{
    private List<FootballPlayer> _players = new List<FootballPlayer>();
    
    public string TeamName { get; set; }
    public int RunningTotal { get; set; }
    
    // Provide essential list operations
    public void AddPlayer(FootballPlayer player) => _players.Add(player);
    public int PlayerCount => _players.Count;
    public FootballPlayer this[int index] => _players[index];
}

Although it requires forwarding code, this enhances encapsulation and maintainability.

Inheriting from Collection<T> for Custom Collection Behavior

Collection<T> is designed for derivation, offering virtual methods for override:

class FootballTeam : Collection<FootballPlayer>
{
    public string TeamName { get; set; }
    public int RunningTotal { get; set; }
    
    protected override void InsertItem(int index, FootballPlayer item)
    {
        // Custom insertion logic, e.g., validate player eligibility
        base.InsertItem(index, item);
    }
    
    // Add methods like AddRange if needed
    public void AddRange(IEnumerable<FootballPlayer> players)
    {
        foreach (var player in players)
            Add(player);
    }
}

This approach balances flexibility and control, suitable for scenarios requiring customized collection behavior.

Implementing IList<T> for Maximum Control

For highly customized collections, implement IList<T> directly:

class FootballTeam : IList<FootballPlayer>
{
    private List<FootballPlayer> _players = new List<FootballPlayer>();
    
    public string TeamName { get; set; }
    public int RunningTotal { get; set; }
    
    // IList<T> member implementations
    public FootballPlayer this[int index] 
    { 
        get => _players[index]; 
        set => _players[index] = value; 
    }
    public int Count => _players.Count;
    public bool IsReadOnly => false;
    public void Add(FootballPlayer item) => _players.Add(item);
    public void Clear() => _players.Clear();
    // Other methods omitted...
}

While more verbose, this grants full control over collection behavior, ideal for complex business rules.

When Might Inheriting from List<T> Be Acceptable?

Inheriting from List<T> is not strictly forbidden but should be limited to mechanism extension scenarios:

// Example: Mechanism extension adding batch operation logging
class LoggedList<T> : List<T>
{
    public override void AddRange(IEnumerable<T> collection)
    {
        Console.WriteLine($"Adding {collection.Count()} items");
        base.AddRange(collection);
    }
}

Guidelines for Design Decisions

When choosing between inheritance and composition, consider:

  1. Domain Semantics: Is the object a business entity or a generic tool? Prefer composition for business objects.
  2. API Stability: Will the code serve as a public API? If so, avoid inheriting from framework classes.
  3. Control Needs: Do you need to override collection behavior? Collection<T> offers more hooks.
  4. Code Maintainability: Composition, though slightly verbose, facilitates testing and extension.

Conclusion

When modeling structures like football teams in C#, inheriting from List<T> often introduces design flaws. Alternatives such as composition, Collection<T>, or interface implementation enable more robust and semantically clear models. Developers should focus on business essence over mechanism reuse to improve code quality and maintainability.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.