# 原则33:只有基类更新处理才使用 new 修饰符
**By D.S.Qiu**
**尊重他人的劳动,支持原创,转载请注明出处:[http://dsqiu.iteye.com](http://dsqiu.iteye.com)**
你可以使用 new 修饰符重定义从基类继承的非虚成员。仅仅是因为你可以做某些事情却不意味着你应该做。重定义非虚方法会有歧义的行为。大多数开发者看到下面两块代码会立即认为他们执行的是同一件事情,如果它们继承于同一个对象:
当涉及到 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 修饰符就构成新的版本。你获得的函数代码取决于你类的函数名字。可能有其他程序集使用你的方法。你在你的类库创建下面类,使用 BaseWidget 定义在其他类库中:
```
public class MyWidget : BaseWidget
{
public void NormalizeValues()
{
// details elided.
}
}
```
你完成你的组件,并且客户正使用它。而后你发现 BaseWidget 公司发布了新版本。渴望得到新的特性,你理解购买她并尝试构建 MyWidget 类。就会失败因为 BaseWidget 自己添加了 NormalizeValues 方法:
```
public class BaseWidget
{
public void Normalizevalues()
{
// details elided.
}
}
```
这就会有问题。你的基类偷偷在你的类名作用域下加了一个方法。有两种方式修复它。你可以改变你的类型 NormalizeValues 方法的名字。注意直观上认为 BaseWidget.NormalizeValues() 和 MyWidget.NormalizeValues() 在语意上的相同操作。如果不是,你不应该调用基类的实现。
```
public class MyWidget : BaseWidget
{
public void NormalizeAllValues()
{
// details elided.
// Call the base class only if (by luck)
// the new method does the same operation.
base.NormalizeValues();
}
}
```
或者,你可以使用 new 修饰符:
```
public class MyWidget : BaseWidget
{
public void new NormalizeValues()
{
// details elided.
// Call the base class only if (by luck)
// the new method does the same operation.
base.NormalizeValues();
}
}
```
如果你可以得到 MyWidget 类的所有客户端代码,你可以改变方法名字因为这很容易长期运行。但如果你是向世界发布 MyWidget 类,就会强制你的所有使用者做无数的更改。这就是 new 修饰符派上用场的地方。你的客户可以不更改继续使用 NormalizeValues() 方法。没有人会调用 BaseWidget.NormalizeValues() 因为它在之前不存在。 new 修饰符可以处理这样的情况,即升级基类使得有一个成员和之前声明的类有冲突的情况。
当然,随着时间的推移,你的使用者可能需要使用 BaseWidget.NormalizeValues() 方法。那样你还是回到原来的问题:两个方法看起来相同但是表现却不同。考虑所有 new 修饰符的长期后果。有时,短期更改你的方法是不方便却更好。
new 修饰符必须谨慎使用。如果你不分青红皂白,你就在创建歧义的方法。它只用于这个在升级基类引起和你的类冲突的特例。即使是那样的情况,在使用它之前也要仔细考虑。最重要的是,不要在其他情况使用它。
小结:
尽量不用 new 修饰符,减少歧义方法。
欢迎各种不爽,各种喷,写这个纯属个人爱好,秉持”分享“之德!
有关本书的其他章节翻译请[点击查看](/category/297763),转载请注明出处,尊重原创!
如果您对D.S.Qiu有任何建议或意见可以在文章后面评论,或者发邮件(gd.s.qiu@gmail.com)交流,您的鼓励和支持是我前进的动力,希望能有更多更好的分享。
转载请在**文首**注明出处:[http://dsqiu.iteye.com/blog/2087773](/blog/2087773)
更多精彩请关注D.S.Qiu的博客和微博(ID:静水逐风)
- 第一章 C# 语言习惯
- 原则1:使用 属性(Poperty)代替可直接访问的数据成员(Data Member)
- 原则2:偏爱 readonly 而不是 const
- 原则3:选择 is 或 as 而不是强制类型转换
- 原则4:使用条件特性(conditional attribute)代替 #if
- 原则5:总是提供 ToString()
- 原则6:理解几个不同相等概念的关系
- 原则7:明白 GetHashCode() 的陷阱
- 原则8:优先考虑查询语法(query syntax)而不是循环结构
- 原则9:在你的 API 中避免转换操作
- 原则10:使用默认参数减少函数的重载
- 原则11:理解小函数的魅力
- 第二章 .NET 资源管理
- 原则12:选择变量初始化语法(initializer)而不是赋值语句
- 原则13:使用恰当的方式对静态成员进行初始化
- 原则14:减少重复的初始化逻辑
- 原则15:使用 using 和 try/finally 清理资源
- 原则16:避免创建不需要的对象
- 原则17:实现标准的 Dispose 模式
- 原则17:实现标准的 Dispose 模式
- 原则18:值类型和引用类型的区别
- 原则19:确保0是值类型的一个有效状态
- 原则20:更倾向于使用不可变原子值类型
- 第三章 用 C# 表达设计
- 原则21:限制你的类型的可见性
- 原则22:选择定义并实现接口,而不是基类
- 原则23:理解接口方法和虚函数的区别
- 原则24:使用委托来表达回调
- 原则25:实现通知的事件模式
- 原则26:避免返回类的内部对象的引用
- 原则27:总是使你的类型可序列化
- 原则28:创建大粒度的网络服务 APIs
- 原则29:让接口支持协变和逆变
- 第四章 和框架一起工作
- 原则30:选择重载而不是事件处理器
- 原则31:用 IComparable<T> 和 IComparer<T> 实现排序关系
- 原则32:避免 ICloneable
- 原则33:只有基类更新处理才使用 new 修饰符
- 原则34:避免定义在基类的方法的重写
- 原则35:理解 PLINQ 并行算法的实现
- 原则36:理解 I/O 受限制(Bound)操作 PLINQ 的使用
- 原则37:构造并行算法的异常考量
- 第五章 杂项讨论
- 原则38:理解动态(Dynamic)的利与弊
- 原则39:使用动态对泛型类型参数的运行时类型的利用
- 原则40:使用动态接收匿名类型参数