面向对象主要概念

【回目录】

说实话,面向对象是个很大很广的概念,我可不敢在园子里瞎白活,以免“误入牛群深处,引来砖头无数”。但是作为面试常考的题,又不得不列举出来,在此,我主要是和大家一起回顾一下面向对象中的几个核心概念,温故罢了,绝无它意。

说到面向对象这个概念很大很广,其实我觉得也不必被这个“很大很广”吓着了,学习面向对象思想,切不可将其专门孤立为一门学科来学习,其实面向对象就在我们日常生活当中,随时随处都能见到,而不仅仅是软件开发。软件开发不过是把我们人类看待世界的思维方式总结抽象出来用到程序设计上,起了个名字叫“面向对象”,可千万别被专业术语给镇住了,IT人士有这毛病,喜欢故弄玄虚,其实例子太常见了,你家马桶就是面向对象的。

面向对象有三个要素:封装(Encapsulation)、继承(Inheritance)和多态(Polymorphism)。把英文也写出来不是卖弄而是给大家伙儿提个醒,很多时候不错的公司面试往往会用英文,可千万别在词汇量上栽跟头,太不划算了。好,废话少说,接下来我们就来挨个儿探讨以上三个概念吧。

什么是封装?面向对象思想有个忌讳,那就是把对象自己的属性和内部实现细节暴露给别的对象,虽是代码但也应该享有隐私权。把不想或者不该告诉别人的东西封起来,把可以告诉别人的公开,这就是封装的基本概念。那什么是该封装起来什么是该公开的呢?很简单,我们来看看下面这段代码:

   1: public class Person
   2: {
   3:     public string Name { get; private set; }
   4:     protected int age = 0;
   5:  
   6:     internal void SetName(string name)
   7:     {
   8:         if (String.IsNullOrEmpty(name))
   9:         {
  10:             return;
  11:         }
  12:  
  13:         this.Name = name;
  14:     }
  15: }

我们声明了一个Person类,里面有一个Name属性。注意,Name属性的访问器(get)是公开的,但设置器(set)是私有的,试想一个人如果走在大街上谁见了他都可以给他取个名字,那岂不乱了套了。但人又不能没有名字,所以有一个SetName的方法,前面说了名字不是谁都能取的,因此我们给这个方法加了一个internal访问修饰符,也就是说当前程序集里可以调用,我们把“当前程序集”设想成这个人所在的家庭,一个小孩子生下来,可能是爷爷奶奶给取名,也可能是爸爸妈妈取名。另外,还有个protected的age属性,女孩子的年龄往往是比较保密的,一个女的听到人家说:“啊,你都35了呀!”肯定不会开心的,所以保护起来,小心别让人知道为好。不论用什么访问修饰符,封装的目的就是根据类之间的关系与设计需要,把不该暴露出来包裹好,把可以让别的对象调用的公开,让对象在内存里活得井然有序。如果你有点儿拿捏不准什么是该暴露出来的什么是该封装起来的,那么你就联系到实际生活中的例子去想想。看看你家的马桶,放水的按钮是暴露出来的,但您按下按钮之后内部是如何运作把水放出来的,这就不用用户关心了,自然也就封装起来了,如果您还印象不够深,那就想想,马桶排水的管道应该暴露还是封装起来呢。

也许有朋友会问,既然女孩子的年龄是隐私,那么为什么不干脆用private呢,这样岂不是封装得更彻底?嗯,只要private了,除了自己,谁都甭想知道。这比较适合犯罪记录,比如“private int _killedPeopleAmount;”我们之所以把age属性设为protected的,就是考虑到面向对象的另一个概念:继承。

所谓“龙生龙,凤生凤,老鼠的儿子会打洞”。同类的事物,可以把公共的部分抽象出来放在基类中,子类去继承,就不用在每个子类中都单独声明了。如果把age设置为private,子类将不能继承,不符合面向对象的思想。请看下面这个类:

   1: public class Chinese : Person
   2: {
   3:     public object HuKou { get; set; }
   4: }

