为何不继承自List<T>?

为何不继承自List

技术背景

在C#开发中,List<T> 是一个常用的泛型集合类,它提供了一系列操作列表的方法。然而,有一些指导原则建议不要直接继承自 List<T>。了解为什么不建议这样做以及何时可以这样做,对于设计出高质量的代码至关重要。

实现步骤

不继承自 List<T> 的原因分析

  1. 语义不符:以足球队为例,从逻辑上讲,足球队不仅仅是球员的列表,它还包含球队名称、教练、管理层等信息。继承自 List<T> 会错误地将足球队建模为仅仅是球员的列表,违背了实际的业务逻辑。
  2. 违反里氏替换原则:里氏替换原则要求子类必须能够完全替代父类。足球队并不满足列表的所有特性,例如列表的顺序对球队状态通常没有描述性,球员的加入和移除也不是基于顺序位置等。因此,继承自 List<T> 会违反该原则。
  3. 封装性问题:继承自 List<T> 会暴露列表的所有方法,无法限制对球员列表的访问。例如,无法阻止外部代码直接调用 RemoveRemoveAllClear 方法,从而破坏了球队对象的内部规则。
  4. 扩展性问题:如果需要添加其他列表属性,如预备队球员列表、退役球员列表等,继承自 List<T> 会使代码设计变得复杂,且限制了继承的灵活性。
  5. 序列化问题:继承自 List<T> 的类在使用 XmlSerializer 进行序列化时,可能无法正确序列化其他属性。

替代方案

  1. 组合:使用组合的方式,将 List<T> 作为类的一个属性。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class FootballTeam
{
private readonly List<FootballPlayer> _roster = new List<FootballPlayer>(53);

public IList<FootballPlayer> Roster
{
get { return _roster; }
}

public int PlayerCount
{
get { return _roster.Count; }
}

// 其他需要暴露或封装的成员
}
  1. 实现接口:实现 IList<T>ICollection<T>IEnumerable<T> 等接口,以提供所需的功能,同时隐藏列表的具体实现。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Team : IEnumerable<Player>
{
private readonly List<Player> playerList;

public Team()
{
playerList = new List<Player>();
}

public IEnumerator<Player> GetEnumerator()
{
return playerList.GetEnumerator();
}

System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}

何时可以继承自 List<T>

当构建一个扩展 List<T> 机制的机制时,可以考虑继承自 List<T>。例如,如果你需要一个具有 AddRange 方法的 IList<T> 接口,可以这样实现:

1
2
3
4
5
6
public interface IMoreConvenientListInterface<T> : IList<T>
{
void AddRange(IEnumerable<T> collection);
}

public class MoreConvenientList<T> : List<T>, IMoreConvenientListInterface<T> { }

核心代码

组合方式实现足球队类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class FootballPlayer
{
public string Name { get; set; }
public int Number { get; set; }
}

public class FootballTeam
{
private readonly List<FootballPlayer> _players = new List<FootballPlayer>();

public IList<FootballPlayer> Players
{
get { return _players; }
}

public int PlayerCount
{
get { return _players.Count; }
}

public void AddPlayer(FootballPlayer player)
{
_players.Add(player);
}

public void RemovePlayer(FootballPlayer player)
{
_players.Remove(player);
}
}

实现 IEnumerable<T> 接口的足球队类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Team : IEnumerable<Player>
{
private readonly List<Player> playerList;

public Team()
{
playerList = new List<Player>();
}

public IEnumerator<Player> GetEnumerator()
{
return playerList.GetEnumerator();
}

System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}

class Player
{
public string Name { get; set; }
}

最佳实践

  1. 设计优先:在决定是否继承自 List<T> 之前,先明确对象的数据和行为,以及未来可能的变化。考虑使用接口来定义对象的行为,而不是依赖于具体的类。
  2. 遵循里氏替换原则:确保子类能够完全替代父类,避免违反该原则的设计。
  3. 保持封装性:使用组合方式将 List<T> 作为私有属性,通过公共方法来控制对列表的访问,以保护对象的内部规则。
  4. 选择合适的接口:根据实际需求选择实现 IEnumerable<T>IReadOnlyList<T>ICollection<T>IList<T> 等接口,以提供所需的功能,同时隐藏实现细节。

常见问题

继承自 List<T> 会导致代码冗余吗?

继承自 List<T> 可能会导致不必要的方法暴露,增加代码的复杂性。使用组合方式可以避免这个问题,通过封装只暴露必要的方法。

如何处理序列化问题?

如果需要序列化继承自 List<T> 的类,可以使用 DataContractSerializer 替代 XmlSerializer,或者实现自己的序列化逻辑。

可以在小型项目中忽略不继承自 List<T> 的建议吗?

在小型项目中,如果没有公共 API 的需求,且不会对代码的可维护性和扩展性造成太大影响,可以根据实际情况考虑是否继承自 List<T>。但建议仍然遵循良好的设计原则,以确保代码的质量。


为何不继承自List<T>?
https://119291.xyz/posts/why-not-inherit-from-list-t/
作者
ww
发布于
2025年5月27日
许可协议