关于c#:设计建议 – 何时有效地使用“虚拟”和“密封”

Design advice - When to use “virtual” and “sealed” effectively

我正在写一个C网络库(主要是作为一个学习练习,如果有人最终使用它,这对我来说并不太重要,因为我确信已经有解决方案了)。

到目前为止,我对自己的结构相当满意…我有几个可用的客户机/服务器层,它们可以通过套接字以原始字节进行通信,或者通过序列化消息对象进行稍微复杂一些的通信。

问题(问题?)我遇到的情况是,我应该在什么时候声明方法、属性或事件sealedvirtual或没有限定符。

我知道所有这些都做了什么——sealed防止类的继承,或者进一步重写方法。virtual将允许通过方法重写实现多态行为。

然而,由于我正在设计一个类库,我不确定何时使用这些类库。这是一个可扩展性问题,我想…我提供了一些接口、一个或两个抽象类,以及一些具体的实现,以供我的库的使用者使用或扩展,但是我很难决定何时明确禁止派生类或允许重写功能是一个"好主意"。

在设计我的类供其他人使用时,有什么通用的提示或建议要记住吗?

这个问题和这个问题有些帮助,就像这个问题一样,但是因为我正在编写一个可分发的库,所以我试图覆盖所有的基础。


从开发类库的Microsoft设计指南开始,特别是扩展性部分,在这里您可以找到有关虚拟成员和密封的文章。

引用,这里:

  • 不要使成员成为虚拟的,除非您有充分的理由这样做,并且您知道与设计、测试和维护虚拟成员相关的所有成本。
  • 对于虚拟成员,确实更喜欢受保护的可访问性而不是公共可访问性。公共成员应该通过调用受保护的虚拟成员来提供可扩展性(如果需要)。

  • 没有充分的理由不要密封类。

  • 不要在密封类型上声明受保护或虚拟成员。
  • 考虑密封您覆盖的成员。

不过,请阅读全文。


首先,我同意其他的答案,这些答案建议积极密封任何不是专门设计来扩展的内容。我不同意你需要一个密封的理由;相反,你需要一个不密封的理由。

您的问题非常一般,所以这里有一个一般性的答案:您的库大概是一组特性,可以用作解决用户问题的工具。您的库中的每个特性大概都在那里,因为您做了一些研究,发现有一个用户问题需要解决,并为他们解决了它。

这些特性中的每一个都有与之相关的特定成本。其中一些成本是过去的——您花在设计、实现、测试、调试和交付代码上的时间。其中一些费用还没有到来:维护、阅读错误报告等等。还有一些更微妙的成本,比如,也许保持与现有功能之一的向后兼容性会增加明天实现新功能的成本。

可扩展性也是一个特性。这是一个功能,如果你弄错了,成本几乎完全在未来。像对待任何其他特性一样对待它:弄清楚它是否是您的用户真正需要的特性,以及它的好处是否支付了它的成本。如果您不能清楚地评估扩展性的好处或成本,那么不小心地实现它是非常危险的。


在我看来,你不能真正预见到你的用户最终会用它做什么。因此,一般来说,最好不要密封任何东西,除非有一些你不想让用户胡闹的内部行为(例如,他们需要知道,在你这样做之前必须先设置这一点,等等)。

至于虚拟,您倾向于使最终用户可能想要覆盖的任何虚拟内容。事件极大地减少了对虚拟功能的需求,但有时您仍然希望使它们成为虚拟的。一般来说,您需要考虑到任何给定的成员函数可能需要最终用户进行定制。


我说过很多次"该死的!为什么这个班是密封的!但是我从来没有说过"天哪-我希望他们能封住那门课!"

总有一天,一个比你更优秀的程序员很有可能想要扩展你的课程,并且知道他们在做什么。我认为很少有好的理由来封印。


声明一个方法virtual意味着一个特定的约定:您的类接受它可以被重写并对此进行预期。

通常有一个明确的理由使某物虚拟化。有疑问时:不要。

而密封恰恰相反,你可以说你不想它被覆盖了。第二个原因可能是这里的性能,但我不会用得太快。


有争议的意见:C类应该默认密封。

如果类不是被设计为要继承的,并且您没有考虑到潜在的缺陷或记录如何正确继承,那么如果人们重写方法,您的类很可能会以奇怪的方式失败。当你不密封你的类时,你告诉你的类的客户从它派生是可以的,并且你将来必须支持你的类的这个"接口",以避免破坏那些选择从你的类继承的客户。这就限制了以后如何修改类。

所以默认情况下密封类,除非您明确希望允许这样做。如果您确实想允许它,请确保您通过创建虚拟的方法来记录哪些方法应该被重写,以及被重写的方法必须做些什么来确保类继续工作(例如调用基方法)。


通常,通过继承来设计类的可扩展性并不容易。当你密封一些东西时,你会说这个类确实不适合实现继承(例如,你可能在用不安全的代码做一些低级的事情)。

为了获得最大的可组合性(在数学意义上),我建议密封所有非抽象类的内容。实际上,这就是如何在语言中实现代数数据类型(在本例中是sum类型)的方法(请参阅http://blog.lab49.com/archives/3011了解一篇振奋人心的文章)。


你这里有利益冲突。

  • 如果您希望让调用者更容易测试他们的代码(例如,进行测试驱动的开发),那么您的用户需要能够模拟您的类。
    • 因此,所有方法ECT必须是虚拟的,或者您必须具有包含所有方法的实现接口。
  • 但是,如果您希望明确说明您在用户可能创建的任何子类上提供的合同,那么您应该遵守Microsoft指南。
    • 除非您有充分的理由这样做,并且您知道与设计、测试和维护虚拟成员相关的所有成本,否则不要使成员成为虚拟成员。遗憾的是,没有一种方法可以标记一个虚拟方法来表示除了模拟之外,您不希望任何客户机代码实现它。