我们把上面的Person类当作人类的基类,其中的Name和age以及SetName方法都是人类共同的地方,因此声明Chinese的时候只需要继承Person类就可以获得这些属性和方法,没必要自己再去重新声明一遍。精力用在刀刃上,考虑Chinese类的属性和方法的时候只需要考虑那些具有中国特色的就行了,例如户口。

接下来我们来探讨探讨什么是多态。所谓多态,中国有句古话可以概括:龙生九子,种种不同。都是龙的儿子,又各有差别,不过这样说起来有点儿泛泛,只是辅助记忆而已,可千万别在这上面认死理儿。多态有两种表现方式,一是同一个类中有多个名字一样但签名不同的方法;二是子类改写父类中方法的内部实现。其实“龙生九子,种种不同”这个概括只能映射其中第二点表现方式,也就是virtual和override之间的关系。至于第一点嘛,可以追溯到多态的英文单词的起源,据说这个词来源于西方神话故事里一个叫Polymorph神,他具体从事什么工作不重要,关键是他有个特点,白天一个样晚上一个样,同一个人表现出不同的样子,人格分裂的典型临床症状,软件开发前辈将他抽象成面向对象中的“多态”,可以很好地解释方法重载。

首先我们来看看方法重载:

   1: public class UserManager
   2: {
   3:     public object GetUser(int id)
   4:     {
   5:         // TODO: Find this user instance by id.
   6:         return new object();
   7:     }
   8:  
   9:     public object GetUser(string name)
  10:     {
  11:         // TODO: Find this user instance by name.
  12:         return new object();
  13:     }
  14: }

我们假设有UserManager这样一个工具类,提供了获取用户实例的机制,但又有两种方式,一是通过id另一种则是通过name,通过方法重载,我们只需要变动一下方法的入参数据类型从而改变了函数签名即可,没必要分别声明GetUserByID和GetUserByName两个方法,那样的话会让整个类在被调用时显得很臃肿,要不然像MessageBox.Show这样提供21种重载方式的方法,对于开发者和设计者来说岂不都是噩梦?

根据我的经验,面向对象在面试中主要考察的部分就是多态,而在多态中主要考察的就是virtual和override之间的关系。我们来看码说话吧:

   1: public class Dragon
   2: {
   3:     public virtual void Fly()
   4:     {
   5:         Console.WriteLine("I'm flying.");
   6:     }
   7: }
   8:  
   9: public class DragonSonA : Dragon
  10: {
  11:     public override void Fly()
  12:     {
  13:         base.Fly();
  14:     }
  15: }
  16:  
  17: public class DragonSonB : Dragon
  18: {
  19:     public override void Fly()
  20:     {
  21:         Console.WriteLine("I can't fly.");
  22:     }
  23: }

这段示例代码比较浅显易懂。我们声明了一个Dragon类,它有一个飞行的方法,因此它可以在天上随意地飞,它的儿子也会从他那里继承到这个飞翔的方法,但又“种种不同”,有的龙恐怕并不会飞,例如DragonSonB。甭管龙的儿子是不是都会飞吧,这段代码主要是展示一下,在父类中有的方法虽然会被子类继承,但将来在子类使用时可能有不同的实现机制,这时候就需要在父类中声明该方法时加上virtual关键字,以表示将来是可以改写的,在子类中需要则使用override关键字,表示儿子自有主见。

很多时候面试官会围绕virtual和override问一些其它情况,例如如果父类里面的方法没有virtual关键字,光是子类里方法前有override关键字呢?这种情况不用多想,语法错误,就算面试官答应,编译器也不会答应的。需要注意的是,如果子类里继承的方法如果没有override关键字,会是怎样呢?当我们把DragonSonB.Fly方法前的override关键字去掉,这种情况编译器在编译的时候会默认在函数声明前面加上一个new关键字,这就阻断了继承链,也就是说DragonSonB.Fly和Dragon.Fly没有什么关系了,只不过方法名雷同而已。具体怎么体现呢?我们来试试下面的代码:

   1: static void Main(string[] args)
   2: {
   3:     Dragon dragon = new DragonSonB();
   4:     dragon.Fly();
   5:  
   6:     Console.ReadLine();
   7: }

