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:
- Enhancing List Functionality: Adding caching, logging, or specific algorithms.
- Internal Use with No API Exposure Risk: Controlled project scope without external dependencies.
- Performance-Critical with No Business Semantics: Pure data structure optimization without domain confusion.
// 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:
- Domain Semantics: Is the object a business entity or a generic tool? Prefer composition for business objects.
- API Stability: Will the code serve as a public API? If so, avoid inheriting from framework classes.
- Control Needs: Do you need to override collection behavior?
Collection<T>offers more hooks. - 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.