多态与 new [C#]
Written by Allen Lee
0.
浪子
在
《今天你多态了吗?》
提出两个这样的
问题
:
- 1) “使用基类继承多态,有一点特别需要注意的就是:基类(抽象或者非抽象)中需要获得多态效果的成员必须有 abstract 或 virtual 修饰。”使用 new 来重写的成员不能形成多态吗?
- 2) “多态就是使得你能够用一种统一的方式来处理一组各具个性却同属一族的不同个体的机制。” new 重写后的成员是否符合了这个范畴?
1. 你通常怎样用多态?
假设我有一个类,里面有一个 PrintStatus 方法,用于打印实例的当前状态,我希望该类的派生类都带有一个 PrintStatus 方法,并且这些方法都用于打印其实例的当前状态。那么我会这样表达我的愿望:
//
Code#01
class
Base
{
public
virtual
void
PrintStatus()
{
Console.WriteLine(
"
publicvirtualvoidPrintStatus()inBase
"
);
}
}
于是我可以写一个这样的方法:
//
Code#02
public
void
DisplayStatusOf(Base[]bs)
{
foreach
(Baseb
in
bs)
{
b.PrintStatus();
}
}
bs 中可能包含着不同的 Base 的派生类,但我们却可以忽略这些“个性”而使用一种统一的方式来处理某事。在 .NET 2.0 中,XmlReader 的 Create 有这样一个版本:
public
static
XmlReaderCreate(Streaminput);
你可以向 Create 传递任何可用的“流”,例如来自文件的“流”(FileStream)、来自内存的“流”(MemoryStream)或来自网络的“流”(NetworkStream)等。虽然每一中“流”的工作细节都不同,但我们却使用一种统一的方式来处理这些“流”。
2. 假如有人不遵守承诺...
DisplayStatusOf 隐含着这样一个假设:bs 中如果存在派生类的实例,那么该派生类应该重写 PrintStatus,当然必须加上 override 关键字:
//
Code#03
class
Derived1:Base
{
public
override
void
PrintStatus()
{
Console.WriteLine(
"
publicoverridevoidPrintStatus()inDerived1
"
);
}
}
你可以把这看作一种承诺、约定,直到有人沉不住气...
//
Code#04
class
Derived2:Base
{
public
new
void
PrintStatus()
{
Console.WriteLine(
"
publicnewvoidPrintStatus()inDerived2
"
);
}
}
假设我们有这样一个数组:
//
Code#05
Base[]bs
=
new
Base[]
{
new
Base(),
new
Derived1(),
new
Derived2()
}
;
把它传递给 DisplayStatusOf,则输出是:
//
Output#01
//
publicvirtualvoidPrintStatus()inBase
//
publicoverridevoidPrintStatus()inDerived1
//
publicvirtualvoidPrintStatus()inBase
从输出结果中很容易看出 Derived2 并没有按照我们期望的去做。但你无需惊讶,这是由于 Derived2 的设计者没有“遵守约定”的缘故。
3. new:封印咒术
new 似乎给人一种这样的感觉,它的使用者喜欢打破别人的约定,然而,如果使用恰当,new 可以弥补基类设计者的“短见”。在 Creating a Data Bound ListView Control 中,Rockford Lhotka 就示范了如何封印原来的 ListView.Columns,并使自行添加的返回 DataColumnHeaderCollection 的 Columns 取而代之。
从 Output #01 中我们可以看到,new 只是把 Base.PrintStatus 封印起来而不是消灭掉,你可以解除封印然后进行访问。对于 Derived2 的使用者,解封的方法是把 Derived2 的实例转换成 Base 类型:
//
Code#06
Based2
=
new
Derived2();
d2.PrintStatus();
//
Output#02
//
publicvirtualvoidPrintStatus()inBase
而在 Derived2 内部,你可以透过 base 来访问:
//
Code#07
base
.PrintStatus();
这种方法是针对实例成员的,如果被封印的成员是静态成员的话,就要透过类名来访问了。
4. 假如 Base.PrintStatus 是某个接口的隐式实现...
假如 Base 实现了一个 IFace 接口:
//
Code#08
interface
IFace
{
void
PrintStatus();
}
class
Base:IFace
{
public
virtual
void
PrintStatus()
{
Console.WriteLine(
"
publicvirtualvoidPrintStatus()inBase
"
);
}
}
我们只需要让 Derived2 重新实现 IFace:
//
Code#09
class
Derived2:Base,IFace
{
public
new
void
PrintStatus()
{
Console.WriteLine(
"
publicnewvoidPrintStatus()inDerived2
"
);
}
}
Derived1 保持不变。则把:
//
Code#10
IFace[]fs
=
new
IFace[]
{
new
Base(),
new
Derived1(),
new
Derived2(),
}
传递给:
//
Code#11
public
void
DisplayStatusOf(IFace[]fs)
{
foreach
(IFacef
in
fs)
{
f.PrintStatus();
}
}
的输出结果是:
//
Output#03
//
publicvirtualvoidPrintStatus()inBase
//
publicoverridevoidPrintStatus()inDerived1
//
publicnewvoidPrintStatus()inDerived2
从输出结果中,我们可以看到,虽然 Derived2.PrintStatus 应用了 new,但却依然参与动态绑定,这是由于 new 只能割断 Derived2.PrintStatus 和 Base.PrintStatus 的联系,而不能割断它与 IFace.PrintStatus 的联系。我在 Derived2 的定义中重新指定实现 IFace,这将使得编译器认为 Derived2.PrintStatus 是 IFace.PrintStatus 的隐式实现,于是,在动态绑定时 Derived2.PrintStatus 就被包括进来了。
5. 谁的问题?
我必须指出,如果 Base(Code #01)和 Derived2(Code #04)同时存在的话,它们俩其中一个存在着设计上的问题。为什么这样说呢?Base 的设计者在 PrintStatus 上应用 virtual 说明了他希望派生类能透过重写这一方法来参与动态绑定,即多态性;而 Derived2 的设计者在 PrintStatus 上应用 new 则说明了他希望割断 Derived2.PrintStatus 和 Base.PrintStatus 之间的联系,这将使得 Derived2.PrintStatus 无法参与到 Base 的设计者所期望的动态绑定中。如果在 Base.PrintStatus 上应用 virtual(即对多态性的期望)是合理的话,那么 Derived2.PrintStatus 应该换用另外一个名字了;如果在 Derived2.PrintStatus 上应用 new(即否决参与动态绑定)是合理的,那么 Base.PrintStatus 应该考虑是否去掉 virtual 了,否则就会出现一些奇怪的行为,例如 Output #01 的第三行输出。
假如继承体系中多态性行为的期望是合理的话,那么更实际的做法应该是把 Base 定义成这样:
//
Code#12
abstract
class
Base
{
public
abstract
void
PrintStatus();
}
而原来 Base 中的实现应该下移到一个派生类中:
//
Code#13
class
Derived3:Base
{
public
override
void
PrintStatus()
{
Console.WriteLine(
"
publicoverridevoidPrintStatus()inDerived3[originallyimplementedinBase]
"
);
}
}
这样,Derived2.PrintStatus 将使得编译无法完成,从而迫使其设计者要么更改方法的名字,要么换用 override 修饰。这种强制使得 Derived2 的设计者不得不重新考虑其设计的合理性。
假如继承体系中多态性行为的期望不总是合理呢?例如 Stream 有这样一个方法:
public
abstract
long
Seek(
long
offset,SeekOriginorigin);
现在假设我有一个方法在处理输入流时需要用到 Stream.Seek:
//
Code#14
public
void
Resume(Streaminput,
long
offset)
{
//
input.Seek(offset,SeekOrigin.Begin);
//
}
当我们向 Resume 传递一个 NetworkStream 的实例,Resume 将会抛出一个 NotSupportedException,因为 NetworkStream 不支持 Seek。那么这是否说明 Stream 的设计有问题呢?
设想 Resume 是一个下载工具进行断点续传的方法,然而,并不是所有的服务器都支持断点续传的,于是,你需要首先判断输入流是否支持 Seek 操作,再决定如何处理输入流:
//
Code#15
public
void
Resume(Streaminput,
long
offset)
{
if
(input.CanSeek)
{
//
input.Seek(offset,SeekOrigin.Begin);
//
}
else
{
//
}
}
如果 CanSeek 为 false,那就只好从头来过了。
实际上,我们并不能保证任何 Stream 的派生类都能够支持某个(些)操作,我们甚至不能保证来自同一个派生类的所有实例都支持某个(些)操作。你可以设想有这样一个 PriorityStream,它能够根据当前登录账号的权限来决定是否提供写操作,这使得拥有足够权限的人才能修改数据。或许 Stream 的设计者已经预料到这类情况的发生,所以 CanRead、CanSeek 和 CanWrite 就被加入到 Stream 里了。
值得注意的是,Code #07 的 Derived2 可能是一个很糟糕的设计,也可能是一个很实用的设计。在本文,它是一个很糟糕的设计,如果你足够细心,你会察觉到 Derived2 的设计者希望 Derived2.PrintStatus 绕过 Base.PrintStatus 而直接和 IFace.PrintStauts 进行关联,表面上这没什么不妥,但实质上 Base.PrintStatus 和 IFace.PrintStauts 在约定上是同质的,这意味着如果与 IFace.PrintStauts 进行关联就等于承认自己和 Base.PrintStatus 是同质的,这样的话,为什么不直接在 Derived2 里重写 PrintStatus 呢?在《基类与接口混合继承的声明问题》中,我示范了一个实用的设计,用 new 和接口重新实现(Interface reimplementation)来纠正非预期的多态行为。
6. 最后...
当我的朋友拿着问题来找我时,我通常都不会直接给出我的答案,而是尽我的能力向他提供足够多的可用信息,以便他能够根据他所面临的实际情况作出处理,毕竟,我不会比他更了解他的问题,而他也应该形成他自己的关于他的问题的思考。我希望浪子能用自己的答案回答他所提出的问题,因为只有这样,那些知识才真正属于他,并且我也相信本文已经提供了足够多的可用信息。
分享到:
相关推荐
对象编程语言最容易搞混这些概念:重载、多态、虚方法、抽象方法。还有这些关键字abstract/virtual/base/overrid/new应该在什么时候用。 本文档用最精简的文字描述了这些概念的区别,并有源码示例。刚看可能觉得写的...
8.2.3 方法的继承与virtual、override及new关键字 8.2.4 sealed关键字与密封类 8.2.5 Abstract关键字与抽象类 8.3 多态性 …… 第9章 泛型 第10章 Windows窗体应用程序开发 第11章 C#数据库编程与ADO.NET 第12章 ...
基类继承式多态如书中所说,基类继承式多态的关键是继承体系的设计与实现。书中举了个简单的列子 代码如下: Files myFile=new WORDFile(); myFile.open(); myFile是一个父类Files变量,保持了指向子类...
继承与多态 以下主题不会进行讨论: ? C++ 与 C# 的共同点 ? 诸如垃圾回收、线程、文件处理等概念 ? 数据类型转换 ? 异常处理 ? .NET 库 编程结构 和 C++ 一样,C# 是大小写敏感的。半角分号(;)是语句分隔...
虽然它们确实存在着差别,他们都是.net框架中最好的两种语言,他们都是一样的强大,visual basic.net是真正的面向对象的语言,包括了新建对象方法New和面向对象的特征—封装、多态、继承和重载。Vb.net和C#.net都使用...
C#多态1:重写父类方法2:多态实现3:里氏转换原则4:Object类 1:重写父类方法 1:重写父类方法 父类方法:virtual 子类方法:override 示例: static void Main(string[] args) { Dog a1 = new Dog (); a1....
(C#已经通过内存托管解决了这一令人头疼的问题)。C++通过new来分配内存,new的参数是一个表达式,该表达式返回需要分配的内存字节数,这是我以前掌握的关于new的知识,下面看看通过这本书,使我们能够更进一步的...
【图书目录】-C#与.NET技术平台实战演练PARTI基础语法篇第1章.NET概述1-1 软件开发结构的演进1-2 桌上型单机应用程序1-3 主从结构应用程序1-4 分布式应用程序结构1-4-1 WindowsDNA...
所属分类:计算机 > 软件与程序设计 > C# ================================================================ 内容简介 本书由浅入深,全面、系统地介绍了C#程序设计。除了详细地讲解C#知识点外,本书还提供了大量...
2011-2012第一学期C#期末复习资料 一、填空题 1. .NET框架包括 公共语言运行库 和 .NET类库。 2. 类的成员或者是静态成员,或者是实例...在派生类中定义与基类同名的方法,使用new修饰符显式隐藏从基类继承的方法成员。
C#中的各种名词: 常数:声明时用const修饰,是隐式静态类型 域:一个代表和某对象或类相关的变量的成员 字段:和属性相同,是用来存储对象的值,可以直接访问数据且不能对数据添加任何限制,但是属性不能且可以对...
【图书目录】-C#与.NET技术平台实战演练PARTI基础语法篇第1章.NET概述1-1 软件开发结构的演进1-2 桌上型单机应用程序1-3 主从结构应用程序1-4 分布式应用程序结构1-4-1 WindowsDNA...
C#不能够像C++一样在数据声明时调用构造函数,必须使用Myclass temp = new Myclass()来调用构造函数; C#不支持多重继承关系, 也就是说,一个派生类不允许有多个基类。简单点就是,父亲可能有好多儿子(父类可以...
(C#已经通过内存托管解决了这一令人头疼的问题)。C++通过new来分配内存,new的参数是一个表达式,该表达式返回需要分配的内存字节数,这是我以前掌握的关于new的知识,下面看看通过这本书,使我们能够更进一步的...
继承与多态..................................................................31 虚函数 ......................................................................31 使用“new”隐藏父类函数 ...................
本文实例讲述了C#虚函数用法。分享给大家供大家参考。具体如下: using System; namespace Test2 { class Plane { public double TopSpeed() { return 300.0D; } } class Jet : Plane { public double ...
C#中结构与类的区别 C#中 const 和 readonly 的区别 利用自定义属性,定义枚举值的详细文本 Web标准和ASP.NET - 第一部分 XHTML介绍 在ASP.NET页面中推荐使用覆写(Override)而不是事件处理(Event Handler) 常用...
Add 方法的参数必须与从 GetEnumerator 方法返回的 IEnumerator.Current 属性所返回的类型一致(多态)。除实现 IEnumerable 外还实现 ICollection 的类(如 CollectionBase)必须有一个取整数的公共 Item 索引属性...
学过java,c#,vb的都知道类的概念,而类具有继承、封装、多态等功能。而javascript它不是面向对象语言,它是解释性语言。 但我们同样可以使用javascript来实现继承、多态。 javascript实现类,有多种方法。 方法一:...
java精神(基于函数式组合子逻辑的javaparser框架) 一。 释名。 为什么叫精神? 如果你熟悉c++,那么你可能知道一个叫做”spirit”的parser库。...new A(new B(), new C())不也是从B和C组合成A了?”