调用dragon.Fly方法时,编译器会顺着继承链往下找,一直找到合适的那个方法体。如果DragonSonB.Fly方法有override关键字,则说明它是从Dragon那儿继承来的,因此dragon.Fly实际上调用的是DragonSonB.Fly,输出结果就是“I can’t fly.”。但如果DragonSonB.Fly去掉了override关键字或者换成了new关键字,那么就意味着这个Fly方法不再是从Dragon那儿继承来的了,因此在调用dragon.Fly方法时,继承链就不能到达DragonSonB.Fly方法而只能停留在它的上一级,也就是调用Dragon.Fly方法。

搞清楚了上面提到的概念,估计面试中面向对象尤其是多态部分问题就不大了,还请各位高手不吝赐教,多多补充完善!

Everything is object.

转载请注明出处。版权所有©1998-2010 Autumoon Lab,保留所有权利。
0
0
(请您对文章做出评价)
« 上一篇:值类型和引用类型的区别
» 下一篇:寻找失落的SubString
posted @ 2008-08-19 17:40 Autumoon 阅读(3547) 评论(26)  编辑 收藏 网摘 所属分类: .NET程序员技术面试迷你手册(C#版)

  回复  引用  查看    
#1楼2008-08-19 18:45 | Jeffrey.Sky      
这是我们首先要掌握的
  回复  引用  查看    
#2楼2008-08-19 19:27 | 路西菲尔      
调用dragon.Fly方法时,编译器会顺着继承链往下找,一直找到合适的那个方法体
=====================================
编译器查找?override不是运行时概念吗

  回复  引用  查看    
#3楼2008-08-19 19:32 | 亚历山大同志      
这三个只是面向对象最主要的特征,而面向对象最重要的是要掌握下面这些设计原则:
开闭原则
里氏代换原则
依赖倒转原则
接口隔离原则
组合/聚合复用原则
迪米特法则
这些才是学习面向对象应该去主要了解掌握的。
封装、继承、多态老实说太浅了一点

  回复  引用  查看    
#4楼2008-08-19 19:33 | 亚历山大同志      
@路西菲尔
override是编译时的,反射才是运行时的

  回复  引用  查看    
#5楼2008-08-19 20:06 | Alone      
貌似和我读得 C#本质论 内容相似
  回复  引用  查看    
#6楼[楼主]2008-08-19 20:48 | Autumoon      
@亚历山大同志

谢谢,不过我这个系列主要把握的水平就是面试通常的水平,请注意看看我的序言中的内容。

  回复  引用  查看    
#7楼2008-08-19 20:54 | Myhsg      
博客园好文很多,

每次都想看完

上博客园真“累”

:-)

  回复  引用  查看    
#8楼2008-08-19 22:12 | 路西菲尔      
@亚历山大同志
我是说,博主为什么说编译器动态查找继承链的,这是什么理论

  回复  引用  查看    
#9楼2008-08-19 22:58 | codemo      
讲的还不错!
  回复  引用  查看    
#10楼2008-08-19 23:06 | guojing      
有点意思 支持lz
  回复  引用  查看    
#11楼2008-08-19 23:18 | 亚历山大同志      
@路西菲尔
这个特征特现了C#编译器对里氏代换原则的支持。也就是:凡是基类适用的地方子类一定适用。
比如class Base,class Sub:base
如果:Base a=new Sub();
那么实际上编译器已经能够确定a所指向的对象的类型。所以方法的地址确实是在编译期就确定了的。

  回复  引用  查看    
#12楼2008-08-19 23:34 | 路西菲尔      
@ 亚历山大同志
这跟里氏代换原则没多大关系。
class Base,class Sub:base 都有方法Foo().

Base a=new Sub();
a.Foo();
编译器就知道Callvirt Base::Foo.
Foo是虚还是非虚编译器不管,在Jit的时候才关心

  回复  引用  查看    
#13楼2008-08-20 08:24 | 非空      
我是来看楼主最后那图的o(∩_∩)o...哈哈
  回复  引用  查看    
#14楼2008-08-20 10:46 | 时间太快      
Dragon dragon = new DragonSonB();
dragon.Fly();

这样声明对象,如果DragonSonB有其它方法,那dragon就无法调用了。。
比如
public class DragonSonB:Dragon
{
public override void Fly()
{

}
public void Study()
{

}
}
所以我现在想搞明白的是:
Dragon dragon = new DragonSonB();和
DragonB dragon = new DragonSonB();这样有什么大的区别??

  回复  引用  查看    
#15楼2008-08-20 10:47 | 炭炭      
Dragon dragon = new DragonSonB();

-----
试想我不知道DragonSonB(),怎么写出这句呢?你还是没解释透阿,呵呵

  回复  引用  查看    
#16楼2008-08-20 10:50 | 炭炭      
@时间太快

干脆我说了吧,Dragon dragon = new DragonSonB(); 这句话是没有意义的,后面应该用不是new的返回对象的方法,比如类工厂,才能真正实现多态!

  回复  引用    
#17楼2008-08-20 11:11 | xingyunet[未注册用户]
虽然很简单!但看完就懂了!很贴近生活!
  回复  引用  查看    
#18楼2008-08-20 11:45 | 时间太快      
@炭炭
也就是说Dragon dragon = new DragonSonB(); 这种方法没什么用,还不如用DragonB dragon = new DragonSonB();
如果用工厂模式这样做。
Factory Dragon=Factory.GetDragonB();
这也许有点看头。。。

  回复  引用  查看    
#19楼2008-08-20 13:40 | 炭炭      
@时间太快

正是这样,试想我们知道了DragonSonB()为什么不直接DragonB dragon = new DragonSonB()呢?
事实是我们在写基类逻辑时是不知道将来有什么子类,以及哪个子类会执行我的调用的。但是基类的逻辑又是不需要关心这个问题的,这才是多态的意义。往深里说就是要实现OCP
Dragon dragon = new DragonSonB() 这一句涌来解释多态是不合适的,因为你已经表明了要用DragonSonB方法而不是多态方法。
解释多态,教科书里比较常用的方法是调用放在一个集合里的子类,写形如

foreach dragon dr in drangoList { ... } 的代码。

实际工作中,比较多的就是类工厂了。

  回复  引用  查看    
#20楼2008-08-20 14:07 | @我本善良      
Encapsulation
  回复  引用  查看    
#21楼2008-08-20 14:12 | Ivony...      
关于封装的部分非常不错……
  回复  引用  查看    
#22楼2008-08-20 14:12 | Ivony...      
--引用--------------------------------------------------
亚历山大同志: 这三个只是面向对象最主要的特征,而面向对象最重要的是要掌握下面这些设计原则:
开闭原则
里氏代换原则
依赖倒转原则
接口隔离原则
组合/聚合复用原则
迪米特法则
这些才是学习面向对象应该去主要了解掌握的。
封装、继承、多态老实说太浅了一点
--------------------------------------------------------


不是把书背下来就可以写好程序的。

  回复  引用  查看    
#23楼2008-10-05 16:57 | RyanXM      
--引用--------------------------------------------------
亚历山大同志: @路西菲尔
override是编译时的,反射才是运行时的
--------------------------------------------------------

"在.NET中,覆写实现了运行时的多态性,而重载实现了编译时的多态性。"

那这句话的说法对不对?

  回复  引用  查看    
#24楼[楼主]2008-10-15 06:28 | Autumoon      
@RyanXM

“在.NET中,覆写实现了运行时的多态性,而重载实现了编译时的多态性。”我认为这句话是指将“override是编译时的,反射才是运行时的”的说法更加学术化了一点儿而已,我个人认为,“多态”是思维层面的东西,不必要非要分一个运行时或编译时,我觉得,如果一定要把它归为哪个时的话,那还是列为“设计时”比较合适,呵呵。

  回复  引用    
#25楼2008-10-26 12:08 | 你亲爹[未注册用户]
@亚历山大同志
你这个人就是看不得别人好是不是

  回复  引用    
#26楼2009-02-13 10:17 | fresh-man[未注册用户]
写得很好!希望能写更多更具体的!谢谢啦