为什么要封装一个类?

110

我想知道.Net框架中大量使用密封类的动机是什么。密封类的好处是什么?我无法理解不允许继承可能有什么用处,我也不是唯一反对这些类的人。

那么,为什么框架被设计成这样,解密所有类是否会破坏性变化?肯定有其他原因而不是只是出于恶意吧?


2
我会说,除非你知道客户会有支持问题,否则不要封闭一个类。 - cregox
10个回答

113

类应该设计为可继承或禁止继承。设计用于继承会有成本:

  • 它可能固定你的实现(你必须声明哪些方法将调用哪些其他方法,以防用户覆盖了其中一个但没有覆盖另一个)
  • 它会暴露你的实现而不仅是其效果
  • 在设计时需要考虑更多的可能性
  • 像 Equals 这样的东西在继承树中很难设计
  • 需要更多的文档
  • 如果子类化的不可变类型可能会变成可变类型(糟糕)

Effective Java 的第17条详细讨论了这个问题 - 尽管它是针对 Java 写的,但建议同样适用于 .NET。

个人认为在 .NET 中默认应该封闭类。


31
如果你继承一个类,如果出现问题难道不是你自己的责任吗? - mmiika
28
如果基类中一个你无法控制的实现更改导致了问题,那该怪谁?继承引入了脆弱性。在我看来,倾向于采用组合而非继承可以提高稳健性。 - Jon Skeet
7
是的,接口很好用 - 也可以偏爱组合的方式。但是如果我在没有仔细考虑的情况下公开了一个未封闭的基类,我应该预料到更改可能会破坏派生类。这对我来说感觉不太好。在我看来,最好封闭类以避免这种破坏。 - Jon Skeet
9
@Joan:组合关系是一种“有”(has-a)的关系,而不是“是”(is-a)的关系。因此,如果你想编写一个类,在某些方面可以像列表一样工作,但在其他方面不同,你可能希望创建一个带有List<T>成员变量的类,而不是从List<T>派生出来。然后,你可以使用该列表来实现各种方法。 - Jon Skeet
3
你在说哪家公司?不,一个基类必须做更多的事情。如果任何虚拟方法调用另一个虚拟方法,则需要记录并且不进行更改-否则子类可能会被此类更改破坏。当设计一个带有继承意图的类时,它限制了以后更改实现的自由。当然,继承有时候很有用。但是,在其他情况下,它可能会增加复杂性而没有好处,而组合则可以简化事情。这完全取决于上下文。 - Jon Skeet
显示剩余18条评论

42
  • 有时候,一些类非常珍贵,且不适合被继承。
  • 运行时/反射在查找类型时,可能会对密封类进行继承假设。一个很好的例子是-建议将属性设为sealed以提高查找运行时速度,type.GetCustomAttributes(typeof(MyAttribute)) 将显著加快速度,如果MyAttribute是密封的话。

该主题的MSDN文章为通过密封类限制可扩展性


