- 第3章 面向对象编程的基础知识
面向对象编程(Object-Oriented Programming)的缩写OOP,这是一种编程技术,最初是为了编写模拟程序而开发的。OOP很快就俘获了其他种类软件(尤其是涉及图形用户界面的软件)开发者的心。很快OOP就成为了业内一个非常重要的流行词。它被誉为具有魔力的银色子弹,可以使编程工作变得简单而愉悦。
当然,这种说法是明显的广告用语。要精通OOP,仍然需要学习和练习。不过它确实能确化某些编程任务,甚至能让编程变得更加有趣。本书将频繁讨论OOP,主要是因为Cocoa基于OOP概念,并且Objective-C是一种面向对象的语言
那么到底什么是OOP呢?OOP是一种编程架构,可构建由多个对象组成的软件。对象就好比存在于计算机中的小零件,它们通过互相传递信息来完成工作。本章我们将关注OOP的基本概念,研究可实现OOP的编程风格,并讲解一些OOP特性背后的原理,最后会全面介绍OOP的机制。
说明 OOP演变自20世纪60年代的Simula、70年代的Clascal以及其他相关语言。C++、Java、Python和Objective-C等现代编程语言都从这些早期语言中获得了灵感。
在研究OOP的过程中,你恐怕要做好迎接各种陌生技术术语的准备。OOP包含了很多听起来非常奇怪的术语,这会让人误以为它非常神秘并且难以理解,但事实并非如此。你甚至会怀疑计算机科学家们之所以创造了这些夸张的词汇只是为了向人们显摆他们有多聪明。
在讨论OOP之前,先来看看OOP的一个关键概念:间接(indirection,默然说话:感觉应该是封装,但是单词又是这样。。。。唉。。。。)。
- 间接
间接这个词看似隐晦,其实意思非常简单——在代码中通过指针获取某个值,而不是直接获取。下面举一个现实生活中的例子:你可能不知道自己最喜欢的比萨店的电话号码,但你知道可以通过查阅电话簿来找到它。使用电话簿就是一种间接的形式。
间接也可以理解为让其他人代替自己做某件事。假设你有一箱书要还给住在城镇另一头的朋友。而你的邻居今天要去你的朋友家,你就不必亲自开车横跨城镇把书给他,可以拜托邻居送那箱书。这就是另一种间接:让他人代替你自己去完成工作。
- 变量与间接
基本变量就是间接的一种实际应用。
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[])
{
NSLog(@"从1到5的数字");
for (int i=1; i<=5; i++) {
NSLog(@"%d\n",i);
}
return 0;
}
这个程序中有一个运行了5次的for循环,它使用NSLog()来显示循环每次运行的值。运行结果如下
-----------------------------------------------------------------------------------------------------------------------
2013-06-02 19:22:10.721 Count-1[3060:303] 从1到5的数字
2013-06-02 19:22:10.723 Count-1[3060:303] 1
2013-06-02 19:22:10.723 Count-1[3060:303] 2
2013-06-02 19:22:10.724 Count-1[3060:303] 3
2013-06-02 19:22:10.724 Count-1[3060:303] 4
2013-06-02 19:22:10.725 Count-1[3060:303] 5
------------------------------------------------------------------------
现在假设你想要更新这个程序,使其能输出1到10的数字。那么你必须修改代码,如下:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[])
{
NSLog(@"从1到10的数字");
for (int i=1; i<=10; i++) {
NSLog(@"%d\n",i);
}
return 0;
}
执行这样的简单修改显示不需要太多技巧,你可以通过简单的搜索替换操作来完成,而且只改2个地方,然而在比较庞大的(比如2万行代码)程序中,执行搜索和替换就必须非常谨慎了,这是因为代码中有些数字5是与循环次数无关的,所以不应该更改为10。
变量就是用来解决此类问题的。不需要在代码中直接修改循环的上限值(5或10),我们可以将这个数字放入某个变量中,通过添加一层间接来解决这个问题。使用变量之后,就是告诉程序“查看名为count的变量值,它会说明需要执行多少次循环”,而不是此前的“执行5次循环”。更改如下:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[])
{
int count=5;
NSLog(@"从1到5的数字");
for (int i=1; i<=count; i++) {
NSLog(@"%d\n",i);
}
return 0;
}
通过添加了这个变量,代码变得比之前更简洁了,而且也更易于编辑了,尤其是在其他编程人员需要修改此代码时。他们不必为了修改循环次数而仔细查看程序中使用的每个数字5,以确定是否需要修改。而只需要修改count变量的值。
- 使用文件名的间接
文件是间接的另一个示例。先看下面的代码(Word-length-1):
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[])
{
const char *words[4]={"aardvark","abacus","allude","zygote"};
int wordCount=4;
for (int i=0; i<wordCount; i++) {
NSLog(@"%s有%lu个字符",words[i],strlen(words[i]));
}
return 0;
}//main
for循环决定了每次处理的是words数据中的哪一个单词。循环里面的NSLog()命令通过%s格式说明符输出单词。%lu用于输出strlen()函数计算出的字符串长度的整数值。
运行之后的结果如下:
-----------------------------------------------------------------------------------------------------------------
2013-06-10 16:24:10.202 Word-Length[80669:303] aardvark有8个字符
2013-06-10 16:24:10.204 Word-Length[80669:303] abacus有6个字符
2013-06-10 16:24:10.204 Word-Length[80669:303] allude有6个字符
2013-06-10 16:24:10.205 Word-Length[80669:303] zygote有6个字符
-----------------------------------------------------------------------------------------------------------------
现在假设你希望使用另一组单词,那你就必须编辑源文件,将原始单词替换为新的单词。之后还要重新构建生成程序。如果程序是在网站上运行的,你还必须重新测试和部署程序才能升级至Word-length-2。
构造此程序的另一种方法就是将所有名字都移到代码之外的某个文本文件中,每行一个名字。没错,这就是间接。无需将名字直接放入源代码,而是让程序在其他地方查找这些名字。该程序从一个文本文件中读取一列名字,然后输出名字及其长度。
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[])
{
//第二版,使用文件间接,提高单词替换的效率,减少代码的修改
FILE *wordFile=fopen("/Users/mouyong/Documents/Objective-C/ch03/Word-Length/Word-Length/words.txt", "r");
char word[100];
while (fgets(word, 100, wordFile)) {
word[strlen(word)-1]='\n';
NSLog(@"%s有%lu个字符",word,strlen(word));
}//while
fclose(wordFile);
return 0;
}//main
输出结果如下:
-----------------------------------------------------------------------------------------------------------------
Joe-Bob "Handyman" Brown有25个字符
Jacksonville "Sly" Murphy有26个字符
Shinara Bain有13个字符
George "Guitar" Book有21个字符
-----------------------------------------------------------------------------------------------------------------
这是一个很出色的间接示例。你不用直接在程序中输入名字,采用这种方案之后,你可以随时更改单词集合,只需编辑文本文件,而不必修改程序。你可以尝试向word.txt文件中添加几个单词,然后重新运行程序。
这个方法要更好一些,因为文本文件更易于编辑,而不像源代码那样易受修改的破坏(默然说话:比如你在程序里输入字符串双引号,你就得写成\”,相当麻烦,而且容易写错。)。你可以让非编程人员使用文本编辑应用来编辑,让市场销售人员替你更新单词列表,这样你就有时间去处理更有兴趣的任务。
- 在面向对象编程中使用间接
OOP真正的革命性在于它使用间接来调用代码。不是直接调用某个函数,而是间接调用。只要理解了这一点,你就算掌握OOP的内涵了。其他一切都是通过间接产生的引申效果。
- 过程式编程
为了理解OOP的灵活性,我们先来看一下过程式编程(Procedual Programming),这样你就能明白OOP是为了解决哪些问题而创造出来的。过程式编程问世已久,它是编程的入门书籍和课程都会讲解的典型内容。诸如BASIC、C、Tcl和Perl等编程语言都是过程式的。
在过程式编程中,数据通常保存在简单的结构体中(例如C语言的struct)。还有一些较为复杂的数据结构,例如链表和树。当调用一个函数时,你将数据传递给函数,函数会处理这些数据。在过程式编程中经常会用到函数:你决定使用什么函数,然后调用它并传递其所需的数据。
1.绘制几何形状
Shapes-Procedural程序并不会真的在屏幕上绘制图形,而只是输出一些与该形状相关的文本信息。我们省略了绘图代码,以降低程序的复杂度,避免分散注意力,我们应集中精力编写一个可以以相同的方式处理多种元素的程序。
Shapes-Procdural程序中使用的是纯C语言和过程式编程方式。在代码的开始需要定义一些常量和一个结构体。下面是完整的代码:(默然说话:注意各函数的定义顺序不能错,没办法,C语言就是这样的。)
#import <Foundation/Foundation.h>
//形状类型
typedef enum{
kCircle,//圆形
kRectangle,//矩形
kEgg//蛋?!或者是椭圆?
}ShapeType;
//颜色类型
typedef enum{
kRedColor,//红色
kGreenColor,
kBlueColor
}ShapeColor;
//矩形,考虑绘制位置和大小
typedef struct{
int x;
int y;
int width;
int height;
}ShapeRect;
//形状,考虑绘制形状和颜色
typedef struct{
ShapeType type;
ShapeColor fillColor;
ShapeRect bounds;
}Shape;
//获得颜色名称字符串
NSString *colorName(ShapeColor color){
switch (color) {
case kBlueColor:
return @"蓝色";
case kGreenColor:
return @"绿色";
case kRedColor:
return @"红色";
default:
break;
}
}//colorName
//绘制矩形
void drawRectangle(ShapeRect bounds,ShapeColor color){
NSLog(@"在(%d,%d)处绘制一个宽%d高%d的%@矩形",bounds.x,bounds.y,bounds.width,bounds.height,colorName(color));
}//drawRectangle
//绘制圆形
void drawCircle(ShapeRect bounds,ShapeColor color){
NSLog(@"在(%d,%d)处绘制一个宽%d高%d的%@圆形",bounds.x,bounds.y,bounds.width,bounds.height,colorName(color));
}//drawCircle
//绘制椭圆
void drawEgg(ShapeRect bounds,ShapeColor color){
NSLog(@"在(%d,%d)处绘制一个宽%d高%d的%@椭圆",bounds.x,bounds.y,bounds.width,bounds.height,colorName(color));
}//drawEgg
//决定如何绘制的函数
void drawShapes(Shape shapes[],int count){
for (int i=0; i<count; i++) {
switch (shapes[i].type) {
case kCircle:
drawCircle(shapes[i].bounds,shapes[i].fillColor);
break;
case kRectangle:
drawRectangle(shapes[i].bounds,shapes[i].fillColor);
break;
case kEgg:
drawEgg(shapes[i].bounds,shapes[i].fillColor);
break;
default:
break;
}
}
}//drawShapes
int main(int argc, const char * argv[])
{
Shape shapes[3];
ShapeRect rect0={0,0,10,30};
shapes[0].type=kCircle;
shapes[0].fillColor=kRedColor;
shapes[0].bounds=rect0;
ShapeRect rect1={30,40,50,60};
shapes[1].type=kRectangle;
shapes[1].fillColor=kGreenColor;
shapes[1].bounds=rect1;
ShapeRect rect2={15,18,37,29};
shapes[2].type=kEgg;
shapes[2].fillColor=kBlueColor;
shapes[2].bounds=rect2;
drawShapes(shapes,3);
return 0;
}//main
在Foundation头文件的导入命令之后,首先是指定了两个枚举类型,用来规定我们的程序中会使用到的形状(圆,矩形和椭圆)和颜色(红绿蓝)。
//形状类型
typedef enum{
kCircle,//圆形
kRectangle,//矩形
kEgg//蛋?!或者是椭圆?
}ShapeType;
//颜色类型
typedef enum{
kRedColor,//红色
kGreenColor,
kBlueColor
}ShapeColor;
接下来定义了两个结构体,一个规定了绘制这个形状必须的几个属性。无论是圆,矩形还是椭圆,绘制它们都需要用到它们的坐标(默然说话:画在哪里),还有它们的宽和高(默然说话:画多大)。
//矩形,考虑绘制位置和大小
typedef struct{
int x;
int y;
int width;
int height;
}ShapeRect;
第二个结构体规定了绘制形状所需要的另外两个属性:类型和颜色。它将前面的所有内容结合了起来,整体地描述一个形状。
//形状,考虑绘制形状和颜色
typedef struct{
ShapeType type;
ShapeColor fillColor;
ShapeRect bounds;
}Shape;
2.接下来的工作
示例中接下来就是多个函数的声明,最后是main()函数。我们从main()开始吧,因为程序都是从main()入口的。下列代码声明了一个Shape的数组。并为Shape的每个属性都进行了初始化。下列代码为我们设计了一个红色的圆形、一个绿色的矩形和一个蓝色的椭圆形。
int main(int argc, const char * argv[])
{
Shape shapes[3];
ShapeRect rect0={0,0,10,30};
shapes[0].type=kCircle;
shapes[0].fillColor=kRedColor;
shapes[0].bounds=rect0;
ShapeRect rect1={30,40,50,60};
shapes[1].type=kRectangle;
shapes[1].fillColor=kGreenColor;
shapes[1].bounds=rect1;
ShapeRect rect2={15,18,37,29};
shapes[2].type=kEgg;
shapes[2].fillColor=kBlueColor;
shapes[2].bounds=rect2;
drawShapes(shapes,3);
return 0;
}//main
方便的C语言快捷操作 |
Shape-Procedural程序的main()方法中的绘图区域是通过C语言中一个小技巧声明的:声明结构体变量时,你可以一次性初始化该结构体的所有属性。 ShapeRect rect0={0,0,10,30}; 结构体中的属性按照声明的顺序取值。 使用这个技巧可以减少程序中需要输入的字符量,并且不会影响可读性。 |
|
数组初始化完成之后,main()调用了drawShapes()函数来决定如何绘制这些形状。
//决定如何绘制的函数
void drawShapes(Shape shapes[],int count){
for (int i=0; i<count; i++) {
switch (shapes[i].type) {
case kCircle:
drawCircle(shapes[i].bounds,shapes[i].fillColor);
break;
case kRectangle:
drawRectangle(shapes[i].bounds,shapes[i].fillColor);
break;
case kEgg:
drawEgg(shapes[i].bounds,shapes[i].fillColor);
break;
default:
break;
}
}
}//drawShapes
drawShapes()中的循环先检查数组中的每个Shape属于哪种类型(使用switch对比type属性),然后调用对应的绘制函数来绘制图形。如果是圆形,就调用drawCircle(),如果是矩形,就调用drawRectangle(),如果是椭圆,就调用drawEgg()。
//绘制矩形
void drawRectangle(ShapeRect bounds,ShapeColor color){
NSLog(@"在(%d,%d)处绘制一个宽%d高%d的%@矩形",bounds.x,bounds.y,bounds.width,bounds.height,colorName(color));
}//drawRectangle
//绘制圆形
void drawCircle(ShapeRect bounds,ShapeColor color){
NSLog(@"在(%d,%d)处绘制一个宽%d高%d的%@圆形",bounds.x,bounds.y,bounds.width,bounds.height,colorName(color));
}//drawCircle
//绘制椭圆
void drawEgg(ShapeRect bounds,ShapeColor color){
NSLog(@"在(%d,%d)处绘制一个宽%d高%d的%@椭圆",bounds.x,bounds.y,bounds.width,bounds.height,colorName(color));
}//drawEgg
这3个函数具体极相似的函数声明和函数体,传入两个参数:ShapeRect是决定绘制的位置和大小,ShapeColor决定了颜色。函数体仅使用NSLog()函数对位置、大小和颜色进行了输出。唯一需要注意的就是颜色的输出调用了一个转换函数colorName(),它完成了把ShapeColor枚举类型转换为NSString类型。
//获得颜色名称字符串
NSString *colorName(ShapeColor color){
switch (color) {
case kBlueColor:
return @"蓝色";
case kGreenColor:
return @"绿色";
case kRedColor:
return @"红色";
default:
break;
}
}//colorName
下面是Shape-Procedural程序的输出结果(省略NSLog()添加的时间和其他信息)
-----------------------------------------------------------------------------------------------------------------
在(0,0)处绘制一个宽10高30的红色圆形
在(30,40)处绘制一个宽50高60的绿色矩形
在(15,18)处绘制一个宽37高29的蓝色椭圆
-----------------------------------------------------------------------------------------------------------------
这一切看起来似乎无懈可击,但是,仔细思考一下你就会发现其中的问题。在使用过程式编程时,我们要花大量的时间连接数据和用来处理该数据的函数。还要非常小心的对各种不同的数据使用正确的函数。但其实我们很容易就会将矩形数据传递给了绘制椭圆的函数。
编写这种代码的另一个问题就是程序的扩展和维护变得很困难。假设现在我们要为程序添加一种新的形状:三角形。那我们就必须修改程序中至少4个不同的位置才能完成该任务(默然说话:天呀~十几年没写C了,现在突然注意到,C不仅仅要把代码写对,甚至连代码的位置也要放对。。。。为啥当初我会如此热爱这门语言?)
首先,我们要在ShapeType枚举中添加kTriangle常量。然后,编写看上去和其他函数一样的drawTriangle()函数。接下来,在drawShapes()的switch语句中增加一个新的case判断,来决定是否调用drawTriangle()函数。最后,为shapes数据增加一个三角形。别忘记更改shapes数组中增加形状的数量,另外drawShapes()函数的第2个参数也要修改哦。(默然说话:嗯,给点挑战吧?我不给修改后的代码了,下面仅有修改后的输出,看看你能不能改对?花多少时间把代码改对?)
让我们来看看修改后的Shapes-Procedural的运行结果:
-----------------------------------------------------------------------------------------------------------------
在(0,0)处绘制一个宽10高30的红色圆形
在(30,40)处绘制一个宽50高60的绿色矩形
在(15,18)处绘制一个宽37高29的蓝色椭圆
在(47,32)处绘制一个宽80高50的红色三角形
-----------------------------------------------------------------------------------------------------------------
添加对三角形的支持并不是很难,不过我们的小程序仅用于实现一种操作——绘制形状(其实应该是输出形状的文字信息)。程序越复杂、扩展起来就越麻烦。如果我们的程序不仅用于绘制各种形状,还必须能计算这些形状的面积,并判断鼠标光标是否位于这些形状中。在这种情况下,就必须修改每个对形状执行操作的函数,而修改过去正常工作的代码就意味着会引入新的错误。
另一种情况会使得代码变得更麻烦:增加的新形状需要更多的信息来描述。例如:圆角矩形。它除了坐标和宽高外,还需要圆角的半径。为了支持圆角矩形的绘制,你可能会在Shape结构体中增加半径属性,但是这会浪费空间,因为别的形状不需要这个属性,或者,你也可以使用C语言的联合体(unoin)来覆盖相同结构体中不同的数据布局,但是把各种形状融入联合体中并获取有用数据的过程也会使问题更加复杂。
OOP完美地解决了这些问题。等会儿我们在程序中使用OOP的时候,你就会看到OOP如何解决第一个问题——修改现有的代码来增加新的形状。
- 实现面向对象编程
过程式编程建立在函数之上,数据为函数服务,而面向对象编程则以程序的数据为中心,函数为数据服务。在OOP中,不再重点关注程序中的函数,而是专注于数据。
在OOP中,数据通过间接方式引用代码,代码可以对数据进行操作。不是通知drawRectangle()函数“根据这个形状进行绘制”,而是要求这个形状“绘制自身”。借助间接的强大功能,这些数据能够知道如何查找相应函数来进行绘制。
对象到底是什么呢?它其实就像C语言中的struct一样,神奇的是它能够通过函数指针查找与之相应的代码。
在Objective-C中,通知对象执行某种操作称为发送消息(默然说话:Java语言叫“方法调用”)。
- 有关术语
在深入研究下一个程序(Shape-Object)之前,先介绍一些有关面向对象的术语。
- 类(class)是一种表示对象类型的结构体。对象通过它的类来获取自身的各种信息,尤其是执行每个操作需要运行的代码。简单的程序可能仅包含少量的类,中等复杂的程序会包含几十个类。建议开发人员在用Objective-C编程时采用首字母大写的类名。
- 对象(object)是一种包含值和指向其类的隐藏指针的结构体。运行中的程序通常都包含成百上千个对象。指向对象的变量通常不需要首字母大写。
- 实例(instance)是“对象”的另一种称呼。比方说circle对象也可以称为Circle类和实例。
- 消息(message)是对象可以执行的操作,用于通知对象去做什么。在[shape draw]代码中,通过向shape对象发送draw消息来通知对象绘制自身。对象接收消息后,将查询相应的类,以便找到正确的代码来运行。
- 方法(method)是为响应消息而运行的代码。根据对象的类,消息可以调用不同的方法。
- 方法高度(method dispatcher)是Objective-C使用的一种机制,用于推测执行什么方法以响应某个特定的消息。我们将在下一章深入讨论Objective-C的方法调度机制。
- 接口(interface)是类为对象提供的特性描述。例如,Circle类的接口声明了Circle类可以接受draw消息。
- 实现(implementation)是使接口能正常工作的代码。
说明 接口的概念不只用在OOP中。例如C语言的头文件提供了库接口,比如标准I/O库(通过#include<stdio.h>获得)和数学库(通过#include<math.h>获得)。接口不提供实现代码的细节信息,也就是说你不必了解它是怎么实现的。
- Objective-C语言中的OOP
头痛吧?那是很正常的。因为这么多的新知识,你需要更多的时间去消化。不过,并不影响你继续学习,边学边消化吧,这是没关系的,你的大脑比你想象的还要坚强。
我们可以重新创建一个项目,但Shapes-Procedural中的枚举类型和结构体ShapeRect必须保留。下面是Shapes-Object的完整源代码。(默然说话:在敲下面的代码时我发现了一个Xcode 4.6的所谓bug,它会造成我们下面的代码产生一个误报:Must explicitly describe intended ownership of an object array parameter。如果你看到了这个错误信息,你可以选中左边的项目名,然后再选中右边的PROJECT-->Apple LLVM compiler 4.2 - Language-->Objective-C Automatic Reference Counting,它本来的值是Yes,将它的值改为No,就可以正常编译运行了。如果看不懂,可以将错误信息放到百度里进行搜索,可以得到更详细的说明)
//
// main.m
// Shapes-Object
// Shapes-Procedural的OOP版本
// Created by mouyong on 13-6-10.
// Copyright (c) 2013年 mouyong. All rights reserved.
//
#import <Foundation/Foundation.h>
//形状类型
typedef enum{
kCircle,//圆形
kRectangle,//矩形
kEgg,//椭圆
kTriangle
}ShapeType;
//颜色类型
typedef enum{
kRedColor,//红色
kGreenColor,
kBlueColor
}ShapeColor;
//矩形,不考虑绘制
typedef struct{
int x;
int y;
int width;
int height;
}ShapeRect;
//获得颜色名称字符串
NSString *colorName(ShapeColor color){
switch (color) {
case kBlueColor:
return @"蓝色";
case kGreenColor:
return @"绿色";
case kRedColor:
return @"红色";
default:
break;
}
}//colorName
//替代Shape的圆形类,最大特点就是数据与方法绑定了
@interface Circle : NSObject
{
@private
ShapeColor fillColor;
ShapeRect bounds;
}
- (void) draw;
- (void) setFillColor:(ShapeColor) fillColor;
- (void) setBounds:(ShapeRect) bounds;
@end//Circle类的声明
@implementation Circle
- (void) setFillColor: (ShapeColor) c{
fillColor=c;
}//setFillColor
- (void) setBounds: (ShapeRect) b{
bounds=b;
}//setBounds
- (void) draw{
NSLog(@"在(%d,%d)处绘制一个宽%d高%d的%@圆形",bounds.x,bounds.y,bounds.width,bounds.height,colorName(fillColor));
}
@end//Circle类的实现
//替代Shape的矩形类,最大特点就是数据与方法绑定了
@interface Rectangle : NSObject
{
@private
ShapeColor fillColor;
ShapeRect bounds;
}
- (void) draw;
- (void) setFillColor:(ShapeColor) fillColor;
- (void) setBounds:(ShapeRect) bounds;
@end//Rectangle类的声明
@implementation Rectangle
- (void) setFillColor: (ShapeColor) c{
fillColor=c;
}//setFillColor
- (void) setBounds: (ShapeRect) b{
bounds=b;
}//setBounds
- (void) draw{
NSLog(@"在(%d,%d)处绘制一个宽%d高%d的%@矩形",bounds.x,bounds.y,bounds.width,bounds.height,colorName(fillColor));
}
@end//Rectangle类的实现
//替代Shape的椭圆类,最大特点就是数据与方法绑定了
@interface Egg : NSObject
{
@private
ShapeColor fillColor;
ShapeRect bounds;
}
- (void) draw;
- (void) setFillColor:(ShapeColor) fillColor;
- (void) setBounds:(ShapeRect) bounds;
@end//Egg类的声明
@implementation Egg
- (void) setFillColor: (ShapeColor) c{
fillColor=c;
}//setFillColor
- (void) setBounds: (ShapeRect) b{
bounds=b;
}//setBounds
- (void) draw{
NSLog(@"在(%d,%d)处绘制一个宽%d高%d的%@椭圆",bounds.x,bounds.y,bounds.width,bounds.height,colorName(fillColor));
}
@end//Egg类的实现
//OOP方式的drawShapes
void drawShapes(id shapes[],int count){
for (int i=0; i<count; i++) {
[shapes[i] draw];
}
}//drawShapes
int main(int argc, const char * argv[])
{
id shapes[3];
ShapeRect rect0={0,0,10,30};
shapes[0]=[Circle new];
[shapes[0] setFillColor: kRedColor];
[shapes[0] setBounds:rect0];
ShapeRect rect1={30,40,50,60};
shapes[1]=[Rectangle new];
[shapes[1] setFillColor: kGreenColor];
[shapes[1] setBounds:rect1];
ShapeRect rect2={15,19,37,29};
shapes[2]=[Egg new];
[shapes[2] setFillColor: kBlueColor];
[shapes[2] setBounds:rect2];
drawShapes(shapes,3);
return 0;
}//main
- @interface部分
创建某个特定类的对象之前,Objective-C编译器需要一些有关该类的信息,尤其是对象的数据成员(即对象的C语言类型结构体应该是什么样子)及其提供的功能。可以使用@interface指令把这些信息传递给编译器。
说明 在Shapes-Object程序中,我们将所有内容都放在它的main.m文件里。而在大型程序中,则需要使用多个文件,每个类都有自己的文件。我们将在第6章介绍组织类和文件的方式。以下是Circle类的接口。
//替代Shape的圆形类,最大特点就是数据与方法绑定了
@interface Circle : NSObject
{
@private
ShapeColor fillColor;
ShapeRect bounds;
}
- (void) draw;
- (void) setFillColor:(ShapeColor) fillColor;
- (void) setBounds:(ShapeRect) bounds;
@end//Circle类的声明
第一行代码如下所示:
@interface Circle : NSObject
这句代码的意思就是告诉编译器:“这是新类Circle的接口,它继承了NSObject”。
声明完新类之后,我们将告诉编译器Circle对象需要的各种数据成员。
{
@private
ShapeColor fillColor;
ShapeRect bounds;
}
它们的意思和前面的Shape结构体是差不多的,只不过这里是类而已。在类声明中指定fillColor和bounds后,每次创建Circle对象,对象中都包含这两个元素。因此,每Circle类对象都将拥有自己的fillColor和bounds。fillColor和bounds的值称为Circle类的实例变量(instance variable)。
接下来的几行代码看起来有点像C语言中的函数原型。
- (void) draw;
- (void) setFillColor:(ShapeColor) fillColor;
- (void) setBounds:(ShapeRect) bounds;
在Objective-C中,它们称为方法声明(method declaration)。它们看起来很像是旧式的C语言函数原型,用于说明“这是我支持的功能”。方法声明列出了每个方法的名称、方法返回值的类型和某些参数。
我们从最简单的draw方法开始:
(void) draw;
前面的短线表明这是Objective-C方法的声明。这是区分函数原型与方法声明的一种方式,函数原型中没有先行短线。短线后是方法的返回类型,位于圆括号中。在我们的示例中,draw方法仅用于绘制图形,并不返回值。Objective-C使用void表示无返回值。
Objective-C方法可以返回与C函数相同的类型:标准类型(整数、浮点型和字符串)、指针、引用对象和结构体。
接下来的两个方法都带有一个参数,一定要注意它们的格式。首先是短线和返回值开头:
(void)
然后是方法名:
setFillColor:
注意结尾处的冒号是名称的一部分,它告诉编译器和编程人员后面还有参数。
(ShapeColor) fillColor
圆括号里指定的是参数类型,紧随其后的是参数名称。
注意冒号 |
注意,冒号是方法名称非常重要的组成部分。记住这样一个规则:如果方法有参数,则需要冒号,否则不需要冒号 |
|
接口介绍到此结束,接下来我们将编写代码,使该类能真正实现某些功能。
- @implementation部分
@interface部分,它用于定义类的公共接口。而真正使对象能够运行的代码位于@implementation部分中。
完整的Circle代码实现如下:
@implementation Circle
- (void) setFillColor: (ShapeColor) c{
fillColor=c;
}//setFillColor
- (void) setBounds: (ShapeRect) b{
bounds=b;
}//setBounds
- (void) draw{
NSLog(@"在(%d,%d)处绘制一个宽%d高%d的%@圆形",bounds.x,bounds.y,bounds.width,bounds.height,colorName(fillColor));
}
第一行代码中的@implementation是一个编译器指令,表明你将为某个类提供代码。类名出现在之后。结尾处没有分号。
接下来是各方法的定义。它们不必按照在@interface指令中的顺序出现。你甚至可以在@implementation中定义那些在@interface中没有声明过的方法。但是你要注意,Objective-C中不存在真正的私有方法,也无法把某个方法标识为私有方法,从而禁止其他代码调用它。这是Objective-C动态本质的副作用。
你们应该已经注意到了,在实现方法的时候,我们修改了变量名(fillColor变成了c,而bounds变成了b)。@interface和@implementation间的参数名不同是允许的。在这里,如果我们继续使用参数名fillColor或bounds,就会覆盖实例变量,并且编译器会生成警告信息。(默然说话:我试过,Objective-C似乎并没有this之类的对象,所以没办法,只能改名字。坑爹呀。)
在@interface部分的方法声明中使用名称fillColor只是为了告诉读者参数的作用。而在实现中,由于我们必须区分参数名和实例变量名,所以最简单的方式就是将参数改名。
最后一个方法draw。注意,方法名的结尾处没有冒号,说明它不使用任何参数。
- 实例化对象
我们最后要介绍的是Shapes-Object程序中非常关键的过程。在该过程中,我们创建了形状对象,比如红色的圆形和绿色的矩形。这个过程的专业术语叫做实例化(instantiation)。实例化对象时,需要分配内存,然后将这些内存初始化并保存为有用的默认值,这些值不同于通过新分配的内存获得的随机值(默然说话:给没学过C语言的同学补充下:C语言中分配内存就是分配内存,并不会象其他语言那样给已分配内存一个默认值,这些内存里会放个原来的某些莫名其妙的值,我们称它们为随机值)内存分配和初始化工作完成后,就意味着新的对象实例已经创建好了。
int main(int argc, const char * argv[])
{
id shapes[3];
ShapeRect rect0={0,0,10,30};
shapes[0]=[Circle new];
[shapes[0] setFillColor: kRedColor];
[shapes[0] setBounds:rect0];
ShapeRect rect1={30,40,50,60};
shapes[1]=[Rectangle new];
[shapes[1] setFillColor: kGreenColor];
[shapes[1] setBounds:rect1];
ShapeRect rect2={15,19,37,29};
shapes[2]=[Egg new];
[shapes[2] setFillColor: kBlueColor];
[shapes[2] setBounds:rect2];
drawShapes(shapes,3);
return 0;
}//main
为了创建一个新的对象,我们需要向相应的类发送new消息。该类接收并处理完new消息后,我们就会得到一个可以使用的新对象实例。
Objective-C具有一个极好的特性,你可以把类当成对象来发送消息。对于那些不局限于某个特定的对象而是对全体类都能用的操作来说,这非常便捷。最好的例子就是给新对象分配空间。
以下是main()函数的代码,它用于创建圆形、矩形和椭圆形。
main()函数中的代码显得有些稀奇古怪,这些不过是Objective-C的语法而已,让我们来仔细的分析一下:
这句代码完成了一个数组的声明,数组的类型是id,它是Objective-C中定义的可以指向任意类型的指针。
在Objective-C中,通知对象执行某种操作称为发送消息,也叫方法调用。[Circle new]、[shapes[0] setFillColor: kGreenColor]等等带中括号的代码,都是发送消息的语法。
中缀符 |
Objective-C有一种名为中缀符(infix notation)的语法技术。方法的名称及其参数都是合在一起的。例如,你可以这样调用带一个参数的方法: [circle setFillColor: kRedColor]; 带两个参数的方法调用如下所示: [textThing setStringValue:@”hello there” color:kBlueColor]; setStringValue:和color:都是参数名(事实上它们是方法名称的一部分,后面会详细介绍),@”hello there”和kBlueColor是被传递的参数。 这种语法和C不同,C语言调用函数时,是把所有的参数都放在函数名之后的小括号中。 中缀语法的优点就是它使代码的可读性更强,参数的用途更容易理解,缺点就是调用代码写得太长,显得比较罗嗦和麻烦。 |
|
代码里面最神奇的,就是drawShapes()函数,和以前的版本相比,它变得出奇的简单,一个循环,一个draw()函数的调用,程序就能完成正确的绘制,而不需要象以前那样去一一识别。逻辑一下变得非常的简单。对象真的能知道自己该做什么事情!
//OOP方式的drawShapes
void drawShapes(id shapes[],int count){
for (int i=0; i<count; i++) {
[shapes[i] draw];
}
}//drawShapes
- 扩展Shapes-Object程序
还记得我们在Shapes-Procedural程序中增加绘制三角形的功能吗?下面我们在Shapes-Object中也增加同样的功能。但是这次操作就简单多了,我们只需要创建一个新类,然后在main()函数中新添加一段初始化的代码,而无需象原来一样修改N个地方,一不小心改错就带来无尽的麻烦。(默然说话:Triangle类我就不贴出了,大家自己试试吧!)
Shapes-Object中的代码正好验证了面向对象编程大师Bertrand Meyer的开/闭原则。即软件实体应该对扩展开放,对修改关闭。遵循开/闭原则的软件会更加坚实耐用,因为你不必修改那些可正常运行的代码。
- 小结
本章指出了“函数第一,数据第二”这种观念导致的局限性。介绍了面向对象的编程 ,它坚持“数据第一,函数第二”的编程风格。
下一章我们将介绍继承,该特性可以让你充分利用现有对象的行为,编写更少的代码来完成工作。听起来很棒吧!
本人接受个人捐助,如果你觉得文章对你有用,并愿意对默然说话进行捐助,数额随意,默然说话将不胜感激。
