我是谁?[C#]

我是谁?[C#]

 

Written by Allen Lee

 

0. xuzicn 提出了这样一个问题

有一个 interface ABC 包括了如下的方法 M():

public   interface  ABC
{
    
void  M();
}

另外有个类 Class1 继承了 ABC 并且拥有自己的方法 N():

public   class  Class1 : ABC
{
    
public  Class1() {}

    
public   void  M()
    
{
        Console.WriteLine(
" Now we are in Class1, using method which inherit from Interface ABC  " );
    }


    
public   void  N()
    
{
        Console.WriteLine(
" Now we are in Class1, we used method which not inherit from Interface ABC  " );
    }

}

程序中有如下语句:

//  Code #01

ABC t 
=   new  Class1();
t.M();

这种情况下编译可以通过而且会输出正确的结果。但是如果是下面的情况:

//  Code #02

ABC t 
=   new  Class1();
t.N();

编译就会出错,所以通过 ABC t = new Class1() 这句话我们得到的肯定不是 Class1 的一个实例。但是接口是不可以实例化的,那么 ABC t = new Class1() 究竟实例化了一个什么东西?

 

1. 我是谁?

很久以前,成龙上演了一部《我是谁》,现在 t 也遭遇了相同的问题。成龙当时就不太幸运了,因为要唤醒人的记忆不是那么容易;相比之下,t 就幸运的多了,因为它有元数据这张 ID 卡。那么,t 究竟是谁?

//  Code #03

ABC t 
=   new  Class1();
Console.WriteLine(t.GetType());

你猜答案是什么?是 Class1!

 

2. 选择性透过...

既然 t 指向 Class1 的实例,为什么 t.N() 不能通过编译呢?如果你玩过 Flash,你会知道 Flash 的 Mask 可以做出类似于聚光灯的效果,把聚光圈以外的东西统统隐藏起来。从目标对象的角度来看,接口利用这种“遮掩”的特性“屏蔽”了目标对象的部分功能;从客户端的角度来看,接口就像细胞膜那样有选择性的让目标对象的功能进入客户端的作用范围。

请看下图:

对应 Code #01 和 Code #02,t 发挥了接口的“选择性透过”特性,仅让客户端和 Class1 实例的 M() 接触。

 

3. 噢,别这样!

虽然 Code #02 中的 t 确实指向了 Class1 的实例,但编译器却不允许你调用 t.N(),因为 ABC 并不认识 Class1.N()。不过我们心知肚明 t 是有能力调用 N() 的,于是就去和编译器沟通一下。最后我们和编译器达成共识,让 t 这样调用 N():

//  Code #04

((Class1)t).N();

正当你为让 t 发挥了潜能而高兴时,你却听到另外一种声音:别这样!为什么?假如我们有这样一个方法:

//  Code #05

void  Process(ABC abc)
{
    ((Class1)abc).N();
}

某天不知道谁搞了一个 Class2:

//  Code #06

class  Class2 : ABC
{
    
public   void  M()
    
{
        Console.WriteLine(
" Now we are in Class1, using method which inherit from Interface ABC  " );
    }


    
public   void  O()
    
{
        Console.WriteLine(
" Ouch! Are you expecting me? " );
    }

}

然后这个人把 Class2 的实例传递给 Process():

//  Code #07

ABC t 
=   new  Class2();
Process(t);

那个人当然不知道我们在 Process 里面搞什么鬼,但却莫名其妙的得到一个 InvalidCastException!

好吧,如果你需要在 Process 里面处理 Class1 的实例,那你为什么不直接把你的想法告诉别人呢?

//  Code #08

Process(Class1 c)
{
    c.N();
}

同样的道理,如果我们希望调用 Class1.N(),为什么我们不直接做:

//  Code #09

Class1 c 
=   new  Class1();
c.N();

而要写 Code #02 那样的代码呢?

回顾 Code #05,我们除了知道 abc 肯定有一个 M() 之外,根本无从得知具体的实例还会有别的什么方法可用,那我们凭什么要求 Code #02 可以通过编译呢?

 

4. 我还应该说点什么呢?

好吧,我们绕了一个大圈,最后发现虽然 ABC t = new Class1() 无法通过编译,但 t 确实指向一个 Class1 实例,只是这个实例好像被 t 搞得有点“低能儿”罢了。我认为真正的问题并不在于 t 究竟是什么,而是在于我们为什么要这样使用接口。这是一个很基础的问题,除非你不打算在 OO 方面有所发展,否则你得把它弄清楚,要不然你真的会很麻烦,至少你会误解接口的真正意义。

posted @ 2005-11-24 14:13 Allen Lee 阅读(5159) 评论(42)  编辑 收藏 网摘 所属分类: C#

  回复  引用  查看    
#1楼 2005-11-24 14:52 | 寒带鱼      

  回复  引用    
#2楼 2005-11-24 14:59 | try [未注册用户]
非常好,每次你的文章都是那么的深入。。。。。佩服
想问下:为什么要使用接口
  回复  引用    
#3楼 2005-11-24 15:43 | xuzicn [未注册用户]
呵呵,真的是高手,上午提问下午就可以得到答案
  回复  引用    
#4楼 2005-11-24 16:05 | xuzicn [未注册用户]
吕震宇的BLOG上有一篇“不再只专注于技术”
http://zhenyulu.cnblogs.com/archive/2004/10/17/53443.aspx
不知道ALLEN怎么想,我才20刚满,计算机本科马上毕业,比较迷茫
  回复  引用  查看    
#5楼 [楼主]2005-11-24 18:58 | Allen Lee      

To xuzicn:

吕震宇在《痛苦的选择:不再只专注于技术》中主要提到两个问题:兴趣的导向和专注的范围。对于前者,我和李开复先生的观点是一致的:根据兴趣选择发展道路;对于后者,我想我的《“市场风险”和“竞争风险”》应该能够给你一点儿帮助。^-^


  回复  引用    
#6楼 2005-11-24 20:23 | asdfs [未注册用户]
哈哈..楼主的文章真的得深入理解啊.
  回复  引用  查看    
#7楼 2005-11-24 21:26 | flyye_cs      
把传引用分析得比较透彻.. ^O^
  回复  引用  查看    
#8楼 2005-11-24 23:35 | linkcd      
◎try
用interface是实现oo的关键

Allen Lee同学,我一直很喜欢你的风格:)
  回复  引用  查看    
#9楼 2005-11-25 00:09 | 天寒      
接口的作用没讲出来吧?
  回复  引用  查看    
#10楼 [楼主]2005-11-25 08:52 | Allen Lee      
To try:

“为什么要使用接口”,如果我们在实时交流,我一定反问你“为什么要使用 OO”。我相信对于这些你都有你自己的答案的,只不过是好奇我的答案,所以才提出来。

好吧,假设你现在要为某公司招聘一批程序员,你肯定不希望某个人走到你面前告诉你他哪里不舒服然后叫你给他开点药,你公司也肯定不希望将来工作的时候才发现招进来的是一个小提琴手或者一个自称是程序员的骗子。OK,这些都必须在面试的过程中处理!从这个角度来看,面试的过程就是一个筛选的过程,而你就是执行筛选的人。还缺什么?当然是一个“筛子”,一个用于把你们想要的程序员识别出来的“筛子”。联想到什么了吗?接口的“选择性透过”!那么,接口为什么会有这种特性?接口是如何进行筛选的?你可以把接口看作一个标准、一个约定、一个规范或者别的类似的你能想到的。作为一名面试官,你不可能一接到公司的任务就直接冲上阵去,你会事先思考一下,公司究竟需要什么样的程序员,符合什么条件的程序员才会被招近来工作,否则你就不是一名合格的面试官了,因为你不但有可能会使公司的工作陷入困境,而且有可能会埋没一个人才,因为他可能在别的地方会有更好的发展,而你却让他进来无所事事。

接着,假设你们公司打算让这批程序员到美国分部从事 ASP.NET 的开发,那么你的“筛子”可以这样表达:

interface IProgrammerStaff
{
    void SpeakEnglish();
    void DevelopASPNET();
}

然后,你把“筛子”用到面试中去:

Interviewer you = new Interviewer();
you.Interviews(somebody);

现在,假设有一个脸色苍白的人走到你面前,但他递给你的不是简历而是病历,那么面试的具体工作当然就不会被展开,因为编译器不允许你这样做!

从这里你可以看出,接口的使用带有很强的目的性,你可以把它看作你对事物的一种期望,如果你压根就不知道想要什么,那你还是不要使用接口了。

最后我还有一点要说说的,interface 是狭义的接口,广义上,接口泛指任何形式的标准、规范、约束或者期望。

好了,我想该说的我都说了,如果哪位认为需要补充的或者有不同看法的,欢迎提出来一起研究。
  回复  引用  查看    
#11楼 2005-11-25 08:55 | 生如夏花      
分析的透彻
  回复  引用  查看    
#12楼 2005-11-25 09:12 | 菩提树      
接口,是写给接口的使用者看到
就好像是,图纸是画给施工人员看的
  回复  引用  查看    
#13楼 [楼主]2005-11-25 09:14 | Allen Lee      
今天早上发现两件趣事:

1. 成了 linkcd 的同学;
2. 成了“.NET 新手 training 活动”团队的成员。

希望明天早上能发现更多的趣事,呵呵~~~
  回复  引用  查看    
#14楼 [楼主]2005-11-25 09:19 | Allen Lee      
To 菩提树:

这当然!但为什么我们要写接口给使用者看呢?正如图纸向施工人员表明了设计师的想法一样,接口也向使用者表明我们的意图。从这个角度来看,接口是一种存在于设计者和使用者之间的约定,是一种把设计者的期望传递给使用者的沟通方式。
  回复  引用  查看    
#15楼 2005-11-25 09:25 | FantasySoft      
To Allen,

我觉得接口最重要的还是强制性吧,当一个类实现一个接口的时候,就必须提供接口所定义方法的具体实现。至于选择性,在类的继承关系中,不一样有体现吗?
在你评论中给出的例子,Interviewer类的Interviews方法的signature是这样的吧: public void Interviews(IProgrammerStaff candidate)。然后你说的选择性是不是somebody实例所属的类必须实现IProgrammerStaff的接口呢?

BTW:linkcd也是我的同学,哈哈~~
  回复  引用  查看    
#16楼 2005-11-25 09:37 | 第一控制.NET      
@Allen
一直关注你design方面的文章,受益非浅。古语有云:能者为师。以后想跟你多交流,多学习。没意见吧?MSN:JPWAR@163.COM
  回复  引用  查看    
#17楼 2005-11-25 09:39 | 装配脑袋      
接口不仅可以掩盖共同实现者各自独有的方法,还可以掩盖共同实现者所不同的泛型类型参数。这一特点被我用在VBF中用作延迟执行(DelayInvoke)。

@FantasySoft
你终于出现了,怎么老不MSN上线呀……
  回复  引用  查看    
#18楼 [楼主]2005-11-25 09:43 | Allen Lee      
To FantasySoft:

你说的也是对的,对于目标对象来说,接口具有强制性。还是拿面试这个例子来说,某人想进入我们公司么?能说流利的英语么?能熟练运用 ASP.NET 么?如果可以,那么此人是幸运的;否则,就只好回去学习学习了。而这个学习的过程明显就是实现 IProgrammerStaff 接口。从这个角度来看,设计师透过接口强制目标对象达到自己的期望。我们强制应聘者具备某些条件,如果应聘者不具备并且不愿学习,那么我们惟有请他走人了。
  回复  引用    
#19楼 2005-11-25 09:46 | try [未注册用户]
谢谢大家的解答,进一步加深了我的理解
  回复  引用  查看    
#20楼 2005-11-25 10:37 | FantasySoft      
To Allen,

其实选择性不就是OO里面所强调的类型吗? 正如你正文里给出的例子,t是类ABC的实例,但是它的type却是ABC,这个在编译期就已经决定的了。类型确定了,也就是确定了这个实例的function table包含了哪些function,这里的包含实际上是指function的地址,而非function本身。至于function地址具体指向哪里则是在运行期决定的。 分清楚编译期和运行期所做的事情,对于理解OO中对象行为是很有帮助的。

To Ninputer,

原来我成通缉犯了,嘻嘻~~ 我有看到你上线啊,但是你都很忙,都不敢打扰你。我晚上都会上的,到时候见。 :)
  回复  引用  查看    
