Effective C# 原则29:仅在对基类进行强制更新时才使用new修饰符

JerryXia 发表于 , 阅读 (2,141)

你可以用new修饰符来重新定义一个从基类中继承来的非虚成员。你可以这样做,但并不意味着需要这样做。重新定义非虚方法会导致方法含意的混乱。如果两个相关的类是继承关系,那么很多开发人员可能会立即假设两段代码块是做完全相同的事情,而且他们也会这么认为:

object c = MakeObject( );

// Call through MyClass reference:
MyClass cl = c as MyClass;
cl.MagicMethod( );

// Call through MyOtherClass reference:
MyOtherClass cl2 = c as MyOtherClass;
cl2.MagicMethod( );

一但使用了new修饰符以后,问题就完全不一样了:

public class MyClass
{
  public void MagicMethod( )
  {
    // details elided.
  }
}

public class MyOtherClass : MyClass
{
  // Redefine MagicMethod for this class.
  public new void MagicMethod( )
  {
    // details elided
  }
}

这样的实际操作会让很多开发人员迷惑。因为当你在同一个对象上调用相同的函数时,一定希望它们执行同样的代码。但实际上是,一但你用不同的引用来调用同名的函数,它们的行为是不一样的,这感觉非常糟糕。它们是不一致的。一个MyOtherClass类型的对象所表现的行为会因为你引用的方式不一样而有所不同。这就是new修饰符用在非虚成员上的后果。其实这只是让你在类的名字空间中添加了一个不同的方法(虽然它们的函数名是相同的)。

非虚方法是静态绑定的,不管哪里的代码,也不管在哪里引用,MyClass.MagicMethod()
总是严格的调用类中所定义的函数。并不会在运行时在派生类中查找不同的版本。另一方面,虚函数动态的。运行时会根据不同的类型对象调用不同的版本。

建议大家避免使用new修饰符来重新定义非虚函数,这并不要太多的解释,就像推荐大家在定义一个基类时应该用虚方法一样。一个类库的设计者应该按照合某种约设计虚函数。也就表示你期望任何派生类都应该修改虚函数的实现。虚函数的集合就相当于是定义了一个行为的集合,这些行为是希望在派生中重新实现的。设计默认的虚函数就是说派生可以修改类中的所有虚的行为。这确实是说你不想考虑所有派生类可能要修改行为的分歧问题。相反,你可以把时间花在考虑把什么样的方法以及属性设计成多态的。当然,只有它们是虚行为的时候才能这样做。不要考虑这样会限制类的用户。相反,应该认为这是给类型的用户定义行为提供了一个入口向导。

有且只有一种情况要使用new修饰符,那就是把类集成到一个已经存在的基类上时,而这个基类中已经使用了存在的方法名,这时就要使用new了(译注:就是说基类与派生类都已经存在了,是后来添加的继承关系,结果在添加继承关系时,发现两个类中使用了同样的方法名,那么就可以在派生类中添加一个new来解决这个问题)。因为有些代码已经依懒于类的方法名,或者已经有其它程序集在使用这个方法。例如你在库中创建了下面的类,使用了在另一个库中定义的BaseWidget:

public class MyWidget : BaseWidget
{
  public void DoWidgetThings( )
  {
    // details elided.
  }
}

你完成了你的widget,
而且用户可以使用它。然而你却发现BaseWidget公司发布了一个新的版本。而这正是你所渴望的,于是你立即购买并编译你的MyWidget类。结果失败了,因为BaseWidget的家伙们已经添加了他们自己的DoWidgetThings
方法:

public class BaseWidget
{
  public void DoWidgetThings()
  {
    // details elided.
  }
}

这是个难题,你的基类中隐藏了一个方法,而这又是在你的类的名字空间中。有两个方法解决这个问题,一个就是修改你的类中的方法名:

public class MyWidget : BaseWidget
{
  public void DoMyWidgetThings( )
  {
    // details elided.
  }
}

或者使用new修饰符:

public class MyWidget : BaseWidget
{
  public new void DoWidgetThings( )
  {
    // details elided.
  }
}

如果你可以拿到所有使用MyWidget类的源代码,那么你应该选择修改方法名,因为这对于今后的运行会更简单。然而,如果你已经向全世界的人发布了MyWidget类,这会迫使所有用户来完成这个众多的改变。这正是new修饰符容易解决的问题,你的用户不用修改DoWidgetThings()方法而继续使用它。没有人会调用到BaseWidget.DoWidgetThings()方法,因为(对于派生类而言)它们根本不存在。在更新一个基类时,如果发现它与先前申明的成员发生了冲突,可以用new修饰符来解决这个问题。

当然,在某些时候,你的用户可能想调用基类的Widget.DoWidgetThings()方法,这时你又回到了原来的问题上:两个方法看上去是一样的,但其实是不同的。考虑到new修饰长期存在的歧意问题,有时候,还是在短期上麻烦一下,修改方法名为上策。(译注:长痛不如短痛。呵呵)

new修饰符必须小心谨慎的使用。如果它是有歧意的,你就在类上创建了个模糊的方法。这只有在特殊情况下才使用,那就是升级基类时与你的类产生冲突时。即使在这种情况下,也应该小心的使用它。最重要的是,其它任何时候都不要用它。

添加新评论