分享

条款29:只有当新版基类导致问题时才考虑使用new修饰符

 兰亭文艺 2018-01-17
我们一般在类成员上使用new修饰符,来重新定义继承自基类的非虚成员。我们可以这么做并不意味着我们就应该这么做。重新定义非虚方法会导致含混不清的行为。例如,对于下面的代码,绝大多数开发人员都会不假思索地认为它们的行为是一样的(假设两个类有继承关系):
object c = MakeObject( );
// 通过MyClass引用调用:
MyClass cl = c as MyClass;
cl.MagicMethod( );
// 通过MyOtherClass引用调用:
MyOtherClass cl2 = c as MyOtherClass;
cl2.MagicMethod( );
如果使用了new修饰符,情况就不是这样了:
public class MyClass
{
  public void MagicMethod( )
  {
    // 忽略细节。
  }
}
public class MyOtherClass : MyClass
{
  // 重新定义MagicMethod。
  public new void MagicMethod( )
  {
    // 忽略细节。
  }
}
这种做法会导致许多含混不清的地方。如果在同样的对象上调用同样的函数,我们期望同样的代码被执行。但事实是,如果我们更改了用来调用函数的引用,函数调用的行为也将有所不同。这种不一致的行为看上去很荒唐。一个MyOtherClass对象,由于对它的引用不同,而有不同的行为。修饰符new并不会将一个非虚方法变为一个虚方法。相反,它会在类中添加一个不同的方法。
非虚方法为静态绑定。任何引用MyClass.MagicMethod()的源代码调用的都将是该方法。系统不会在运行时寻找派生类中定义的其他版本。另一方面,虚函数使用的是动态绑定。系统会根据对象的运行时类型来选择调用正确的函数。
避免使用new修饰符来重定义非虚函数,并非意味着我们要将基类中所有的函数都定义为虚函数。当程序库的设计者将一个函数定义为虚函数时,实际上是为类型订立了一项合同:即表明任何派生类都可以更改虚函数的实现。事实上,虚函数集合定义了派生类中所有可能改变的行为。“默认为虚”的设计表明派生类可以更改类的所有行为。这意味着我们没有仔细思考派生类到底会更改哪些部分的行为。我们不应该这么做。相反,我们应该花费时间仔细考虑应该将哪些方法和属性声明为多态成员。我们应该仅将它们声明为虚成员。不要认为这种做法是对类的用户的限制。相反,应该将这种做法当作是在为定制类型行为提供一些入口点。
仅有一种情况我们需要使用new修饰符,那就是在我们使用新版的基类后,其增添的方法名和子类中现在已经被使用的方法名冲突。因为已经有代码在依赖子类中现有的方法名了,比如可能有其他程序集在使用这样的方法。例如,我们通过继承另外一个程序库中定义的BaseWidget,定义了新的MyWidget类:
public class MyWidget : BaseWidget
{
  public void DoWidgetThings( )
  {
    // 忽略细节。
  }
}
假设我们完成了MyWidget之后,已经有客户在使用它。然后我们发现BaseWidget公司又发布了一个新版的BaseWidget。由于对其中的新功能抱有热切的期待,我们立即购买了它,并试图生成新版的MyWidget。可是,生成的时候失败了,原因在于BaseWidget添加了自己的DoWidgetThings方法。
public class BaseWidget
{
  public void DoWidgetThings()
  {
    // 忽略细节。
  }
}
这是一个问题,我们的基类悄无声息地在其内引入了一个和子类同名的方法。有两种修正该问题的方法。首先,我们可以更改DoWidgetThings方法的名字:
public class MyWidget : BaseWidget
{
  public void DoMyWidgetThings( )
  {
    // 忽略细节。
  }
}
或者,我们可以使用new修饰符:
public class MyWidget : BaseWidget
{
  public new void DoWidgetThings( )
  {
    // 忽略细节。
  }
}
如果能访问到MyWidget类的所有客户程序代码,我们应该选择更改方法的名字,因为这在长期来讲比较方便。但是,如果我们的MyWidget类发行遍布全世界,那将迫使所有客户做繁多的更改。这就是 new修饰符的用武之地了。我们的客户可以继续使用DoWidgetThings()方法而无需做任何更改。他们也不会调用BaseWidget.DoWidget- Things(),因为这样的调用在客户代码中不可能存在。修饰符new正是应用于这样的场合:新版的基类增添的成员与子类中先前已经声明的成员发生了冲突。
当然,随着时间的推移,我们的用户可能也会试图去使用BaseWidget.DoWidget-Things()方法。这时候我们又回到了原来的问题上:两个方法看起来相同,但实际上不同。因此,我们应该考虑new修饰符所带来的长期不良影响。有时候,短期内更改方法名所导致的不方便可能仍然是值得的。
综上所述,使用new修饰符必须小心。如果不分青红皂白地使用,便会在对象上出现含混不清的方法调用。只有在“新版的基类增添的成员与子类中已存在的成员发生了冲突”这样特殊的情况下,我们才应考虑使用 new修饰符。即使在这种情况下,在使用它之前我们也要慎重考虑。除此之外,我们不应该再在任何其他情况下使用new修饰符。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多