#21楼 2005-11-25 12:42 | edobnet      
接口就一个类的同性抽象,你需要的是ABC的功能,才调用设计这个ABC的接口,
使用时调用了一个Class1对ABC的实现,这个很正常
ABC t = new Class1();
但是,如果你要调用
((Class1)i).N();
只能说你的设计与调用不合理,或者你就直接
Class1 t = new Class1();
还要用接口吗?
接口是个很好的东西,定义要标准,就按标准来做,如果以前的标准不够强大,扩展一个
public interface ABC2:ABC
{
void N();
}
  回复  引用  查看    
#22楼 [楼主]2005-11-25 14:16 | Allen Lee      
To FantasySoft:

你说的是对的。例如下面语句:

you.Interviews(somebody);

在编译期,编译器将会帮助你检查 somebody 是否实现了 IProgrammerStaff,只有类型匹配才能通过编译。这就相当于面试官设立一个初级检查站,看看应聘者能否提供某些书面资料来说明他具备英语交流能力和 ASP.NET 开发能力。而在运行期,Interviewer.Interviews 会进一步检查 somebody 的“工作效果”,把合适的人才招近来。从这个角度来看,接口的“选择性透过”特性就是类型检查。
  回复  引用  查看    
#23楼 2005-11-25 15:44 | FantasySoft      
To Allen:

