过程式程序建立在函数之上,数据为函数服务。面向对象编程从相反的角度来看待问题,它以程序的数据为中心,函数为数据服务。在OOP中,不再重点关注程序中的函数,而是专注于数据。

这听起来非常有趣,但它如何工作呢?在OOP中,数据通过间接方式包含对自身操作的引用代码。不是通知drawRectangle()函数“使用这个形状结构绘制矩形”,而是要求矩形“绘制自身”(天哪,听起来太荒谬了,但事实上却并不荒谬)。通过间接方式的威力,矩形数据知道如何查找相应的函数绘制图形。

那么,对象到底是什么呢?它其实是一种神奇的C struct。通常,该结构能通过函数指针查找与之相关的代码。图3-1展示了4种Shape对象:两个正方形、一个圆形和一个椭圆形。每个对象都能查找相应的函数并实现其绘图功能。

enter image description here

每个对象都有自己的draw()函数,知道如何绘制特定的形状。例如,Circle对象的draw()函数知道如何绘制圆形,Rectangle的draw()函数知道绘制由4条直线构成的矩形。

Shapes-Object程序(代码请参考03.10-Shapes-Object)可以完成与Shapes-Procedural相同的功能,但前者使用Objective-C的面向对象特性来实现。以下是Shapes-Object的drawShapes()代码:

enter image description here

该函数包含一个循环,用于输出数组的每种形状。在循环过程中,程序通知形状对象绘制自身。 注意区分该形式的drawShapes()与原始版本有何不同。首先,本函数更简短!代码不必询问每个形状的种类。

另一个不同点是函数的第一个参数shapes[],此刻它是一个id数组对象。什么是id?它是与大脑有关的心理学术语,用于描述天生的本能冲动和原始过程吗?在本例中并非表示此意,它代表identifier(标识符)。id是一种泛型,用于表示任何种类的对象。回忆一下,对象是带有代码的C struct。因此,id实际上是一个指针,指向其中的某个结构。在本例中,结构由各种形状构成。

drawShapes()函数的第三个变化是循环主体:

enter image description here

第一行看上去像普通的C语言。代码从shapes数组获取id(即指向某个对象的指针),并将其赋值给名为shape(它具有类型id)的变量。这只是一种指针赋值过程,它实质上并不会复制shape的全部内容。看看图3-2,注意Shapes-Object中各种可用的形状。shapes[0]是一个指向红色圆形的指针,shapes1是一个指向绿色矩形的指针,shapes2是个指向蓝色椭圆形的指针。

enter image description here

现在,我们看看函数的最后一行代码:

enter image description here

非常奇怪吧。这是怎么回事呢?我们知道,C使用方括号引用数组元素,但在此我们并没有使用数组实现任何功能。在Objective-C中,方括号还有其他意义:它们用于通知某个对象该做什么。在方括号内,第一项是对象,其余部分是你需要对象执行的操作。在本例中,我们通知名称为shape的对象执行draw操作。如果shape是圆形,我们会得到圆形;如果shape是矩形,我们会得到矩形。

在Objective-C中,通知对象执行某种操作称为发送消息(有些人也将其称为“调用方法”)。代码[shape draw]表示向shape对象发送draw消息。[shape draw]可以理解成“向shape发送draw”。至于形状如何实际绘制图形,则取决于shape的实现。

向对象发送消息时,如何调用必要的代码呢?这是通过幕后名为类的帮手来协助完成的。

请看图3-3。该图的左侧展示了shapes数组中索引为0的circle对象,该对象最近在图3-2中出现过。circle对象含有一个指向其类的指针。类是一种结构,用于描述该种类对象的构造。在图3-3中,Circle类含有一个指针指向用于绘制圆形、计算圆形的面积以及实现其他必要功能的代码。

enter image description here

类对象有什么用呢?就让每个对象直接指向它的代码不是更简单吗?确实是更简单一些,而且某些OOP系统也是那样做的。但是,拥有类对象会具备极大的优势:如果在运行时改变某个类,则该类的所有对象会自动继承这些变化(我们将在后面各章中进一步讨论该内容)。

图3-4展示了draw消息经过怎样的过程最终调用了circle对象中适当的函数。

以下是图3-4中展示的步骤。

(1) 对象是消息(图中的圆形)的目标,需要查询它,看看它属于什么类。

(2) Circle类浏览其代码,查找draw函数的位置。

(3) 找到draw函数后,将执行绘制圆形的函数。

图3-5展示了基于数组中的第二个形状(它是一个绿色的矩形)调用[shape draw]时的情况。

enter image description here

enter image description here

图3-5中使用的步骤和图3-4中的步骤几乎相同。

(1) 查询消息(图中的矩形)的目标对象,看看它属于什么类。

(2) Rectangle类查找其代码块,然后获取draw函数的地址。

(3) Objective-C运行可绘制矩形的代码。

该程序展示了一些非常棒的间接操作!在该程序的过程式版本中,我们必须编写代码来决定要调用哪个函数。现在,可以由Objective-C在幕后作出决定,它将查询对象属于哪个类。这可以降低调用错误函数的几率,同时使程序代码更易于维护。