OOP技术按照现实世界的特点来管理复杂的事物,把它们抽象为对象,具有自己的状态和行为,通过对消息的反应来完成一定的任务。这种编程方法提供了非常强大的多样性,大大增加了代码的重用机会,增加了程序开发的速度;同时降低了维护负担,将具备独立性特制的程序代码包装起来,修改部分程序代码时不至于会影响到程序的其他部分。
1. 对象
什么是对象?实际上,现实世界就是由各种对象组成的,如人、汽车、动物、植物等。复杂的对象可以由简单的对象组成。对象都具有各自的属性,如形状、颜色、重量等;对外界都呈现出各自的行为,如人可以走路、说话、唱歌;汽车可以启动、加速、减速、刹车、停止等。
在OOP中,对象就是变量和相关的方法的集合。其中变量表明对象的属性,方法表明对象所具有的行为。一个对象的变量构成了这个对象的核心,包围在它外面的方法使这个对象和其他对象分离开来。例如:我们可以把汽车抽象为一个对象,用变量来表示它当前的状态,如速度、油量、型号、所处的位置等,它的行为则为上面提到的加速、刹车、换档等。操作汽车时。不用去考虑汽车内部各个零件如何运作的细节,而只需根据汽车可能的行为使用相应的方法即可。实际上,面向对象的程序设计实现了对象的封装,使我们不必关心对象的行为是如何实现的这样一些细节。通过对对象的封装,实现了模块化和信息隐藏。有利于程序的可移植性和安全性,同时也利于对复杂对象的管理。
简单地说,对象非常类似于本书前面讨论的结构类型。略为复杂的对象可能不包含任何数据,而是只包含函数,表示一个过程。
2.类
在研究对象时主要考虑对象的属性和行为,有些不同的对象会呈现相同或相似的属性和行为,如轿车、卡车、面包车。通常将属性及行为相同或相似对象归为一类。类可以看成是对象的抽象,代表了此类对象所具有的共同属性和行为。典型的类是“人类”,表明人的共同性质。比如我们可以定义一个汽车类来描述所有汽车的共性。通过类定义人们可以实现代码的复用。我们不用去描述每一个对象(如某辆汽车),而是通过创建类(如汽车类)的一个实例来创建该类的一个对象,这样大大碱化了软件的设计。
类是对一组具有相同特征的对象的抽象描述,所有这些对象都是这个类的实例。在C#中,类是一种数据类型,而对象是该类型的变量,变量名即是某个具体对象的标示名。
3.属性和字段
通过属性和字段可以访问对象中包含的数据。对象数据可以区分不同的对象,因为同一个类的不同对象可能在属性和字段中存储了不同的值。包含在对象中的不间数据统称为对象的状态。
假定一个对象类表示一杯咖啡,叫做CupOfCoffee。在实例化这个类(即创建这个类的对象)时,必须提供对于类有意义的状态。此时可以使用属性和字段,让代码能通过该对象来设置要使用的咖啡品牌,咖啡中是否加牛奶或方糖,咖啡是否即溶等。给定的咖啡对象就有一个指定的状态,例如“Columbian filter coffee with milk and two sugars”。
可以把信息存储在字段和属性中,作为string变量、int变量等。但是,属性与字段是不同的,属性不能直接访问数据。一般情况下,在访问状态时最好提供属性,而不是字段,因为这样可以更好地控制整个过程,而使用它们的语法是相同的。
对属性的读写访问也可以由对象来明确定义。某些属性是只读的,只能查看它们的值,而不能改变仑们(至少不能直接改变)。还可以有只写的属性,其操作方式类似。
除了对属性的读写访问外,还可以为字段和属性指定另—种访问许可,这种可访问性确定了什么代码可以访问这些成员,它们是可用于所有的代码(公共),还是只能用于类中的代码(私有),或者更复杂的模式。常见的情况是把字段设置为私有,通过公共属性访问它们。
例如,CupOfCoffee类,可以定义5个成员:Type、isInstant、Milk、Sugar、Description等。
4.方法
对象的所有行为都可以用方法来描述,在C#中,方法就是对象中的函数。
方法用于访问对象的功能,与字段和属性—样:方法可以是公共的或私有的,按照需要限制外部代码的访问。它们常常使用对象状态——访问私有成员。例如,CupOfCoffee类定义了一个方法AddSugar()来增加方糖数属性。
实际上,C#中的所有东西都是对象。控制台应用程序中的Main()函数就是类的一个方法。前面介绍的每个变量类型都是一个类。前面使用的每个命令都是一个属性或方法。句点字符“.”把对象实例名和属性或方法名分隔开来。
5.对象的生命周期
每个对象都一个明确定义的生命周期,即从使用类定义开始一直到删除它为止。在对象的生命周期中,除了“正在使用”的正常状态之外,还有两个重要的阶段:
● 构造阶段——对象最初进行实例化的时期。这个初始化过程称为构造阶段,由构造函数完成。
● 析构阶段——在删除一个对象时,常常需要执行一些清理工作,例如释放内存,由析构函数完成。
5.1构造函数
所有的对象都有一个默认的构造成数,该函数没有参数,与类本身有相同的名称。一个类定义可以包含几个构造函数,它们有不同的签名,代码可以使用这些签名实例化对象。带有参数的构造函数通常用于给存储在对象中的数据提供初始值。
在C#中,构造函数用new关键字来调用。例如,可以用下面的方式实例化一个CupOfCoffee对象:
CupOfCoffee myCup = new CupOfCoffee();
对象还可以用非默认的构造函数来创建。与默认的构造函数一样,非默认的构造函数与类同名,但它们还带有参数,例如:
CupOfCoffee myCup = new CupOfCoffee(“Blue Mountain”);
构造函数与字段、属性和方法一样,可以是公共或私有的。在类外部的代码不能使用私有构造函数实例化对象,而必须使用公共构造函数。—些类没有公共的构造函数,外部的代码就不可能实例化它们。
5.2 析构函数
析构函数在用于清理对象。一般情况下,不需要提供解构方法的代码,而是由默认的析构函数执行操作。但是,如果在删除对象实例前,需要完成一些重要的操作,就应提供特定的析构函数。
6.静态成员
属性、方法和字段等成员是对象实例所特有的,即改变一个对象实例的这些成员不影响其他的实例中的这些成员。除此之外,还有一种静态成员(也称为共享成员),例如静态方法、静态属性或静态字段。静态成员可以在类的实例之间共享,所以它们可以看作是类的全局对象。静态属性和静态字段可以访问独立于任何对象实例的数据,静态方法可以执行与对象类型相关、但不是特定实例的命令,在使用静态成员时,甚至不需要实例化类型的对象。例如,前画使用的Console.WriteLine()方法就是静态的。
下面讨论OOP中的一些技术,包括:抽象与接口、继承、多态性、运算符重载等。
1. 抽象与接口
抽象化是为了要降低程序版本更新后,在维护方面的负担,使得功能的提供者和功能的用户分开,各自独立,彼此不受影响。
为了达到抽象化的目的,需要在功能提供者与功能使用者之间提供一个共同的规范,功能提供者与功能使用者都要按照这个规范来提供、使用这些功能。这个共用的规范就是接口,接口定义了功能数量、函数名称、函数参数、参数顺序等。它是一个能声明属性、字段和方法的编程构造。它不为这些成员实现,只提供定义。接口定义了功能提供者与功能使用者之间的准则,因此只要接口不变,功能提供者就可以任意更改实现的程序代码,而不影响到使用者。
一旦定义了接口,就可以在类中实现它。这样,类就可以支持接口所指定的所有属件和成员。注意,不能实例化接口,执行过程必须在实现接口的类中实现。
在前面的咖啡范例中,可以把较一般用途的属性和方法例如AddSugar(),Milk,Sugar和
Instant组合到一个接口中,称为IhotDrink(接口的名称一般用大写字母I开头)。然后就可以在其他对象上使用该接口,例如CupOfTea类。
一个类可以支持多个接口,多个类也可以支持相同的接口。
2.继承
继承是OOP最重要的特性之—。任何类都可以从另—个类继承,这就是说,这个类拥有它继承的类的所有成员。在00P中,被继承(也称为派生)的类称为父类(也称为基类)。注意C#中的对象仅能派生于一个基类。
公共汽车、出租车、货车等都是汽车,但它们是不同的汽车,除了具有汽车的共性外,它们还具有自己的特点,如不同的操作方法,不同的用途等。这时我们可以把它们作为汽车的子类来实现,它们继承父类(汽车)的所有状态和行为,同时增加自己的状态和行为。通过父类和子类,我们实现了类的层次,可以从最一般的类开始,逐步特殊化,定义一系列的子类。同时,通过继承也实现了代码的复用,使程序的复杂性线性地增长,而不是呈几何级数增长。
在继承一个基类时,成员的可访问性就成为一个重要的问题。派生类不能访问基类的私有成员,但可以访问其公共成员。不过,派生类和外部的代码都可以访问公共成员。这就是说,只使用这两个可访问性,不仅可以让一个成员被基类和派生类访问,而且也能够被外部的代码访问。为了解决这个问题,C#提供了第三种可访问性:protected,只有派生类才能访问protected成员。
除了成员的保护级别外,我们还可以为成员定义其继承行为。基类的成员可以足虚拟的,也就是说,成员可以由继承它的类重写。派生类可以提供成员的其他执行代码。这种执行代码不会删除原来的代码,仍可以在类中访问原来的代码,但外部代码不能访问它们。如果没有提供其他执行方式,外部代码就访问基类中成员的执行代码。虚拟成员不能是私有成员。
基类还可以定义为抽象类。抽象类不能直接实例化。要使用抽象类,必须继承这个类,抽象类可以有抽象成员,这些成员在基类中没有代码实现,所以这些执行代码必须在派生类中提供。
最后,类可以是密封的。密封的类不能用作基类,所以也没有派生类。
在C#中,所有的对象都有—个共同的基类object,我们在第二章中曾提到过。
3.多态性
多态是面向对象程序设计的又一个特性。在面向过程的程序设计中,主要工作是编写一个个的过程或函数,这些过程和函数不能重名。例如在一个应用中,需要对数值型数据进行排序,还需要对字符型数据进行排序,虽然使用的排序方法相同,但要定义两个不同的过程(过程的名称也不同)来实现。
在面向对象程序设计中,可以利用“重名”来提高程序的抽象度和简洁性。首先我们来理解实际的现象,例如,“启动”是所有交通工具都具有的操作,但是不同的具体交通工具,其“启动”操作的具体实现是不同的,如汽车的启动是“发动机点火——启动引擎”、“启动”轮船时要“起锚”、气球飞艇的“启动”是“充气——解缆”。如果不允许这些功能使用相同的名字,就必须分别定义“汽车启动”、“轮船启动”、“气球飞艇启动”多个方法。这样一来,用户在使用时需要记忆很多名字,继承的优势就荡然无存了。为了解决这个问题,在面向对象的程序设计中引入了多态的机制。
多态是指一个程序中同名的不同方法共存的情况。主要通过子类对父类方法的覆盖来实现多态。这样一来,不同类的对象可以响应同名的方法来完成特定的功能,但其具体的实现方法却可以不同。例如同样的加法,把两个时间加在一起和把两个整数加在一起肯定完全不同。
通过方法覆盖,子类可以重新实现父类的某些方法,使其具有自己的特征。例如对于车类的加速方法,其子类(如赛车)中可能增加了一些新的部件来改善提高加速性能,这时可以在赛车类中覆盖父类的加速方法。覆盖隐藏了父类的方法,使子类拥有自己的具体实现,更进一步表明了与父类相比,子类所具有的特殊性。
多态性使语言具有灵活、抽象、行为共享的优势,很好地解决了应用程序函数同名问题。
注意并不是只有共享同一个父类的类才能利用多态性。只要子类和孙子类在继承层次结构
中有一个相同的类,它们就可以用相同的方式利用多态性。
4.重载
方法重载是实现多态的另一个方法。通过方法重载,一个类中可以有多个具有相同名字的方法,由传递给它们的不同个数的参数来决定使用哪种方法。例如,对于一个作图的类,它有一个draw()方法用来画图或输出文字,我们可以传递给它一个字符串、一个矩形、一个圆形,甚至还可以再制定作图的初始位置、图形的颜色等。对于每一种实现,只需实现一个新的draw()方法即可,而不需要新起一个名字,这样大大简化了方法的实现和调用,程序员和用户不需要记住很多的方法名,只需要传入相应的参数即可。
因为类可以包含运算符如何运算的指令,所以可以把运算符用于从类实例化而来的对象。 我们为重载运算符编写代码,把它们用作类定义的一部分,而该运算符作用于这个类。也可以重载运算符,以相同的方式处理不同的类,其中一个(或两个)类定义包含达到这一目的的代码。
注意只能用这种方式重载现有的C#运算符,不能创建新的运算符。
5.消息和事件
对象之间必须要进行交互来实现复杂的行为。例如,要汽车加速,必须发给它一个消息,告诉它进行何种动作(这里是加速)以及实现这种动作所需要的参数(这里是需要达到的速度等)。一个消息包含三个方面的内容:消息的接收者、接收对象应采用的方法、方法所需要的参数。同时,接收消息的对象在执行相应的方法后,可能会给发送消息的对象返回一些信息。如上例中,汽车的仪表上会出现已达到的速度等。
在C#中,消息处理称为事件。对象可以激活事件,作为它们处理的一部分。为此,需要给代码添加事件处理程序,这是一种特殊类型的函数,在事件发生时调用。还需要配置这个处理程序,以监听我们感兴趣的事件。
使用事件可以创建事件驱动的应用程序,这类应用程序很多。例如,许多基于Windows的应用程序完全依赖于事件。每个按钮单击或滚动条拖动操作都是通过事件处理实现的,其中事件是通过鼠标或键盘触发的。本章的后面将介绍事件是如何工作的。