嗯,嗯~~ 那么换了另外的角度,这个选择性透过还有其他的意思吗?
  回复  引用  查看    
#24楼 2005-11-25 16:24 | 装配脑袋      
Allen Lee所说的招聘例子,我觉得是描述的泛型的抽象力而不是接口(面向对象)的抽象力。Interface不是通过那些成员“检查”哪些类型可以使用,而是一种让使用者不关心类型的具体实现就知道类型上面可以进行哪些操作,他知道这操作类型一定可以执行,但如何执行就是接口实现者的任务了。
那个招聘的例子,在.NET中恐怕只有VB9的Dynamic Interface可以胜任
  回复  引用  查看    
#25楼 [楼主]2005-11-25 21:08 | Allen Lee      

To 装配脑袋:

Oh,你说的是对的,接口可以用来隔离实现,而这又是我们通常使用接口的原因之一。我得承认面试那个例子并不是一个好例子,它未能让大家以更多的角度去了解接口。

现在我用《多样式星期名字转换》的“反向方案”来举例。在该文中,我的目的是进行星期名字的转换,我期望转换器可以这样使用:

string name = nameConverter.ToString(day);

我把这种期望制定成一种标准,并以接口的形式表达出来:

interface INameConverter
{
    string ToString(Day day);
}

客户端也通过 INameConverter 得知我的目的。由于我把这种期望上升为一种标准,于是它具备了一种强制性,犹如法律一般。而 NameConverter 的

