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

编程|关键字

  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. 最后...

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

时间: 2024-11-03 21:55:17

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

《JavaScript应用程序设计》一一2.12 范型编程与集合多态

2.12 范型编程与集合多态 范型编程是指在多种数据类型上提供可复用的一套算法与数据结构,这种语言机制的产生缘由在于算法通常能适配多种数据类型.范型编程一般从几种特定的数据类型实现开始入手,随后逐步将之抽象成为能够兼容更多数据类型的通用版本. 范型编程并没有对不同类型的数据做算法差异性处理,相反,被传入的每种数据类型必须实现算法中所约定的功能,这些功能被称为"接口需求". 范型编程具有参数多态性,它是作用于范类型参数上的逻辑分支,相比之下,重载则需要对所有不同类型的参数分别创建一套处理

详解Java设计模式编程中的策略模式_java

定义:定义一组算法,将每个算法都封装起来,并且使他们之间可以互换.类型:行为类模式类图: 策略模式是对算法的封装,把一系列的算法分别封装到对应的类中,并且这些类实现相同的接口,相互之间可以替换.在前面说过的行为类模式中,有一种模式也是关注对算法的封装--模版方法模式,对照类图可以看到,策略模式与模版方法模式的区别仅仅是多了一个单独的封装类Context,它与模版方法模式的区别在于:在模版方法模式中,调用算法的主体在抽象的父类中,而在策略模式中,调用算法的主体则是封装到了封装类Context中,抽

举例解析Java的设计模式编程中里氏替换原则的意义_java

里氏替换原则,OCP作为OO的高层原则,主张使用"抽象(Abstraction)"和"多态(Polymorphism)"将设计中的静态结构改为动态结构,维持设计的封闭性."抽象"是语言提供的功能."多态"由继承语义实现. 里氏替换原则包含以下4层含义: 子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法. 子类中可以增加自己特有的方法. 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更

C++中的多态与虚函数的内部实现方法_C 语言

1.什么是多态 多态性可以简单概括为"一个接口,多种行为". 也就是说,向不同的对象发送同一个消息, 不同的对象在接收时会产生不同的行为(即方法).也就是说,每个对象可以用自己的方式去响应共同的消息.所谓消息,就是调用函数,不同的行为就是指不同的实现,即执行不同的函数.这是一种泛型技术,即用相同的代码实现不同的动作.这体现了面向对象编程的优越性. 多态分为两种: (1)编译时多态:主要通过函数的重载和模板来实现. (2)运行时多态:主要通过虚函数来实现. 2.几个相关概念 (1)覆盖.

java代码-关于CSDN英雄会挑战赛 java编程中的一些问题

问题描述 关于CSDN英雄会挑战赛 java编程中的一些问题 各位大神,我在挑战英雄会的题目的时候,在线下运行的没有问题,可是一提交就会提示,"挑战失败:你的程序正常编译,不过运行时发生错误,通常是代码有问题,如除数为零数组上下界溢出等" 我觉得可能是java输入数据时的问题,因为题目要求:输入多行数据,输出多行我是这样写的: Scanner scanner = new Scanner(System.in); ArrayList list = new ArrayList(); do {

C++UDP组播编程中,组播地址(组播号)与组播地址的关系是什么?和TCP中IP和端口的关系一样吗?

问题描述 C++UDP组播编程中,组播地址(组播号)与组播地址的关系是什么?和TCP中IP和端口的关系一样吗? C++UDP组播编程中,组播地址(组播号)与组播地址的关系是什么?和TCP中IP和端口的关系一样吗?另外,一台计算机上(不经过路由器或交换机)只能有一个组播吗?急求!!! 解决方案 组播地址和多播地支的关系与TCP中IP和端口的关系不太一样. 首先,网卡查看由信道传送过来的帧,确定是否接收该帧,若接收后就将它传往设备驱动程序.通常网卡仅接收那些目的地址为网卡物理地址或广播地址的帧. 使

如何在wavecom编程中通过AT指令判断被呼方的第一声回铃音

问题描述 如何在wavecom编程中通过AT指令判断被呼方的第一声回铃音 如何在wavecom编程中通过AT指令判断被呼方的第一声回铃音 解决方案 wavecom 的 AT 返回 RING,即可认为是被叫方的回铃声

深入理解JavaScript编程中的同步与异步机制

  这篇文章主要介绍了深入理解JavaScript编程中的同步与异步机制,不仅仅是AJAX已经深入到了各个角落,Node.js的火爆也让JS的异步编程格外引人注目,需要的朋友可以参考下 JavaScript的优势之一是其如何处理异步代码.异步代码会被放入一个事件队列,等到所有其他代码执行后才进行,而不会阻塞线程.然而,对于初学者来说,书写异步代码可能会比较困难.而在这篇文章里,我将会消除你可能会有的任何困惑. 理解异步代码 JavaScript最基础的异步函数是setTimeout和setInt

Java和jsp编程中应该注意的几个常见问题

1. 对应String类型的对象使用println()方法时,如果对象为null,将打印null而不是引发NullPointerException,由此引用的问题是容易造成错觉,对于以后对字符串的操作容易引起问题. 2. 引发NullPointerException异常,主要原因是没有对对象的存在性进行验证,在jsp编程中经常出现:if(request.getParameter("username").equals("xxx")).out.println(sessi