漫谈C#编程中的多态与new关键字

2016-02-19 15:21 5 1 收藏

只要你有一台电脑或者手机,都能关注图老师为大家精心推荐的漫谈C#编程中的多态与new关键字,手机电脑控们准备好了吗?一起看过来吧!

【 tulaoshi.com - 编程语言 】

  1. 你通常怎样用多态?

  假设我有一个类,里面有一个 PrintStatus 方法,用于打印实例的当前状态,我希望该类的派生类都带有一个 PrintStatus 方法,并且这些方法都用于打印其实例的当前状态。那么我会这样表达我的愿望:

  

// Code #01class Base{ public virtual void PrintStatus() {  Console.WriteLine("public virtual void PrintStatus() in Base"); }}

  于是我可以写一个这样的方法:

  

// Code #02public void DisplayStatusOf(Base[] bs){ foreach (Base b in bs) {  b.PrintStatus(); }}

  bs 中可能包含着不同的 Base 的派生类,但我们却可以忽略这些个性而使用一种统一的方式来处理某事。在 .NET 2.0 中,XmlReader 的 Create 有这样一个版本:

(本文来源于图老师网站,更多请访问https://www.tulaoshi.com/bianchengyuyan/)

  

public static XmlReader Create(Stream input);

  你可以向 Create 传递任何可用的流,例如来自文件的流(FileStream)、来自内存的流(MemoryStream)或来自网络的流(NetworkStream)等。虽然每一中流的工作细节都不同,但我们却使用一种统一的方式来处理这些流。

  2. 假如有人不遵守承诺...

  DisplayStatusOf 隐含着这样一个假设:bs 中如果存在派生类的实例,那么该派生类应该重写 PrintStatus,当然必须加上 override 关键字:

  

// Code #03class Derived1 : Base{ public override void PrintStatus() {  Console.WriteLine("public override void PrintStatus() in Derived1"); }}

  你可以把这看作一种承诺、约定,直到有人沉不住气...

  

// Code #04class 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 #06Base d2 = new Derived2();d2.PrintStatus();// Output #02// public virtual void PrintStatus() in Base而在 Derived2 内部,你可以透过 base 来访问:// Code #07base.PrintStatus();

  这种方法是针对实例成员的,如果被封印的成员是静态成员的话,就要透过类名来访问了。

  4. 假如 Base.PrintStatus 是某个接口的隐式实现...

  假如 Base 实现了一个 IFace 接口:

  

// Code #08interface IFace{ void PrintStatus();}class Base : IFace{ public virtual void PrintStatus() {  Console.WriteLine("public virtual void PrintStatus() in Base"); }}

  我们只需要让 Derived2 重新实现 IFace:

  

// Code #09class Derived2 : Base, IFace{ public new void PrintStatus() {  Console.WriteLine("public new void PrintStatus() in Derived2"); }}

  Derived1 保持不变。则把:

  

// Code #10IFace[] fs = new IFace[]{ new Base(), new Derived1(), new Derived2(),}

  传递给:

  

// Code #11public 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 #12abstract 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:

(本文来源于图老师网站,更多请访问https://www.tulaoshi.com/bianchengyuyan/)

  

// Code #14public void Resume(Stream input, long offset){ // input.Seek(offset, SeekOrigin.Begin); //}

  当我们向 Resume 传递一个 NetworkStream 的实例,Resume 将会抛出一个 NotSupportedException,因为 NetworkStream 不支持 Seek。那么这是否说明 Stream 的设计有问题呢?

  设想 Resume 是一个下载工具进行断点续传的方法,然而,并不是所有的服务器都支持断点续传的,于是,你需要首先判断输入流是否支持 Seek 操作,再决定如何处理输入流:

  

// Code #15public 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. 最后...

  当我的朋友拿着问题来找我时,我通常都不会直接给出我的答案,而是尽我的能力向他提供足够多的可用信息,以便他能够根据他所面临的实际情况作出处理,毕竟,我不会比他更了解他的问题,而他也应该形成他自己的关于他的问题的思考。我希望浪子能用自己的答案回答他所提出的问题,因为只有这样,那些知识才真正属于他,并且我也相信本文已经提供了足够多的可用信息。

来源:https://www.tulaoshi.com/n/20160219/1609468.html

延伸阅读
mutable要害字 要害字mutable是C++中一个不常用的要害字,他只能用于类的非静态和非常量数据成员 我们知道一个对象的状态由该对象的非静态数据成员决定,所以随着数据成员的改变, 对像的状态也会随之发生变化! 假如一个类的成员函数被声明为const类型,表示该函数不会改变对象的状态,也就是 该函数不会修改类的非静态数据成员.但...
在c++中,extern关键字用来声明变量和函数,在声明函数的时候,有和没有extern的效果一样,即下面两条语句具有同样的效果: 代码如下: extern void fun(); void fun(); 但是对于变量,有和没有extern就有区别,当有extern时,只是告知编译器存在这个变量,编译器并不为该变量分配存储空间,即真正的声明;若没有extern,则在声明的同时...
static 1.在类中,用static修饰的属性,称为静态属性。为这个类的所有对象所共有,存放在静态存储区,所有该类的对象都可以访问且访问的都是同一变量。可以用作计数器,来统计总共创建了多少个各类的对象。 2.在类中,用static 修饰的方法为静态方法,在静态方法中不可以访问非静态的属性和方法,但在非静态方法中可以访问静态方法和属性;且...
像Java一样,C#提供了一整套相当丰富的类库、方法以及事件以供开发者使用。C#还引入了GDI+,它是由GDI演变而来的,具有比GDI更强大的功能而且简化了程序员的编程工作。所以开发者运用这些,就可以很方便的开发出具有强大图形图像功能的应用程序了。本文,笔者就通过一些实例像读者介绍一下C#中的图形编程的基本知识。 简单实例: ...
C99中新增加了restrict修饰的指针: 由restrict修饰的指针是最初唯一对指针所指向的对象进行存取的方法, 仅当第二个指针基于第一个时,才能对对象进行存取。 对对象的存取都限定于基于由restrict修饰的指针表达式中。 由restrict修饰的指针主要用于函数形参,或指向由malloc()分配的内存空间。 restrict数据类型不改变程序的语义。 编译器...

经验教程

710

收藏

66
微博分享 QQ分享 QQ空间 手机页面 收藏网站 回到头部