3
很高兴看到他们现在明确表示“谨慎使用”…不过希望他们言出必行。 - mmiika
14
我觉得那听起来是错误的建议 :( - Jon Skeet
4
@CVertex:对不起,我并不是想批评你,只是这篇文章。 - Jon Skeet
17
@generalt:我相信在设计时要么考虑继承,要么禁止它。为继承而设计需要付出很多工作,并且通常会限制未来的实现。继承还会给调用者带来不确定性,因为他们无法精确知道自己将要调用什么内容。此外,它与不可变性(我喜欢的特性)不太搭配。我只发现类继承在相对较少的地方有用(而我喜欢接口)。 - Jon Skeet
1
@CVertex 如果你使用过.NET,你可能遇到了这个问题,但没有注意到,几乎所有的.NET核心类都是密封的。 - CoryG
显示剩余4条评论

16
似乎自从这个问题被提出约9年后,Microsoft的官方封装指南已经发生了变化,由选择性封装(默认情况下进行封装)转为选择性公开(默认不进行封装):

不要没有充分理由就对类进行封装。

仅仅因为你想不到扩展性场景而去封装一个类是不足以成为一个好理由的。框架用户喜欢从各种非显而易见的原因中继承类,例如添加方便成员。请参阅未封装类的示例,了解用户希望从类型继承的非显而易见的原因。

封装一个类的好理由包括以下几点:

  • 该类是静态类。请参阅静态类设计。
  • 该类在继承的受保护成员中存储安全敏感密钥。
  • 该类继承了许多虚函数,并且单独封装这些函数的成本会超过让该类保持未封装状态的好处。
  • 该类是需要非常快速的运行时查找的属性。封装的属性比未封装的属性具有稍高的性能水平。请参阅属性。

不要在封装类型上声明受保护或虚函数成员。

按定义,封装类型无法被继承。这意味着无法调用封装类型上的受保护成员,也无法覆盖封装类型上的虚函数。

✓ 考虑封装你重写的成员。引入虚拟成员可能会导致一些问题(如虚拟成员中所述),尽管影响稍微小了一点,但对重写同样适用。对重写进行封装可以从继承层次结构的那一点开始避免这些问题。

的确,如果你搜索ASP.Net Core代码库,你只会找到大约30个sealed class的出现,其中大部分是属性和测试类。

我认为保持不可变性是支持封装的好理由。


4

我在MSDN文档中发现了这句话:"Sealed类主要用于防止派生。因为它们永远不能用作基类,一些运行时优化可以使调用sealed类成员稍微快一些。"

我不知道性能是否是sealed类的唯一优点,个人也想知道其他原因...


4
很有意思,想看看他们所说的性能收益是什么样的… - mmiika
可能是在这个答案中提到的 https://dev59.com/onVC5IYBdhLWcg3whRcw#269130 - StingyJack

3

Java中String被定义为final的原因并非性能,而是安全性。 - CesarB
@CesarB:是的,但是,String不是普通的Java类。它是Java中唯一(我相信)支持运算符重载的类(有关更多信息,请参见此处,部分:“即使C和Java也有(硬编码的)运算符重载”),这在普通类中是不可能的。由于这个原因,即使它没有被标记为final,String类可能也无法被子类化。 - wchargin

1

Sealed用于防止“脆弱基类问题”。我在MSDN上找到了一篇好文章来解释这个问题。


链接的文章是403,考虑找到一个存档副本。 - StingyJack
找不到存档副本,但搜索“脆弱基类问题”会得到许多解释详细的结果。 - ihebiheb

0

将类封装起来可以更轻松地管理可释放资源。


0

密封允许您实现一些轻微的性能提升。在JIT和懒惰的pessimization世界中,这种情况不太真实,而在C++等领域中则更为真实,但由于.NET不像Java编译器那样擅长pessimization,主要是因为不同的设计哲学,因此仍然很有用。它告诉编译器它可以直接调用任何虚拟方法,而不是通过vtable间接调用它们。

当您想要一个“封闭的世界”用于诸如相等比较之类的事物时,这也很重要。通常,一旦我定义了一个虚拟方法,我就几乎无法定义真正实现该想法的相等比较概念。另一方面,我可能能够为具有虚拟方法的类的特定子类定义它。密封该类可确保相等性确实成立。


0
另一个需要考虑的因素是,密封类无法在单元测试中进行存根。来自Microsoft文档的说明如下:

由于存根类型依赖于虚方法分派,因此无法对密封类或静态方法进行存根。对于这种情况,请使用Shim类型,如在使用Shim类型隔离应用程序与其他程序集进行单元测试中所述。


0

为确定是否封装一个类、方法或属性,通常应考虑以下两点:

•派生类通过自定义您的类可能获得的潜在好处。

•派生类修改您的类的潜力,使其不能正确地工作或按预期工作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接