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-10-31 04:20:21

C# 多态与new关键字的相关文章

java学习中,面向对象的三大特性:封装、继承、多态 以及 super关键字和方法的重写(java 学习中的小记录)

java学习中,面向对象的三大特性:封装.继承.多态 以及 super关键字和方法的重写(java 学习中的小记录) 作者:王可利(Star·星星) 封装     权限修饰符:public 公共的,private 私有的     封装的步骤:          1.使用private 修饰需要封装的成员变量.          2.提供一个公开的方法设置或者访问私有的属性              设置 通过set方法,命名格式:     set属性名();  属性的首字母要大写 访问 通过ge

多态、抽象类、魔术方法

多态 接口的方法实现可以有很多.多以对于接口里面定义的方法的具体实现是多种多样的,这种特性我们称为多态 接口A两个实现B和C,B和C对A里面定义的方式实现可以是不同的,这种现象就是多态 相同的一行代码对于传入不同的接口的实现的对象的时候,表现是不同的.这就是多态 抽象类 abstract 关键字用于定义抽象类 抽象类里面 1.可以定义方法体 2.可以定义普通方法,有方法的具体实现 继承抽象类的子类需要实现抽象类中定义的抽象方法 魔术方法 __toString() 当对象被当做string使用的时

深入浅出OOP(三): 多态和继承(动态绑定/运行时多态)

在前面的文章中,我们介绍了编译期多态.params关键字.实例化.base关键字等.本节我们来关注另外一种多态:运行时多态, 运行时多态也叫迟绑定. 运行时多态或迟绑定.动态绑定 在C#语音中,运行时多态也叫方法重写(overriding),我们可以在子类中overriding基类的同签名函数,使用"virtual & override"关键字即可. C#的New.Override关键字 创建一个console 示例工程,命名为InheritanceAndPolymorphis

面向对象理解,封装、继承、多态知识总结

面向对象 类就是对象在程序中的模拟实现,类决定了对象将要拥有的属性和行为 类是一种数据类型,用户自定义的数据类型 类的组成:字段.属性.方法.构造函数等 对象时具体的,是类的具体实例.对象具有属性(特征)和方法(行为) 一.面向对象的三大特征: 封装 类和对象本身就是封装的体现 (1)属性封装了字段 (2)方法的多个参数封装成了一个对象 (3)将一堆代码封装到了一个方法中 (4)将一些功能封装到了几个类中 (5)将一些具有相同功能的代码封装到了一个程序集中(dll.exe),并且对外提供统一的访

java中面向对象的一些知识(二)

一. 封装的讲解 什么是封装?为什么要封装?怎么实现封装? 封装的目的是为了提高程序的安全性.封装就是把不想让第三者看的属性,方法隐藏起来. 封装的实现方法是: 1.修改属性的可见性,限制访问. 2.设置属性的读取方法. 3.在读取属性的方法中,添加堆属性读取的限制. package com.chen.test; public class Test9 { private int age; // 定义年龄 private String name; // 定义姓名 private double pr

【C#】第3章学习要点(二)自定义类和结构

分类:C#.VS2015 创建日期:2016-06-19 使用教材:(十二五国家级规划教材)<C#程序设计及应用教程>(第3版) 一.要点概述 别人提供的类都是为了简化你的工作量用的,可是实际处理的事情千差万别,要通过写代码做实际的事,你还得自己定义类.因此,先把如何自定义类及其涉及的相关概念和要点掌握住,是写程序绕不过去的第一关. 二.类和成员的基本概念 1.基本格式 [访问修饰符] [static] class 类名 [: 基类 [,接口序列]] { [类成员] } 要点: (1)基类最多

修罗场第二天:C#之面向对象基础(下)

------------接(上)http://www.cnblogs.com/HoloSherry/p/7100795.html 抽象类 抽象类也可以实现多态,使用关键字abstract.那么什么时候用抽象类呢?     当父类的方法不知道怎样去实现的时候,此时可以将父类写成抽象类,将方法写成抽象方法. 以下是抽象类的特点: 抽象成员必须标记为abstract,而且不能有方法体: 抽象成员必须在抽象类中: 抽象类中可以有抽象成员,也可以有非抽象成员,非抽象成员可以给子类使用: 抽象类不能被实例化

抽象类 抽象方法 接口 类部类 匿名类部类 设计模式之单例模式(懒汉模式及饿汉模式)

---恢复内容开始--- 抽象类  关键字  abstract 不能被实例化(创建对象),可通过类名调用静态方法 子类继承抽象类必须重写父类的所有抽象方法,然后用多态调用 接口:关键字 interface   类名 implements 接口名 1.接口中只能有抽象方法,并且不能被实例化,通过多态调用 2.接口与接口之间的关系: 继承关系(可以多继承); 类部类: 在类中定义的类 创建类部类对象    外部类名.内部类名  对象名 = new 外部类名().new内部类名() 匿名类部类: 在写

对象1

对象 this static 权限 继承 重写 继承中的构造方法 转型 多态 抽象类 final关键字 接口 原文地址:https://www.cnblogs.com/xiaodangshan/p/9557452.html