为何不继承自List<T>?
为何不继承自List?
技术背景
在C#开发中,List<T>
是一个常用的泛型集合类,它提供了一系列操作列表的方法。然而,有一些指导原则建议不要直接继承自 List<T>
。了解为什么不建议这样做以及何时可以这样做,对于设计出高质量的代码至关重要。
实现步骤
不继承自 List<T>
的原因分析
- 语义不符:以足球队为例,从逻辑上讲,足球队不仅仅是球员的列表,它还包含球队名称、教练、管理层等信息。继承自
List<T>
会错误地将足球队建模为仅仅是球员的列表,违背了实际的业务逻辑。 - 违反里氏替换原则:里氏替换原则要求子类必须能够完全替代父类。足球队并不满足列表的所有特性,例如列表的顺序对球队状态通常没有描述性,球员的加入和移除也不是基于顺序位置等。因此,继承自
List<T>
会违反该原则。 - 封装性问题:继承自
List<T>
会暴露列表的所有方法,无法限制对球员列表的访问。例如,无法阻止外部代码直接调用Remove
、RemoveAll
或Clear
方法,从而破坏了球队对象的内部规则。 - 扩展性问题:如果需要添加其他列表属性,如预备队球员列表、退役球员列表等,继承自
List<T>
会使代码设计变得复杂,且限制了继承的灵活性。 - 序列化问题:继承自
List<T>
的类在使用XmlSerializer
进行序列化时,可能无法正确序列化其他属性。
替代方案
- 组合:使用组合的方式,将
List<T>
作为类的一个属性。例如:
1 |
|
- 实现接口:实现
IList<T>
、ICollection<T>
或IEnumerable<T>
等接口,以提供所需的功能,同时隐藏列表的具体实现。例如:
1 |
|
何时可以继承自 List<T>
当构建一个扩展 List<T>
机制的机制时,可以考虑继承自 List<T>
。例如,如果你需要一个具有 AddRange
方法的 IList<T>
接口,可以这样实现:
1 |
|
核心代码
组合方式实现足球队类
1 |
|
实现 IEnumerable<T>
接口的足球队类
1 |
|
最佳实践
- 设计优先:在决定是否继承自
List<T>
之前,先明确对象的数据和行为,以及未来可能的变化。考虑使用接口来定义对象的行为,而不是依赖于具体的类。 - 遵循里氏替换原则:确保子类能够完全替代父类,避免违反该原则的设计。
- 保持封装性:使用组合方式将
List<T>
作为私有属性,通过公共方法来控制对列表的访问,以保护对象的内部规则。 - 选择合适的接口:根据实际需求选择实现
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/