
C#的语法脱胎于C因而保留了virtual关键字可以定义一个虚方法或虚属性。一个类的成员被定义为virtual就意味着它在告诉自己的子类我准备了一笔遗产你可以全盘接受也可以完全拒绝或者修改我的遗嘱。显然虚方法授予子类的权利甚至大于抽象方法。子类面对抽象方法只有重写override的权利而对于虚方法它还可以选择完全继承。毫无疑问虚方法破坏了对象的封装性。如果不加约束的使用会对调用方造成破坏至少它有可能破坏子类与父类之间在外在行为上的一致性。因此当我们在重写虚方法时务必要遵循Liskov替换原则。我们要保证对于调用方而言子类对于父类是完全可以替换的。这里所谓的“替换”是指子类不能破坏调用方对父类行为的期待。准确地说子类在重写父类的虚方法时必须遵循调用该方法的前置条件与后置条件。这也是“契约式设计”的思想。最理想的状态是让使用对象甚至无法知道是否存在派生类[1]。即类的继承体系对于调用者而言必须体现外部接口的一致性这样才能做到调用者对派生类无知。如果确实需要重写父类的方法最好的方式是扩展而不是修改。这实际上也是开放-封闭原则的体现。例如在Decorator模式中我们重写父类方法的目的是为了实现对该方法的装饰。Proxy模式的实现同样如此。Michael C. Feathers对此给出的忠告是[2]1尽可能避免重写具体方法。2倘若真的重写了某个具体方法那么看看能否在重写方法中调用被重写的那个方法。Feathers的忠告是针对Java语言因为在C#中我们无法重写具体方法只能利用new关键字在子类中新建一个相同方法签名的具体方法而这样的方法并不具备多态性。这里涉及到一个有趣的话题是关于Java和C#的比较。在Java语言中如果没有添加任何关键字则方法默认就是虚方法任何子类都可以重写它。C#则相反它对虚方法给予了显式的定义。Java语言的缔造者显然是“性本善”论者他认为所有子类的实现者均抱着善意的态度来对待父类的方法因而他赋予了子类相当程度的自由但却可能被别有用心者偷偷打开封装的后门。如果确有非常重要的隐私防止被篡改则可以利用final关键字来强制保护。C#语言的发明者则持有“性本恶”的论调他恶意地揣测子类总是会不怀好意所以提供了一套默认的强权来保护父类的隐私。如果需要对子类开放则明确地声明为virtual这就牢牢地把控制权攥紧在父类的手中。C#保守的做法使得语言的特质更加安全(当然Java会更加自由)我们可以使用virtual的自由性搭配方法的访问限制搭建一个安全合理的白盒框架。virtual关键字的含意本身就是面向子类的所以我们应该尽可能地将其放在protected方法中使用。如果该方法代表的行为确实需要公开给调用者我们可以定义一个公开的具体方法在其中调用一个受保护的虚方法。在Template Method模式中体现了C#这种划分具体方法和虚方法的好处。Template Method模式要求子类只能部分地替换父类的实现整个骨架则必须保持固定不变。在父类中我们将模板方法定义为具体方法将基本方法定义为抽象方法。模板方法规定了基本方法的调用顺序如果我们可以在子类中重写模板方法就可能破坏基本方法的调用顺序从而对整个策略造成影响。Strategy模式就不存在这个问题因为它的策略是整体的。Template Method模式在模板方法中规定的骨架实际上就是为调用者制订的前置条件和后置条件。有一种说法是不要在虚方法中访问私有字段[3]。这存在一定的合理性。因为一旦我们在父类的虚方法中访问了私有字段那么在子类重写该虚方法时由于无法获得父类的私有字段值就可能会导致该字段值的缺失。但这种说法并不完全准确。一方面我们认为Liskov替换原则主要是为了约束Is-A关系在行为上的一致性[4]如果该字段对行为不会造成影响则无大碍。另一方面这也说明我们在重写虚方法时最佳实践还是需要在重写的同时调用父类的虚方法如Decorator模式的实现方式。