注册会员,创建你的web开发资料库, 1. 你通常怎样用多态?
假设我有一个类,里面有一个 printstatus 方法,用于打印实例的当前状态,我希望该类的派生类都带有一个 printstatus 方法,并且这些方法都用于打印其实例的当前状态。那么我会这样表达我的愿望:
// code #01
class base
{
public virtual void printstatus()
{
console.writeline("public virtual void printstatus() in base");
}
}
于是我可以写一个这样的方法:
// code #02
public void displaystatusof(base[] bs)
{
foreach (base b in bs)
{
b.printstatus();
}
}
bs 中可能包含着不同的 base 的派生类,但我们却可以忽略这些“个性”而使用一种统一的方式来处理某事。在 .net 2.0 中,xmlreader 的 create 有这样一个版本:
public static xmlreader create(stream input);
你可以向 create 传递任何可用的“流”,例如来自文件的“流”(filestream)、来自内存的“流”(memorystream)或来自网络的“流”(networkstream)等。虽然每一中“流”的工作细节都不同,但我们却使用一种统一的方式来处理这些“流”。
2. 假如有人不遵守承诺...
displaystatusof 隐含着这样一个假设:bs 中如果存在派生类的实例,那么该派生类应该重写 printstatus,当然必须加上 override 关键字:
// code #03
class derived1 : base
{
public override void printstatus()
{
console.writeline("public override void printstatus() in derived1");
}
}
你可以把这看作一种承诺、约定,直到有人沉不住气...
// code #04
class derived2 : base
{
public new void printstatus()
{
console.writeline("public new void printstatus() in derived2");
}
}
假设我们有这样一个数组: // code #05
base[] bs = new base[]
{
new base(),
new derived1(),
new derived2()
};
把它传递给 displaystatusof,则输出是:
// output #01
// public virtual void printstatus() in base
// public override void printstatus() in derived1
// public virtual void printstatus() in base
从输出结果中很容易看出 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
base d2 = new derived2();
d2.printstatus();
// output #02
// public virtual void printstatus() in base
而在 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("public virtual void printstatus() in base");
}
}
我们只需要让 derived2 重新实现 iface:
// code #09
class derived2 : base, iface
{
public new void printstatus()
{
console.writeline("public new void printstatus() in derived2");
}
}
derived1 保持不变。则把:
// code #10
iface[] fs = new iface[]
{
new base(),
new derived1(),
new derived2(),
}
传递给:
// code #11
public void displaystatusof(iface[] fs)
{
foreach (iface f in fs)
{
f.printstatus();
}
}
输出结果是:
// output #03
// public virtual void printstatus() in base
// public override void printstatus() in derived1
// public new void printstatus() in derived2
从输出结果中,我们可以看到,虽然 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("public override void printstatus() in derived3 [originally implemented in base]");
}
}
这样,derived2.printstatus 将使得编译无法完成,从而迫使其设计者要么更改方法的名字,要么换用 override 修饰。这种强制使得 derived2 的设计者不得不重新考虑其设计的合理性。
假如继承体系中多态性行为的期望不总是合理呢?例如 stream 有这样一个方法:
public abstract long seek(long offset, seekorigin origin);
现在假设我有一个方法在处理输入流时需要用到 stream.seek:
// code #14
public void resume(stream input, long offset)
{
//
input.seek(offset, seekorigin.begin);
//
}
当我们向 resume 传递一个 networkstream 的实例,resume 将会抛出一个 notsupportedexception,因为 networkstream 不支持 seek。那么这是否说明 stream 的设计有问题呢?
设想 resume 是一个下载工具进行断点续传的方法,然而,并不是所有的服务器都支持断点续传的,于是,你需要首先判断输入流是否支持 seek 操作,再决定如何处理输入流:
// code #15
public void resume(stream input, 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. 最后...
当我的朋友拿着问题来找我时,我通常都不会直接给出我的答案,而是尽我的能力向他提供足够多的可用信息,以便他能够根据他所面临的实际情况作出处理,毕竟,我不会比他更了解他的问题,而他也应该形成他自己的关于他的问题的思考。我希望浪子能用自己的答案回答他所提出的问题,因为只有这样,那些知识才真正属于他,并且我也相信本文已经提供了足够多的可用信息。