public void AddInnerNameConverter(INameConverter innerNameConverter);

使得我可以通过编译器的编译期类型检查把这种强制性加在客户端所传递的对象上。仅当这一切都 OK 才会有下文。

“反向方案”的一个很重要的特征就是透过 INameConverter 隔离具体的转换实现,这点可以在 NameConverter 的

public string ToString(Day day)
{
    foreach(INameConverter nameConverter in m_NameConverters)
    {
        m_Content.AppendFormat("{0} ", nameConverter.ToString(day));
    }

    return m_Content.ToString().TrimEnd();
}

中得到体现。NameConverter 是一个“总管事”的角色,它不是负责具体的转换工作,而是负责把整个转换工作分配给具体的“员工”。NameConverter 没必要也不可能知道具体的转换细节,而 INameConverter 就发挥了隔离这些细节的功效了。

从这里我们可以看出,从顶层的 Conceptual Perspective 到中层的 Specification Perspective 再到底层的 Implementation Perspecitive,视角的转变让你看到接口的不同侧面。我觉得我们现在已不仅仅在简单的回答“为什么要使用接口”了,而是在试图回答“什么是接口”,这个问题对于我们就犹如“什么是人性”对于心理学家一样,不同的人从不同的角度会得出不同的看法,甚至一个理论体系。人性是复杂的,现代心理学已倾向于把各派的观点看作是人性的某一剖面。同样的道理,接口也会因为情况的不同和视角的不同而让我们得到不同的见解。

