Allen Lee's Magic

这里没有答案,顶多给你几个值得一试的猜想。

多态与 new [C#]

多态与 new [C#]

 

Written by Allen Lee

 

0. 浪子 《今天你多态了吗?》 提出两个这样的 问题

  • 1) “使用基类继承多态,有一点特别需要注意的就是:基类(抽象或者非抽象)中需要获得多态效果的成员必须有 abstractvirtual 修饰。”使用 new 来重写的成员不能形成多态吗?
  • 2) “多态就是使得你能够用一种统一的方式来处理一组各具个性却同属一族的不同个体的机制。” 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. 最后...

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

posted on 2006-03-13 09:40 Allen Lee 阅读(7303) 评论(20)  编辑 收藏 网摘 所属分类: C#

评论

#1楼 2006-03-13 10:59 浪子      

谢谢Allen Lee,我想我已经明白了。

关键在于“new:封印咒术”

new只是在子类封印了父类的方法(可能是不合理,所以才需要封印),但是父类的东西还是客观存在的。

我原来理解的错误在于

“使用 new 来重写的成员不能形成多态吗?”

我认为new也是重写,其实只是“封印”,而不是“重写”,两字之差相去万里啊。

我也明白了idior 的这个多态小quiz http://idior.cnblogs.com/archive/2005/03/04/113143.html
  回复  引用  查看    

#2楼 2006-03-15 10:49 riordan      

获益良多,非常感谢!   回复  引用  查看    

#3楼 2006-03-15 11:12 唐伯虎.秋香      

学习...   回复  引用  查看    

#4楼 2006-03-20 02:52 卡卡.net      

very good!   回复  引用  查看    

#5楼 2006-04-07 11:56 anchky      

收藏!   回复  引用  查看    

#6楼 2006-12-07 22:19 dataflow[匿名][未注册用户]

good,向达人致敬……   回复  引用    

#7楼 2007-12-08 16:26 Edison.Feng      

thx:)   回复  引用  查看    

#8楼 2007-12-10 09:59 冬虫草      

写的不错,收益良深   回复  引用  查看    

#9楼 2008-04-16 00:16 锦瑟无端五十弦      

喜欢看这样深入分析的好文   回复  引用  查看    

#10楼 2008-08-18 14:34 ttttt[未注册用户]

" new 则说明了他希望割断 Derived2.PrintStatus 和 Base.PrintStatus 之间的联系"说的隔断实际上就是隔断动态子类与父类的动态绑定??   回复  引用    

#11楼[楼主] 2008-08-18 20:56 Allen Lee      

@ttttt
这里使用new实际上是指不复用基类的虚方法“槽”。
  回复  引用  查看    

#12楼 2008-09-10 13:50 ZenosZeng[未注册用户]

过路的
谢谢讲解的这么透彻!
刚才把代码运行了一下

// Code #09

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

这段代码好像有奇异。
  回复  引用    

#13楼[楼主] 2008-09-10 23:42 Allen Lee      

@ZenosZeng
你觉得哪里不妥?
  回复  引用  查看    

#14楼 2008-09-21 23:04 独自旅行[未注册用户]

正好我今天复习OO思想,看了楼主的文章后,感觉思路顿时更加清晰了不少,但是我还是有个小问题,具体如下(我在CSDN上提问,暂时还没有收到比较满意的答案,汗):

如果有以下代码:


C# code
public class A
{
public virtual void Fun()
{
Console.WriteLine("A");
}

public void SomeOtherMethod()
{
Console.WriteLine("Some other method.");
}
}

public class B : A
{
public override void Fun()
{
Console.WriteLine("B");
}

public new void SomeOtherMethod()
{
Console.WriteLine("A new Some other method.");
}
}




这样调用:
A a;
B b = new B();

a = b;
a.Fun();
a.SomeOtherMethod();

如果这样的话,输出结果如下:
B
Some other method.

○ 第一个函数的输出结果大家都知道,肯定是要表现出多态性,因此实际上调用的是B.Fun()

○ 但是第二个函数的输出结果就有点神奇了,按理说貌似因该调用B.SomeOtherMethod(),但是实际上调用的是A.SomeOtherMethod(),据说这是“就近原则”,像这样的调用,系统会使用引用变量中申明的函数调用,而不是实例化对象中的函数调用。不过我就纳闷了,这个程序段中根本没有建立A类的实例,那A.SomeOtherMethod()这个函数是怎么运行出来的?
  回复  引用    

#15楼[楼主] 2008-09-30 19:50 Allen Lee      

@独自旅行
在给出我的解释之前,我想问你一个问题:B类里面是否只有一个SomeOtherMethod方法?

我们知道,B类继承自A类,很自然会把A类的SomeOtherMethod方法也继承过来,再加上B类自己的,于是B类里面就有两个SomeOtherMethod方法了。实质上,由于它们不是虚方法,它们没有共用同一个“槽”,而是名字相同的两个独立方法。说到这里,你很可能会问,如果这两个方法都存在的话,分别如何调用?秘密就在于变量的类型。当变量类型是A时,A的SomeOtherMethod方法就会激活;当变量类型是B时,B的SomeOtherMethod方法就会激活。

未知这个回答有没有使你稍稍满意?
  回复  引用  查看    

#16楼 2008-10-07 16:55 Enmiky[未注册用户]

好,我又明白了一点东西   回复  引用    

#17楼 2009-02-06 22:39 akwhole[未注册用户]

多谢楼主   回复  引用    




发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 348760




相关文章:

相关链接: