面向对象方法论
在.net中面向对象是一个非常基本的概念,但是由于面向对象方法非常的抽象,很多人理解起来非常的困难。这里我尽可能的抛开书本上的书面化绕口和晦涩的定义,来描述以下面向对象方法论。为了帮助大家理解,我主要包括了几个部分:面向对象的概念,面向对象的思想,面向对象的实现,和深入面向对象。
下面我对很多词语和概念给出的定义和书本是完全不同的,因为我只是为了让大家更直观的理解这些概念,而使用了我觉得最能够体现它们的语言。所以如果你如果要考试的话,千万不要照着我说的写,否则多半是没分的。
一、面向对象的概念:
什么是面向对象?面向对象是一个方法论——注意,不是只有c#才有面向对象,更不是.net才有面向对象,而只是c#是支持面向对象的或者是基于面向对象。我们这里讲的面向对象适用于所有的面向对象。c#的语法上的规定我们是忽略的,实际上任何语法上的规定我们这里都会尽量的忽略。
要理解面向对象的概念,必须而且只需要理解这几个概念:封装、类、继承、重载、重写、多态。
封装:
我用一句话来描述封装就是:为了重复使用而将代码集中于一个独立体内,就是封装。封装的方法有哪些呢? 我们最早接触到的函数,包括我们接下来要知道的类,甚至于程序本身都可以称为封装。
封装有以下几个特点:
目的:是为了将一段代码重复使用——就是为程序员提供了偷懒的方法;
特点:使用者可以直接使用被封装的代码而不用关心被封装的代码是如何实现其功能的。
为什么要封装呢?
我们先来写一个小程序(我我描写的代码几乎都是伪代码,是不能直接执行的,只是为了方便大家理解而使用的一种类似代码的而表示算法或者结构一种类似代码语法的结构):
string a= “hello,word”
print>a; // 打印a;
这段小程序没有什么问题,现在我做一个非常可恶的要求给程序员:把a打印20遍~
string a= “hello,word”
print>a;
print>a;
……..
print>a;
我估计任何一个写程序的人都会烦死了,于是程序员,想出了一个方法:
string a= “hello,word”
pa();
pa();
pa();
pa();
pa() {print>a;print>a;print>a;print>a;}
这里,他在pa()里封装了四个print>a。 这就是一个最基本的封装。
所以简单来说,把一段单独放置在一个可以直接使用的地方就是封装。
类:
在理解了封装的概念之后,我们就可以更好的理解类。同样我用一句话来描述——将变量和函数一起封装起来后形成的东西就是类——当然在.net中还有另一样东西也可以把变量和函数封装在一起,这里我们先放下那个不应该存在的捣乱的家伙(那就是struct——结构体)。
现在我要让我的程序员在刚才那段程序上再修改:说三次hello,china! 说三次hello,world! 说三次hello,you!
于是我的程序员又得想办法偷懒了,原来的方法已经不管用了,他就是这样偷懒的。
Class A
{
string a;
pa() {print>a;print>a;print>a;}
}
A a,b,c;
a.a = “hello,china!”; a.pa();
b.a = “hello,world!”; a.pa();
c.a = “hello,you!”; c.pa();
你们看到了么?我们的程序员为了偷懒就这样弄了一个类出来~~~
继承:
什么是继承,我用一句话来描述它:一个为了扩展类的功能而从一个类扩展出一个新的类。这就是继承。
接下来,我们的程序员会做一件神奇的事情让我们理解什么是继承。我现在要让他修改程序,改成什么呢——我跟他说一个人的名字xxx要让他连在一起说三次hello,xxx。
起初他是这样做的:
Class B
{
string a;
string b;
pa() {print>a+b;print>a+b;print>a+b;}
}
A a;
a.a = hello; a.b =xxx; pa();
后来我又跟他说了两个名字,三个名字,甚至是数字,甚至还让他在里面打加法结果。于是他终于不厌其烦了。因为他的类变的越来越大,而且变得他自己都无法理解了。在我告诉他打印hell以及两个自然数的和的时候,他把程序改成了下面的样子。
Class B:A
{
int i,j;
pb() {print>(i+j);}
}
B b; b.a=hello; b.i=1; b.j=2; b.pa(); b.pb();
怎么成了这样?似乎这个新的类变的更小了。而且我没有看到这个类的定义里有变量a。这是如何做到的呢?
这就是继承——以A为基础产生的类B完全继承了A的东西。就好像儿子继承了父亲的遗产一样。
重载:
重载的意思就是:利用参数类型和参数个数的不同而让有相同名字的函数实现不同功能。—— 一般教材都会说是为了让同一个函数实现不同的功能之类的——这里我希望你们不要把这些函数当成一个函数,他们只是有不同的名字而已
某一天程序员接到了一个任务——编写一个两个整数相加。于是他写了这样一个函数:
sum(int a,int b){…}
可惜他马上又接到了这样的任务,再编写三个整数相加的,整数和浮点数相加的,两个浮点数相加的…………,于是他有了下面的这些:
sum(int a,int b){…}
sum2(int a, float b){…}
sum3(int a,int b,int c){…}
sum4(float a,float b){…}
…………
最后终于他发现他完全记不请楚他自己的哪个函数有哪些参数了,甚至他都不记得自己写了哪些相加的函数。于是他想出了一个方法——让编译器来根据参数识别,于是有了下面这样的函数们:
sum(int a,int b){…}
sum(int a,int b,int c){…}
sum(int a,float b){…}
sum(float a,float b){…}
…………
现在他方便了,不管要写多少个。调用的时候就直接调用sum就可以了。
重写:
重写是什么呢:重写和重载其实有点像~只不过重写是子类对父类已经定义过的方法的重新定义。比如:
Class A
{
string a;
pa() {print>a;print>a;print>a;}
}
Class B:A
{
int i,j;
pa() {print>(i+j;}
}
这样以来,原来类A中的函数pa();就被新的pa()给替换了~
这就是重写~~~
多态:
多态是什么呢?我就简单说一句:多态就是重载和重写的统称~~~~。
关于重载和重写:
最后我们再来看一个典型的重载和重写:
Class A
{
string a;
pa(int s) {print>a;print>a;print>a;}
}
Class B:A
{
int i,j;
pa(int s) {print>(i+j;}
pa(int x,int y){print>x*y;}
pa(string s){};
}
在B中,很明显同时存在了重载和重写两个概念:
pa(int s)是对A中的pa(int s)的重写;pa(int x,int y)和pa(string s)对A中的pa(int s)的重载;
从这里我们可以看出重载和重写有两个非常大的不同:
重写是方法名、参数类型、参数数量都相同;
而重载是方法名相同,参数数量或者参数类型不同;
那么我重写和重载的本质和他们的对比如下表所示——主要是通过B和A两个函数之间的对比来进行判断是否是重写或者重载:
核心、区分点 | 重写 | 重载 |
作用范围 | 只有子类里的函数能对父类里的函数重写 | 任何范围内 |
参数类型和个数 | 相同 | 至少有一个是不同的 |
方法(函数)名 | 相同 | 相同 |
这里的重载的任何范围内要注意一点:必须是两个函数都可以直接使用函数名调用的情况。也就是编程语言规定的不能重复命名函数的范围。
二、面向对象的思想
要真正的学会使用面向对象,就必须学会面向对象的思想。什么是面向对象的思想?其实面向对象的思想的核心——抽象和实现的过程。抽象和实现的过程对于学习过建模的同学来说可能非常容易理解,而对于没有接触过的同学来说就很模糊了。
既然抽象和实现的过程就是建模的过程,而面向对象的核心就是抽象和实现,因此面向对象的核心可以说就是建模。只不过建模主要是关注于抽象的过程。而面向对象在关注抽象的同时也关注实现的过程。
接下来我们就用一个简单的实例来说明面向对象是怎么创建并实现一个类的,又怎么把这个类变成了一个实例。
这中间我们会附带讲解到三个概念——抽象、实现和实例化。
那么现在我们有一群狗,有好多的狗狗啊:拉不拉多,金毛,腊肠,京巴,德国牧羊犬,苏格兰牧羊犬,暂时就这些吧,够用了。我们还有一群猫:有中华田园猫,波斯猫,咖啡猫,暂时就这么多吧。
现在他们每个都是一个个体。那么这些个体都有很多的特性。
我们现在需要把他们都归到同一类,那么需要做什么呢?需要给他们取一个统一的名称——就叫keing吧。我随便输的,叫什么没关系,反正现在我们把他们堆到一堆了。
那么keing有什么样的特点呢?(我们需要寻找这些所有的猫猫狗狗的共同的特点)
好吧,有四条腿,有嘴巴,有毛,有尾巴,都会叫,会跑,会摇尾巴,还会吃东西。
好吧,我们先记下来
keing : legs =4 ; mouth; hair ; tail;
crying(); running(); waging() ; eating();
为什么我要把 有四条腿,有嘴巴,有毛,有尾巴 和 叫,会跑,会上厕所,还会吃东西 分开放,而且写法不一样呢? 不知道大家注意到了没有;腿啊,嘴巴啊,毛啊,尾巴什么的都是keing们身上特有的。是静态的。而跑啊,叫啊,吃东西啊,摇尾巴啊,这些都是他们利用嘴啊,腿啊,尾巴啊,什么的完成。是动态的。
比如: crying 是利用mouth完成的动作。 Waging() 是利用 tail完成的动作。
因此我们就称为crying()是对mouth操作的方法,即crying()是对mouth的方法。
现在我们可以建立我们的第一个模型:
class keing
{legs =4;
mouth;
hair;
tail;
crying();
running();
waging();
eating();}
现在我们发现了,我们这样的一个模型没有办法区分我们的这些keing们啊。我们得找出他们不同的地方,他们不同的地方都有哪些呢? 他们大小不一样,他们的毛的颜色也不一样,他们的眼睛颜色也不一样。那现在我们把这些也添加进去。
class keing
{legs =4;
body;
eyes-color;
tail-color;
mouth;
hair;
tail;
crying();
running();
waging();
eating();}
好了,现在我们的模型基本就建立完了。现在我们试着给我们这些keings们对号入座吧:
keing k1= new keing();
keing k2 = new keing();
…………
keing k9 = new keing();
现在我们得对这些一大堆的东西赋值……天啊,我想想都头疼了,这么多的属性要赋值~
我们缩减一下吧:既然有些属性是所有的keing都有的,那么这些属性的值理所当然的就一样了,那么我就可以不存放他们了。
于是我们的keing类成了如下的样子:
class keing
{body;
eyes-color;
tail-color;
crying();
running();
waging();
eating();}
好了,到现在为止,我们的一个类就已经建立完成了~~到目前为止我们已经完成了抽象的过程。
在这个过程中我们主要做了什么呢?我们寻找了各个个体之间的共同点,利用这些共同点我们命名了一个新的东西~然后又在这个新的东西里添加了一些能够区分不同个体的特征——这些就是属性。通过对这些属性的赋值的不同,我们就可以表示不同的个体。
而对属性的操作就是方法,同一类的个体对操作属性的方法是相同的,只是他能使用的方法是相同——比如我们的keing们中的任何一个个体对嘴的操作都是叫和吃东西——他们都不能拿嘴去跑步吧,都是拿腿跑步的。所以方法其实就是表明这一类个体如何使用属性。
所以大家现在应该可以理解了抽象的过程——建立模型——类和一般的模型有一点点差别,所以在类的抽象过程比一般的模型建立稍微复杂一点。
通常的建模模型只包含了属性,而类包含了方法。所以一般的建模过程仅仅需要对属性的归纳整理,而类的建立过程除了对属性的归纳外还需要对属性的方法进行归纳整理。
下面的一步,我们就要做一点特别的事情:我们的keing们有了自己的属性,他们会跑,会吃东西,会叫,还会摇尾巴,可是他们是怎么样完成这些动作的呢?我们给他们定义了方法,使得他们可以做这些,但是他们并不知道怎么样去做。于是下面我们就要完成这个工作:
crying():keing{ cryint loudly;}
running():keing {rushing with for legs;}
waging():keing {waging tail left to right;}
eating():keing{eating food;}
好了,现在他们知道怎么样去吃,怎么样去叫,怎么样跑,还知道怎么样摇尾巴。——这个过程是什么呢?这个过程就是实现。
那么综合前面抽象的过程,我们就可以明白清晰的理解抽象和和实现的完整的过程——抽象就是让一个类具有什么属性(HAS),和能够使用什么样的方法(CAN);而实现的过程就是告诉类如何去使用这个方法来完成对属性的操作(HOW)。
现在我们的keing们已经知道他们应该怎么样去完成这些动作了,那么接下来我就需要将他们和我们的那一群猫猫狗狗映射到一起了。比如,我们将我们的波斯猫和苏格兰牧羊犬和keing这个模型分别映射到一起:
keing Bosicat = new keing;
keing Sugelandog= new keing;
这样,我们就联系到了一起,现在Bosicat 和 Sugelandog 都分别是keing中的一个了。他们都是一个独立的keing的个体。这就是实例化的过程。到现在为止,我们已经完成了一个非常简单的面向对象过程了。
接下来我们将会做更多的工作来一步一步的理解面向对象。
首先,来看我们的Bosicat 和sugelandog,他们看起来是一样的,这是为什么呢? 因为我们还没有给他们的keing属性赋值,所以默认情况下,属性都是一样的,那么我们现在就来给他们赋值吧:
Bosicat.body = small;
Bosicat.eyes-color = one blue one green;
Bosicat.tail-color = white;
sugelandog.body = small;
sugelandog.eyes-color = one blue one green;
sugelandog.tail-color = white;
大家有没有觉得很麻烦啊?我是觉得麻烦了xxx.xxx = xxx;这样要是给我们的九个猫猫狗狗全实例化的话,得花多少时间啊!~到这里大家可能都和我一样希望有一个方法能够让我们一次就直接给他们的所有属性赋值。 好吧,我们来看看我们的keing 类,有没有可以改进的地方:
class keing
{body;
eyes-color;
tail-color;
crying();
running();
waging();
eating();}
似乎我们有一个非常简单的方法,我们给他添加一个set()方法,这样我们就可以一次性的给所有属性赋值了。
Set(body, color1, color2) : +keing{body =body; eyes-color = color1; tail-color=color2}
//本文中我都将用“:+ classname”表示为某一个类中添加一个方法或者属性
现在我们来重新实例化一个keing ,这次我们选金毛狗狗吧:
keing jinmao = new keing;
jinmao.Set(large, black, gloden);
这样是不是很方便了呢?既然我们用这个方法能够方便的给属性赋值,那么我们能不能想办法让实例化的时候自动执行这样的方法呢?那样不是就很方便了么;
其实在面向对象思想中是有这样的一个东西的,它被称为构造函数——意思就是用来构造类的函数,这是一个在类实例化的时候会自动执行的函数,怎么定义呢?构造函数的定义和其他函数没有太大的区别,唯一的一点特殊就是:构造函数没有函数类型,其函数名就是类名。
而如果我们没有定义构造函数的话,编程系统通常会为我们创建一个无参数的空构造函数。
现在我们就来为我们的keing 添加一个构造函数,让我们实例化的时候就可以赋值属性,并重新实例化一个keing ,就用中华田园猫吧:
keing(body,color1,color2):+keing{ set(body,color1,color2)}
keing chinacat = new keing(small,grown,grown);
现在来看,我们是不是非常的方便了~~~;
今天我们接到一个电话,说要送来一只哈士奇,哈士奇是什么?我们还没看到呢,但是我希望先给他编入我们的keing里,于是我们这样做了:
keing Hashiqi = new keing(,,); //我们发现一个问题,我们没有办法继续些了,因为我么实在不知道他是什么样的一个东西。
这里我们就可以用到面向对象中一个非常著名的概念——重载了。方法如下:
keing():+keing{} //我再定义一个无参数的构造函数。
这样我就有办法来为我们的chinacat加入keing呢。
keing Hashiqi = new keing();
现在我们来编写一个show()的函数让我们的keing来动一动,叫一叫。看看效果怎么样。我们一样选一个吧,就选金毛和中华田园猫吧:
Show()
{
chinacat.crying(); → cryint loudly
jinmao.crying(); → cryint loudly
chinacat.running(); → rushing with for legs
jinmao.running(); → rushing with for legs
}
这里,我用→表示后面是语句执行的结果。我们发现chinacat和jinmao 叫出来都是一样的~他们跑起来的效果都是一样的。这可不太好,因为狗狗的声音和猫猫的声音听起来并不一样,不过各种狗狗的声音之间以及各种猫猫的声音之间并没有多大的差别。
我们现在该怎么办呢?重新建新的类? 不用,我们可以应用继承的概念,直接从keing继承就可以了:
class dog extends keing
{
}
class cat extends keing
{
}
我们先不给这两个类添加任何东西,我们先把我们映射的实例都转移到这两个类下面来。
dog Jinmao = new dog();
dog Sugelandog = new dog();
dog Hashiqi = new dog();
cat Bosicat = new cat();
cat Chinacat = new cat();
Jinmao.set(large,grawn,grawn);
Sugelandog.set(large,black,white);
…………
我就不写完了,这里大家想想,为什么我们不能直接用构造函数来赋值呢?至于为什么,后面我们更深入的分析的时候再来讨论这个问题。现在继续我们后面的工作,我们来测试一下新的Jinmao和Chinacat的动作吧:
Show()
{
Chinacat.crying(); → cryint loudly
Jinmao.crying(); → cryint loudly
Chinacat.running(); → rushing with for legs
Jinmao.running(); → rushing with for legs
}
没有什么差别。下面我们就要对我们的dog 和cat 这两个类进行一点修改:
crying ():+dog{ wangwang;}
crying() :+cat{ miaomiao;}
running():+dog{running and jump;}
running():+cat{running and hugging;}
下面我们再来看看他们的表现如何呢:
Show()
{
Chinacat.running(); → running and hugging;
Jinmao.running(); → running and jump;
Chinacat.crying(); → miaomiao;
Jinmao.crying(); → wangwang;
}
现在有差别了,我们再看其他的猫猫或者狗狗会有什么样的反应呢?
Show()
{
Bosicat.running(); → running and hugging;
Sugelandog.running(); → running and jump;
Bosicat.crying(); → miaomiao;
sugelandog.crying(); → wangwang;
}
也是一样——这里我们就用了重写的方法成功的猫猫和狗狗们在执行crying和running这两个方法时有了不同的表现了。
到现在我们成功完成了一次完整的面向对象的过程。我们运用了抽象的方法来创建类,又用实现的方法给类定义了方法,接下来又利用了重载、继承、重写的方法让我们的类具有了更多的特性。但是还是有很多东西我们目前还没有涉及到。接下来我们就要深入去面向对象,去挖挖面向对象究竟是怎么工作的。怎样实现它的各个概念。
三、深入面向对象
一切从构造开始:
在这一节我们要一层一层的揭开我们面向对象神秘的面纱。我们来看看面向对象是怎么工作的。下面我创建了一个简单的C++程序(在这一节里有关面向对象的语言我将使用C#语言,但是不一定是标准的C#语言来编写,以便大家能够了解到各种的错误以及不同的情况。):
void main()
{
int a=1, b=2;
cout>”a+b=”>sum(a,b); //这一行的执行结果将在屏幕上输出 a+b=3
}
int sum(int a, int b){return a+b;}
现在我们不要去管这一段的语法是什么,我们关心的是一点,为什么我们一定要有个main()函数,如果没有这个main函数会怎么样呢?
我们把main()改个其他的名字再执行一下,就会发现编译器会报错:未找到main函数。为什么会这样呢?我们试想有这样一堆函数:
void a();
void b();
void c();
void d();
…………
我们就会发现一个问题,究竟应该先执行哪个函数呢?我们无法确定,同样编译器也无法确定,于是在语言设计中就有了这样一个约定:程序的执行永远是从主函数开始执行的。那么到底哪个是主函数呢?一般情况下是main()函数,也许不同的语言中不太一样。但是一定有这样的一个。
现在我们再回到我们的类,我们首先来看一个简单的类:
class A{}
这个类里没有任何东西,但是如果我们给一个中断,会发现他是有类型的,而且也是有值的,
如果我们能够查看内存的话,就会发现他还是占用了内存空间的。
现在我们给他添加一点东西:
class A{
public void fun1(){Console.Write(“this is fun1”);}
}
我们再次的中断,似乎没有什么大的差别,那么我们再改一下呢:
class A{
int a;
public void fun1(){Console.Write(“this is fun1”);}
}