最后,在我看来,接口还有一个剖面我没有提到的——角色扮演。我们知道一个类可以实现多个接口:

class Consultant : IEmployee, IStockholder
{
    // ...
}

上面这个 Consultant 代表某咨询公司的咨询师既是该公司的员工,也是该公司的股东。


  回复  引用    
#26楼 2005-11-30 16:19 | xiao_p [未注册用户]
我认为真正的问题并不在于 t 究竟是什么,而是在于我们为什么要这样使用接口
_________________________

个人感觉这样写的意思就是代表某个对象实现了接口这个契约了吧,
记得过去看过好像是说接口是面向开发者的,而类则是面向对象的,呵呵
浅显的见解,希望大家不要耻笑…………
  回复  引用    
#27楼 2005-12-02 14:58 | thinking [未注册用户]
这不符合 倒置依赖 原则 要依赖于抽象(接口)
  回复  引用    
#28楼 2006-01-13 14:16 | hoodlum [未注册用户]
这个问题至于这样研究么???
ABC t = new Class1();
t.N();
编译就会出错,所以通过 ABC t = new Class1() 这句话我们得到的肯定不是 Class1 的一个实例。但是接口是不可以实例化的,那么 ABC t = new Class1() 究竟实例化了一个什么东西?

这里得描述有错误!这里实例化得当然是class1得实例,因为调用得是class1得构造函数。但是为什么不能用t.N()呢,这是因为编译器仅仅知道t是符合ABC接口的一个实例,但是它并不知道t到底是属于继承该接口的哪个类,也就是说它不知道t的身份,只知道它能干M这件事,至于它在接口以外还有什么别的本事一概不知。它不知道t是class1还是class2或者别的,所以你用t调用N肯定不行,除非你用Class1 t=new Class1();那么编译器就会明白的知道这个t是class1。
  回复  引用    
#29楼 2006-01-13 14:23 | hoodlum [未注册用户]
# re: 我是谁?[C#] 2005-11-30 16:19 xiao_p
我认为真正的问题并不在于 t 究竟是什么,而是在于我们为什么要这样使用接口
_________________________

个人感觉这样写的意思就是代表某个对象实现了接口这个契约了吧,
记得过去看过好像是说接口是面向开发者的,而类则是面向对象的,呵呵
浅显的见解,希望大家不要耻笑…………
~~~~~~~~~~~~~~~~~~~~~~~~~~~
这个说得是没错的。首先,可以看出类可以从接口继承,也就是说,接口里面的函数,属性是它子类的一个子集。每个从接口继承类可以包含更多的接口没有的个性。
接口是所有子类的一种共性。通常它规定了某一种对象必须具有的功能。你可能不知道它到底是汽车,还是火车,但是你却知道它是一种车,车能跑,能停,就行了。
  回复  引用    
#30楼 2006-01-13 14:28 | hoodlum [未注册用户]
5066还句话说,t在声明的时候是符合ABC规范的指针或者句柄,而不是符合class1。
  回复  引用    
#31楼 2006-02-08 01:20 | Barry.Cswords [未注册用户]
好文章,可以给同学们留下深刻的印象!
根据我的理解,这不仅仅是接口的特点吧,基类应该可以得到同样的效果。
有位仁兄问得好:接口到底有什么用。
斗胆借此谈一谈自己的看法:接口比基类更加形式化、更空泛。表现在下面两个方面——
很多API提供接口来实现不同环境间的数据、功能传递;接口使我们可以安全地实现多重继承。
个人看法,不要因为可以去掉就去掉接口的定义,也许会有我们一下看不到的好处。
  回复  引用  查看    
#32楼 2006-03-19 01:04 | 麒麟.NET      
接口的实现者与接口之间是can-do的关系,也就是说,Class1 can-do ABC的方法,但这并不代表ABC can-do Class1的方法
同样,子类与基类之间是is-a的关系,因此,SubClass is-a BaseClass,而BaseClass is-not-a SubClass

看了你的文章让我受益匪浅!:)
  回复  引用  查看    
#33楼 2006-03-19 01:11 | 麒麟.NET      
@hoodlum
你可能不知道它到底是汽车,还是火车,但是你却知道它是一种车,车能跑,能停,就行了。
——————————————————
如果强调能跑,能停,那么应该建立一个能跑能停的接口(不一定实现这个接口的一定是车,还可以包括动物、机器人等等。。。)
如果强调是一种车,还是建议用抽象类
  回复  引用    
#34楼 2006-03-23 22:10 | benyu [未注册用户]
to Allen Lee.
您好,未先经您同意,我已转载了您BLOG上的部份文章(当然有出处和原文引用).
如果您觉得不合适,我会删除!同时希望您能写更多的好文章!
谢!.
Benyu.
  回复  引用  查看    
#35楼 2006-05-06 16:30 | Justin      
嗯,看了这篇文章后有些想法,也写了一篇随笔:http://iheshi.cnblogs.com/archive/2006/05/03/390930.html
欢迎大家去看看,也欢迎大家提出批评或建议。谢谢。
  回复  引用  查看    
#36楼 2006-09-19 14:33 | 张新治      
在c++的概念里

不光是接口
子类的非重新方法,对象
在基础类的对象中都不能访问

从思想上说
子类是基础类功能的扩展
在不修改基础类私有功能和数据的前提下
  回复  引用    
#37楼 2006-10-17 22:55 | 阿东[匿名] [未注册用户]
兄台 我觉得你说的是对的 楼主的讲解很有意思 也有道理 但是并没有把接口给我们完全讲透彻 只能说讲解了接口的部分内涵@hoodlum

  回复  引用    
#38楼 2006-12-15 11:07 | 小新[匿名] [未注册用户]
// Code #04

((Class1)i).N();

根据上下文,这里的 i 应该是 t 吧...

  回复  引用  查看    
#39楼 [楼主]2006-12-15 19:43 | Allen Lee      
To 小新:

谢谢,原文以更正 ^_^
  回复  引用    
#40楼 2007-01-31 08:41 | net [未注册用户]
学习 精辟.
  回复  引用    
#41楼 2007-06-24 23:15 | 带头大哥 [未注册用户]
接口,其实定义类的扩展的那部分..
  回复  引用  查看    
#42楼 2007-12-07 09:12 | jillzhang      
有点意思




标题  
姓名  
主页
Email (博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2006-12-15 19:42 编辑过
Google站内搜索
[推荐职位]上海盛大网络招聘.Net开发工程师

China-pub 计算机图书网上专卖店!6.5万品种 2-8折!
近千种 9-95 新二手计算图书火热销售中!
开发者征途系统新作:《设计模式——基于C#的工程化实现及扩展》

相关文章:

相关链接: