对象、类和消息
这一章主要讨论对象、类和消息在Objective-C 语言中应用的原理和一些实现的方法。同时也介绍一下Objective-C的运行环境。
运行环境(Runtime)
Objective-C语言从编译、连接再到运行,完全采样动态延迟加载的方法。无论在任何时候,代码的运行都是动态执行的,比如说对象的创建、方法的调用等。Objective-C语言可执行的代码不仅要由编译系统来编译,还要有运行系统支持这些编译后的代码运行。这也就是说,代码的成功运行不但要有编译系统对源码的编译,更要有相应的运行环境来支撑代码的动态运行。运行系统其实相当Objective-C语言的操作系统,他负责代码的正常运行。你完全不需要与运行系统直接交互。如果对Objective-C的运行环境的内部具体功能和原来干兴趣,可以参照Apple公司的《Objective-C 2.0RuntimeProgramming Guide》。
对象(Objects)
就像它的名字所表达的一样,面向对象的程序都是围绕一些对象建立起来的。一个对象是由一些数据和一些操作组成,其中有些操作可以改变对象中的数据。Objective-C提供了一种数据类型,它可以存放任何对象而不需要区分对象的类型——这样就可以动态的定义对象了。在一个程序中,你尤其要注意当你使用完一个对象后一定要将对象从内存中释放。
对象基础知识
对象就是数据和一些操作(其中的一些操作可以更改数据)的联合。在Objective-C中,对象中的操作被叫做对象的方法(methods);对象中的数据叫做对象的实体变量(instance variables)。从本质上来说,对象就是将数据结构(instance variables)和操作流程(methods)绑在一起的独立编程单元。
举例来说,如果你正在编写一个画图程序,可以让用户在上面画一些直线,圆圈,长方形,文字和添加一些图标等,那么你就要为用户提供的基本图形,创建相应的类。就拿矩形来说,矩形的对象中就要有可以表示它在图形中所在位置以及它的宽度和长度实体变量,还要有其它的实体变量比如表示矩形是否被颜色填充,以及矩形的边框类型等。矩形对象还有一些方法用来设置矩形的位置,颜色,大小,填充状态和边框的类型以及如何将自己展现在图像中。
在Objective-C中,类的实体变量通常是类的内部私有的。如果你要访问一个类的对象的实体变量,只能通过对象的方法(你也可以对类的实体变量的访问权限进行设定,决定其类的子类对象或其它类的对象是否可以直接访问)。为了让其他人可以使用这些数据,你就要创建可以操作这些数据的方法。比如,一个矩形对象就要为矩形的大小和位置数据定义可访问的方法。
因此,一个对象从外看上去就好像这个对象只设计了方法;这些方法的执行与其它类型的对象没有关系。就像C语言函数体内的局部变量一样,在程序的其它地方这些局部变量是隐藏的,一个对象就是要隐藏它的实体变量以及方法的具体实现。
Id 类型
在Objective-C中,专门设计了用来存放对象标识的数据类型:id。这个类型就是对象的指针——更确切的说是一个指向对象的实体变量的指针,是对象自己特有的数据。就像C语言的函数或数组一样,对象的唯一标识是它的地址。所有的对象都属于id类型,不管它的实体变量和方法是如果定义的。
id anObject;
按照Objective-C的面向对象的设计思想,方法的返回值的默认类型使用id,替代了int(严格按照C语言的标准,方法的返回值的默认类型使int)。
关键字nil用来表示对象为空(null),也就是id类型的值为0。id,nil和其它的Objective-C的基本数据类的定义都定义的在头文件 objc/objc.h 中。
动态类型定义
id类型属于非限定性的类型。就其自身而言,存储在该类型中的对象变量根本不会提供任何有关该对象的信息,只能表示这是一个对象。
但是所有对象都是各不相同的。一个长方形对象的方法、实体变量和一个位图对像的方法、实体变量是不同的。有些时候,程序需要知道对象的更多信息——有哪些实体变量,有哪些方法可以执行,等诸如此类的信息。对于id类型的变量在编译的时候是没有能力向编译器提供具体的信息,只有程序运行的时候才能。
为了能在程序运行的时候区分id变量中对象的类型,每一个对象都会有个叫isa的实体变量用来表示这个对象是属于哪个类的。这样,矩形对象在系统的运行的时候就可以告诉系统它是矩形,圆的对象也会让系统知道它是圆。如果一些对象有相同的行为(method)和相同类型的数据(instancevariables),那么它们就是同一类的对象成员。
因此对象是在系统运行是动态确定类型的。在系统需要的时候,对象会告诉系统它是哪种类型的。(如果想对运行系统有更多的了解,请参考官方的《Objective-C 2.0 Runtime Programming Guide》)动态类型定义是Objective-C实现动态绑定的基础,我们稍后将讨论。
变量isa也让对象有了自省(introspection)功能——了解一些自己的信息(也可以发现其它对象的信息)。编译器会把类的定义信息记录到一个数据结构体中,供系统系统运行时使用。系统在运行时,通过实体变量isa发现对象的类定义信息。这样在系统运行的时候,就能判断是否一个对象实现了一个特定的方法,它的父类叫什么名字。
“类”将在稍后的“类(classes)”章节中具体的讨论。
还可以在源代码中用类的名称静态声明一个对象,这样编译器就能知道对象的类信息。类也是一种特殊的对象,并且类的名字可以当做声明类型的名称。具体的了解可参看“类类型”和“可静态行为”章节。
内存管理
在Objective-C编程中,当一些对象在运行环境中不再使用以后一定要确保将这些对象占用的内存资源释被放掉。这是非常重要的,否则你程序将占用的内存资源将越来越大比你预想的。同时也要确保在对象没有使用完的时候,不能将对象释放。
Objective-C 2.0提供了两种内存管理的方式:
• 引用计数 通过你对引用计数值的管理,来决定对象在什么时候结束自己的生命周期。“引用计数”在《MemoryManagement Programming Guide for Cocoa》文档中将有详细的阐述。
• 垃圾回收 对象内存释放由运行系统的一个自动的回收机制来管理。也就是说不需要人为的干预。“垃圾回收”在 《GarbageCollection Programming Guide》文档中有详细的阐述。
向对象发送消息
这一章主要介绍消息发送的语法,其中包括如何进行消息表达式的嵌套。同时也将讨论实体变量的“可见性”和多态与动态绑定的一些概念。
发送消息语法
为了能让对象做些事情,你要给对象发送消息,告诉它去执行一个它的方法。在Objective-C中发送消息表达式是用中括号[]括起来的:
[receivermessage]
receiver 是一个对象,message是告诉它要做什么。在源码中,message就是一个方法的名称和传给这个方法的一些参数。当发送消息时,运行系统会从接收消息的对象(receiver)的信息列表中选择合适的方法,并调用执行这个方法。
比如要发送一个消息让myRectangle对象去执行它的display 方法来显示自己:
[myRectangle display];
消息表达式的最后要由”;”结束,这和C语言的语法规则一致。
消息表达式中方法名称用来告诉运行系统要“选择”哪个实现方法。因此,信息中的方法名称也被称作“选择器”。
方法可以带些参数的。对于有唯一参数的消息会在选择器名称(信息方法名称)后面附加一个冒号(:),在冒号的后面传入参数值。这种结构就叫做关键字。一个关键字是由冒号结束的,在冒号后跟传入的参数。如下例所示:
[myRectanglesetWidth:20.0];
一个消息选择器的名称只包括关键字和冒号,不会再包括任何其它的信息,比如像返回值类型、参数的类型等。假如我们要发送一个信息给myRectangle 对象,将它的原点定在坐标系(30.0,50.0)的位置。
[myRectanglesetOrigin:30.0:50.0]; // 在多参数中这并不是好的例子
由于冒号作为方法名称的一部分,上面的例子中方法的名称应该是setOrigin::。由于它带有两个参数,因此要有两个冒号。但是第二个参数在方法定义时,没有带参数标签,因此方法名称并没有表达出第二个参数的含义,所以很难判断二个参数在方法中的意图和作用,降低了代码的可读性。
因此,方法的名称要交织着参数的含义,这样方法名称也就很自然地描述了参数的用途。比如,矩形类定义一个方法setOriginX:y:,这样的话对第二参数的意图和目的就非常的明确了。
[myRectanglesetOriginX: 30.0 y: 50.0];// 这是一个好的多参数信息的例子
重点:方法名称——选择器——的参数名称在使用时是不能随意更改的,并且顺序也不能任意的。“命名参数”和“关键字参数”通常带有一些暗含,方法的参数在运行的时候可以是多变的,可以有默认值,可以按不同的顺序,可以拥有附加的命名参数。但这些在Objective-C中是根本不可以的。 Objective-C方法的定义其实就是一个预先带有两个附加参数(具体参考《Objective-C 2.0 Runtime Programming Guide》)的简单C函数。这个和Python等语言是不同的: def func(a, b, NeatMode=SuperNeat, Thing=DefaultThing): pass 这样在调用的时候,Thing(和NeatMode)很有可能被忽略或者会有多个不同的值。 |
也可以让方法带有可变数量的参数,尽管他们很少被应用。这样的参数是在方法的名称后用逗号分隔。(不像冒号,逗号不算数方法名称的一部分)。在接下来的例子中,是向makeGroup:方法传入必须的参数(group)和其它三个可选的参数:
[receivermakeGroup:group, memberOne,memberTwo, memberThree];
方法也有返回值,像C语言的函数一样。接下来的例子中,为变量isFilled赋值,如果myRectangle 填充了颜色isFilled被赋值Yes,否则isFilled被赋值No。
BOOL isFilled;
isFilled = [myRectangleisFilled];
注意变量可以和方法的名字有相同的名字。
一个信息表达式可以嵌套到令一个信息表达式中。在这里,一个矩形的颜色可以赋给另一个矩形。
[myRectanglesetPrimaryColor:[otherRectprimaryColor]];
Objective-C2.0 还提供了点(.)运算符,用来方便对对象的属性方法(accessormethods)的调用。它在对象的属性调用中有很好的应用,具体参考“属性定义”和“点运算符”章节。
发送消息给nil
在Objective-C语言中,给nil发送消息是有可以的——不会给系统的运行造成影响。在Cocoa框架中有几种模式很好的利用了这个特点。发送给nil消息会返回有效的返回值:
• 如果一个方法的返回值是一个对象,那么将这个消息发送给nil,返回值是也是nil(0地址)。举个例子:
Person *motherInLaw =[[aPerson spouse] mother];
如果aPerson 的spouse是nil,那么也就是将mother发送给nil了,它的返回值也是nil。
• 如果一个方法返回一个任意的指针类型,或者是其指针的大小不大于sizeof(void*)的整型,float类型,double类型, long double类型, long long类型, 那么发送消息给nil将返回0。
• 如果一个方法返回一个结构类型,就像Mac OS X ABI Function Call Guide 在注册表中返回的值那样,那么将消息发送给nil,将返回一个每个域的值都为0.0的结构体。
• 如果方法中返回的类型除上诉的类型,那么发送消息给nil的返回类型将是一个未定义类型。
下面的代码片段中将说明发给nil信息的有效应用。
id anObjectMaybeNil = nil;
// this is valid
if ([anObjectMaybeNilmethodThatReturnsADouble] == 0.0)
{
// implementation continues...
}
注意:发送消息给nil的特性在Mac OS X v10.5版本上略有改变。 在MacOSXv10.4或以前的版本,向nil发送消息也是有效的,只要消息返回的是对象,任何类型的指针,void,或者类型的大小不大于sizeof(*void)的整型;只要是这些类型,向nil发送消息后,将返回nil。如果发送给nil一个返回的不是上诉的任何类型(比如结构类型、floating-point 类型或者是矢量类型)的消息,其返回值都是未定义的。因此发送给nil的信息返回值是不可信的,除非方法的返回值是对象、指针或者任何数据类型的大小不大于sizeof(*void)整型。 |
接收对象的实体变量
每个方法都可以自动地访问接收对象的实体变量。你不需要将这些实体变量当做方法的参数传给方法。就上面提到的primaryColor方法来说,虽然它没有参数,但可以找到otherRect对象的primaryColor实体变量值,并且返回这个值。每一个方法都可以接管对象和其内的实体变量,根本不需要为他们定义参数。
这种约定大大节省了很多的源码。这也是对面向对象编程思想一个很好的支持。将消息发送给接收对象就像将信件投递到你家里。消息用参数的形式将外部的信息发送给接收对象;接收对象根本没有必要将自己的信息以参数的形式再发给自己。
方法只能自动地访问接收对象的实体变量。如果方法需要用其它对象的实体变量信息,
就必须向那个对象发送消息,让它将其变量的信息获取出来。上面提到的primaryColor和
isFilled方法为此目的设置的。
”定义类”章节中有更多的关于实体变量的讨论。
多态(Polymorphism)
就像上面例子中提到的那样,在Objective-C中的发送消息与C语言中的函数调用的语法是等同的。但是,Objective-C中的方法是属于一个对象的,因此消息的行为要比C语言中的函数要多变的多。
尤其一个对象只能让它定义的方法来操控。不能将自己的方法与别的对象的方法相混淆,即使它们有相同的名称。这样将意味着同一个消息,由于发送给对象不同返回的结果也是不同的。比如,发送display 消息是每种类型的对象显示自己的唯一方法。发送相同的消息指令,由于圆和长方形不同,其在屏幕上显示的结果也不同的。
这个特性我们就称它为“多态”,在面向对象编程设计中扮演着非常重要的角色。结合动态绑定,它能保证你写的代码适用于任何类型的对象,而不需要你的代码在运行的时候去明确的指出应该使用哪个对象。这些对象甚至有可能是其他的程序员开发其它的程序所产生的。如果你写代码发一个display消息给id类型的对象,那个任何对象都可能是这个消息的接收者。
动态绑定(Dynamic Binding)
消息和函数调用最大的不同是函数在编译代码的时候就将函数与要输入的参数已经结合在一起了,但消息和接收对象是分离的,只有在程序运行的时候,才确定消息的接收对象是谁。因此,一个能响应消息的准确方法,只有在程序运行的时候才能确定,而不是在代码编译的时候。
一个消息要调用哪个方法需要依靠消息的接收对象。不同的接收对象对一个同名的方法(多态)有不同的实现。对于编译系统来说,如果要为消息找一个正确的实现方法就要知道这个消息的接收对象是谁——这个接收对象是属于哪个类的。而只有在系统运行时,将消息发送给哪个接收对象,哪个接收对象才将自己的定义信息显示出来。在源代码中通过类型定义是很难确定接收对象的。
只有系统运行的时候才能确定使用哪个实现的方法。当发送一个消息的时候,运行系统首先要查看消息的接收者是谁,然后寻找与消息中同名的具体实现的方法,最后在将接收对象的指针变量注入到方法中并调用它。(更多的运行机制,可参考《Objective-C 2.0 Runtime Programming Guide》)
方法与消息的动态绑定,结合多态为面向对象编程提供强有力的支持。一个程序的执行可能会有多个结果,不是因为消息本身,而是因为消息的接收对象的不同。每个对象都有自己版本的方法。这样程序在运行的时候,发送的接收对象是不确定的,具体哪个对象要依靠外部的因素变化,比如说用户的交换。
当你执行基于桌面的应用程序的时候,举例来说,用户要发送一些像粘贴、复制、还原等菜单命令消息给一些对象。消息会发送给任何可执行该命令的对象。一个用来文字展现的对象和一个图片展现的对象,在接受一个copy消息的结果是完全不同。一组图形对象的响应是和一个长方形对象的响应也是不同的。由于消息在执行的时候才会去选择实现的方法(方法实现不与消息绑定),所以产生这些的不同原因主要是由于每个响应消息的方法都是独立存在的。执行消息的代码没有必要将消息与这些方法联系在一起,甚至都不需要预先列举出这些方法。每个程序都可以创建适合自己的对象来响应copy消息。
Objective-C更进一步的动态绑定甚至可以将一个要发送的消息(方法名称)存到一个变量中,由运行系统来决定什么时候发送。这些讨论在《Objective-C 2.0 Runtime Programming Guide》中介绍。
动态方法解决
你可以在系统运行的时候用“动态方法解决”的办法提供一些自己实现的类和实例方法。在《Objective-C 2.0 Runtime Programming Guide》的动态方法解决有详述。
点(.)运算符
Objective-C提供了一个点(.)运算符让“访问方法”的调用更加的紧凑更加便捷,取代了以往使用的中括号[]。在你存取或修改另一个对象的属性时特别的有用(注意是另一个对象)。
使用点运算符
概观
你可以用点语法调用“访问方法”就像访问结构体中元素一样。如下:
myInstance.value = 10;
printf("myInstance value: %d",myInstance.value);
点运算符纯粹就是个“语法修饰(syntactic sugar)”——编译器还要将它转换为“访问方法”的调用(所以你并不是真正的对实体变量的访问),上诉例子的代码等价于:
[myInstancesetValue:10];
printf("myInstance value: %d",[myInstance value]);
一般应用
你可以用点(.)运算符去读取或写入一个属性。将在下面的例子中讨论:
Listing1-1 Accessing propertiesusing the dot syntax
Graphic *graphic =[[Graphic alloc] init];
NSColor *color =graphic.color;
CGFloat xLoc =graphic.xLoc;
BOOL hidden =graphic.hidden;
int textCharacterLength= graphic.text.length;
if (graphic.textHidden!= YES) {
graphic.text =@"Hello";
}
graphic.bounds =NSMakeRect(10.0, 10.0, 20.0, 120.0);
访问一个属性就是通过“访问方法”的获取方法获取一个属性(Property)(默认的获取方法名称就是属性的名称Property)和用设置方法为属性设置属性值(默认的设置方法名称是setProperty:)。你可以通过定义属性的语法,给一个属性改换“访问方法”(看“属性定义”章节)。尽管表面看上有些矛盾,但是点(.)运算符确实将实体变量包装起来——使你不能直接访问实体变量,实现了数据的封装。
下面的语句的和Listing1-1的语句在编译以后是相同的。但是用中括号的语法。如下:
Listing 1-2 访问属性用中括号语法
Graphic *graphic= [[Graphic alloc] init];
NSColor *color =[graphic color];
CGFloat xLoc =[graphic xLoc];
BOOL hidden =[graphic hidden];
inttextCharacterLength = [[graphic text] length];
if ([graphicisTextHidden] != YES) {
[graphicsetText:@"Hello"];
}
[graphicsetBounds:NSMakeRect(10.0, 10.0, 20.0, 120.0)];
使用点(.)运算符的最大的优点是如果你要修改一个只读的属性,编译器在编译时会产生一个错误信息使编译失败;而使用中括号[]运算符,当你调用一个没有定义的setPorperty:方法时,编译器顶多只会给个“所调动的方法没有定义”的警告,只有在系统运行时才会发现这个错误。
如果属性类型是C语言的基本类型,那么也可以对属性进行算术运算符的复合运算。举例来说,你可以用复合运算更新NSMutableData 实体对象的length属性。
NSMutableData *data = [NSMutableData dataWithLength:1024];
data.length += 1024;
data.length *= 2;
data.length /= 4;
上诉代码等效于:
[data setLength:[data length] + 1024];
[data setLength:[datalength] * 2];
[data setLength:[datalength] / 4];
有一种情况,属性是不宜使用的。请考虑一下,下面的代码:
id y;
x = y.z; // z是一个没有定义的属性
y没有类型定义,当然z也就是不存在的了。对这种情况,有几方面考虑。一个是由于这样写很含糊,这段代码就是属性未定义的错误代码。也可能是当前的编译单元中有对z属性有唯一的定义,那么它就不再是模糊不清的,也就成有意义的。如果对z属性进行多重定义,只要他们定义的z属性的数据类型是相同的(比如都是BOOL类型),那么它也是合理的。还有就是如果z是定义只读的,那么这个代码同样会让人产生歧义。
nil 值
如果在属性遍历过程中遇到属性值是nil,那么就和给nil发送消息的结果是一样的。举例来说,下面的每一对代码都是等效的:
//each member of the pathis an object
x=person.address.street.name;
x =[[[person address]street] name];
// the pathcontains a Cstruct
// willcrash if window isnil or -contentView returns nil
y =window.contentView.bounds.origin.y;
y =[[window contentView]bounds].origin.y;
// an example ofusing asetter....
person.address.street.name= @"Oxford Road";
[[[personaddress] street]setName: @"Oxford Road"];
self
在类的方法定义中,如果想通过“访问方法”来访问该类中的实体变量,那么就要明确的使用self,如下代码:
self.age = 10;
如果你不使用self,那么你将直接访问实体变量,在下面的例子中,age的设置方法将不会别调用。
age=10;
性能和线程
用点运算符的代码和基本的方法调用的语法产生的代码是等效的。因此,用点运算符写的代码和调用“访问方法”是一样的。由于点运算简化了方法的调用,没有额外的线程依赖,这是被推荐使用的主要原因。
点语法和Key-Value Coding
Key-Value Coding(KVC)定义了访问属性的方法—valueForKey:和setValue:forKey:—用字符类型的键值来代表属性。KVC不仅仅是对“访问方法”的替代——它还有它特有的用处,因为代码可能在编译的时候并不知道属性的名字。
Key-Value Coding 和点运算符是交叉应用的技术。你可以用KVC不管你是否使用点运算符,你也可以使用点运算符不用去关心KVC。尽管它们都是使用了点运算符。在KVC应用中,点语法用来分隔键值(属性名称)的路径符。有一点要记住的是,使用点运算符访问属性其实都是在调用接收对象的“访问方法”(需要重点强调的是,点运算符在KVC中并不是对valueForKey: 或者setValue:forKey:方法的调用)。
你可以使用KVC方法存取一个属性,举例个例子,一个类的定义如下:
@interface MyClass
@property NSString*stringProperty;
@property NSIntegerintegerProperty;
@property MyClass*linkedInstance;
@end
你可以访问对象的属性用KVC。
MyClass *myInstance =[[MyClass alloc] init];
NSString *string =[myInstancevalueForKey:@"stringProperty"];
[myInstancesetValue:[NSNumber numberWithInt:2]forKey:@"integerProperty"];
为了进一步阐述点运算符与KVC键值路径存取属性的用法的区别,请参看下面的代码:
MyClass *anotherInstance= [[MyClass alloc] init];
myInstance.linkedInstance= anotherInstance;
myInstance.linkedInstance.integerProperty= 2;
这个和下面的代码的结果是一样的
MyClass *anotherInstance= [[MyClass alloc] init];
myInstance.linkedInstance= anotherInstance;
[myInstancesetValue:[NSNumber numberWithInt:2] forKeyPath:@"linkedInstance.integerProperty"];
应用总结
aVariable=anObject.aProperty;
调用一个属性的访问方法,并将返回值赋给一个变量。属性aProperty的类型一定要和赋值的变量aVariable类型相匹配,否则编译器发出警告。
anObject.name =@"New Name";
调用anObject对象的setName方法,并将@”NewName”作为参数传给方法。
如果setName:方法不存在、name属性不存在或者setName:方法的返回值不是void,编译器都将会发出警告。
xOrigin = aView.bounds.origin.x;
调用bounds访问方法,并将结构体NSRect类型的origin中的x通过bounds访问方法的返回值赋给xOrigin。
NSInteger i = 10;
anObject.integerProperty= anotherObject.floatProperty= ++i;
将11同时赋给anObject.integerProperty和anotherObject.floatProperty。将预先算好值作为参数分别传给setIntegerProperty和setFloatProty方法。
错误的使用
下面的几种情况,强烈不建议使用:
anObject.retain;
在编译的时候会产生一个警告((warning:value returned from property not used.)
/* methoddeclaration */
- (BOOL)setFooIfYouCan: (MyClass*)newFoo;
/* code fragment*/
anObject.fooIfYouCan= myInstance;
会产生一个编译警告,setFooIfYouCan:似乎不能作为赋值(setter)方法,因为其返回值不是(void)。
flag =aView.lockFocusIfCanDraw;
如果flag的类型与lockFocusIfCanDraw返回值不匹配,编译器将会发出警告。
/* propertydeclaration */
@property(readonly) NSIntegerreadonlyProperty;
/* methoddeclaration */
- (void)setReadonlyProperty:(NSInteger)newValue;
/* code fragment*/
self.readonlyProperty = 5;
由于属性是定义为只读的,这个代码将会产生一个编译警告(warning: assignment to readonlyproperty 'readonlyProperty')。它仍然可以正常运行,只是简单的认为还有一个属性赋值的方法,尽管属性没有设置为readwrite。
类
面向对象编程的一个典型的特点是程序是由各式各样的对象构成。一个基于Cocoa框架的程序有肯能要使用NSMatrix对象,NSWindow对象,NSDictionary对象,NSFont对象,NSText对象,还有其它的对象。程序可能需要用到一个类的多个对象——比如,多个NSArray对象,多个NSWindow对象。
在Objective-C中,对象是通过已定义的类来定义的。类是对象的原型。类声明的实体变量会成为它的所有对象的自有部分,它定义的方法会被它所有的对象共用。
编译器会为每一个类产生唯一一个用来表示这个类的对象——类对象(class object),一个“类对象”知道如何创建一个属于这个类的对象。(正式因为这样,通常管它叫做“工厂对象”)“类对象”是类编译后的版本;它所创建的对象都是类的实例。程序中工作的对象都是由“类对象”在运行时产生的实例。
同一个类的所有实例都有相同的方法,相同模式的实体变量。每一个对象拥有属于它自己的实体变量,但是方法是公用的。
按照惯例,类的名称首字母大写(如:Rectangle);类的实体(对象)的名称首字母要小写(如:rectangle)。
继承
类的定义是一个累积过程;每一个你定义的新类都是基于一个原有的类,通过这个类,你可以将它的实体变量和方法继承过来。新的类只是在它继承的实体变量和方法基础上简单的添加或更改,完全不需要将继承的代码重新写一遍。
将所有继承的类联系在一起,就会形成一个具有树结构(只有一个根节点)的继承层次结构。当你写的代码基于Foundation架构时,那么根类就是NSObject。每一个类都有父类(除了根类),并直接可以向上归宿到根类,同时每一个类都可以作为其它类的父类,这样就可以由根类不断向下扩展。Figure 1-1 中将阐述画图程序中几个类的继承关系。
Figure1-1 Some Drawing Program Classes
这个插图,展现了Square类是Rectangle类的子类,Rectangle是Shape的子类,Shape又是Graphic类的子类,最后Graphic类是NSObject类的子类。继承是累积的。所以Square类拥有Rectangle类、Shape类、Graphic类、NSObject类的所有实体变量和方法,就好想这些都是自己定义的一样。简单地说Square类不只是Square类,它还是Rectangle类、Shape类、Graphic类、NSObject类。
每一个类(除了NSObject)都可以看作是其它类的特例或是改版。每个连续继承下来的子类,觉大多数是将继承下来的再进行修改。Square类只有极小的一部分是自己定义的,大部分还是从Rectangle继承下来的。
当你定义一个类的时候,你要对你要继承的父类声明,使你定义的类能融入到继承的层次结构中。你定义类一定是某个类的子类(除非你要定义一个根类)。有很多的类可以当做父类使用。拥有NSObject类的Cocoa和其它几个框架总共定义了250多个类。程序中有些类是你直接从程序框架中拿过来使用的,还有些类为了适合你自己的需要自己定义的。
一些框架已经为你准备好了几乎所有你需要的类,但是也有些细节需要你通过子类继承来实现。因此你可以写非常少的代码实现非常复杂的类,其实这非常复杂的类的大部分代码已由框架工程师完成。
NSObject类
NSObject类是根类,所以它没有父类。它为所有的Objective-C对象及对象之间的交互定义了一个基本的框架结构。它让所有继承它的“类对象”和类的实例具有对象该应有的基本功能,以来配合系统的运行。
一个类即使你不需要继承任何其它类,但是一定要成为NSObject的子类。你定义的类的实例一定要有像Objective-C其它类在运行时所具有的特性。从NSObject类继承这些特性比你从新定义个类更加简单方便。
注意:实现一个新的根类是一个很烦杂并且还会遇到很多的障碍的工作。这个类必须要重新写很多的NSObject类已经写过的代码,比如,为实例对象分配空间,将它们与它们的类建立连接,和在系统运行时需要唯一验证。所有,你应该用Cocoa框架中的NSObject类作为基类。要想了解NSObject类和NSObject协议 的更多信息可以参考Foundation框架的文档。 |
继承实体变量
当一个类创建一个实例时,这个实例中不仅包含为它定义的类中的实体变量,还包含它的类的父类中的实体变量,以及父类中父类的,最终追述到根类。因此,定义在NSObject中的实体变量isa将成为任何一个类的实体变量。isa是实例和类的连接纽带。
Figure1-2 展现了Rectangle类中的实体变量的详细实现和这些实体变量的来源。你会注意到在Rectangle类中定义的实体变量被加到Shape类定义实体变量中,Shape类中的实体变量又加到Graphic类中的实体变量,以此类推。
Figure 1-2 Rectangle Instance Variables
一个类不用必须定义自己的实体变量。如果它在定义一个方法时,需要实体变量,它可以使用它继承的类的实体变量。举例来说,Square类就不需要任何属于自己的实体变量。
继承方法
一个对象不仅可以访问自己定义的方法,还可以访问它的父类的方法,以及父类的父类的方法,直到访问它的根类中的方法。比如,一个Square对象可以访问Rectangle类、Shape类和NSObject类中的方法,就像访问自己的类中定义的方法一样。
一个新定义的类可以利用它的父类代码。这种方式的继承对面向对象编程极为有利的。当你使用Cocoa框架面向对象编程时,你就可以充分利用框架中的基本功能。你唯一要写的就是为你自己程序需要的特定功能。
“类对象”也有自己的继承体系结构。但是由于他们没有实体变量,它们只是继承了一些方法。
方法覆盖
这是继承中的一个有用的特殊情况:当你定义一个新的类时候,你可以定义一个与继承结构中某个类的方法同名的新方法。这个新方法覆盖了原方法;这个新类的实例将执行这个新的方法而不是原来的那个。并且这新类的子类也不会执行那个原来的方法。
举例来说,Graphic类中的display方法被Rectangle类自己写的display方法所覆盖。那么Graphic的display方法对所有的它的子类对象还是有效的,除了Rectangle对象。这个方法被Rectangle的自己的display方法代替执行。
尽管新定义的方法阻挡了继承来的原方法的使用,但是定义在新类中的方法还是可以跳过新定义的方法,使用原方法的。(具体学习参考“向self和super发送消息”章节)
新定义的方法可以引用它覆盖的方法。如果这样做,那么新的方法将是对它覆盖的方法的改造或是修改,而不是那种完全的覆盖。当继承层次结构中的每个类都定义了同名的方法,并且每个新方法都引用了它覆盖的方法,那么这个方法的实现将涉及到所有的类。
尽管子类可以覆盖它所继承的方法,但是他们是不能覆盖实体变量的。由于每个对象都为它所继承的实体变量分配一个内存空间,所以你就不能用同名的新实体变量覆盖原有的实体变量(同名的变量不能在同一个作用域中定义两次),如果那样做的话,编译器会报错。
抽象类
有些类的设计就是用来为其它的类继承的。这些抽象类将其子类通用的方法和实体变量放在一起。抽象类对自身来说是一个半成品,但是它所包含的代码减少了子类的代码实现(因为抽象类一定要有子类的继承,因此又被叫做抽象父类。)
Objective-C并不像其它的语言,有标记某个类是抽象类的特定语法,也不会阻止你对抽象类实体化。
在Cocoa中NSObject就是一个典型的抽象类。在应用中你从来没有用过NSObject的实例——它没有任何意义。它只是一个类,在实现的细节上什么也没有做。
NSView类是另一个抽象类,偶尔你也可以直接使用它的实例。
抽象类中的代码,通常用来定义应用的架构。当你实现这些类的子类时,这些新类的实例将非常容易的契合到应用架构中,并且彼此之间轻松自如的协调工作。
类 类型
类为所有的对象定义了规范。类实际上就是一个数据类型的定义。这种类型的定义不仅仅是定义了数据结构(实体变量),还定义了行为(方法)。
类的名称可以出现任何在C语言的类型可出现的地方——举例来说,可以将类名作为sizeof函数的参数:
inti = sizeof(Rectangle);
静态类型定义
你可以用类的名称替代id去指定一个对象的类型:
Rectangle *myRectangle;
因为这样定义一个对象可以让编译器知道它是哪种类型的对象,因此称这种定义为“静态类型定义”。就像id被用来定义一个指向对象的指针一样,静态定义的对象的指针指向一个类。这些对象指针都是有型别的,静态类型定义使指针指向的类型非常明确,不像id将这些信息隐藏起来。
静态类型定义允许编译器进行类型检测——举例来说,将一个消息的返回值赋给一个静态类型定义的对象时,如果返回值的类型与对象不匹配,编译器将会发出警告——而将返回值赋给id声明的变量将会没有这样的约束。另外,你也可以让别人能看懂你写的代码的目的。然而,它对动态绑定没有冲突,也不会改变系统运行时的动态类型判定。
一个对象可以静态类型定义为它自己的类或者它的类所继承的类。举例来说,由于继承的关系,Rectangle被看做是Graphic的一个类型,因此Rectangle实例可以静态定义为Graphic类。
Graphic*myRectangle;
这么做的原因是Rectangle是一个Graphic。尽管它是一个Graphic,但是它还有比Graphic还多的从Shape和Rectangle继承来的实体变量和方法。出于类型检测的目的,编译器将myRectangle看做是Graphic,而在系统运行的时候再确定它是Rectangle。
在“可静态行为”章节中对静态定义和它的特性有更多的介绍。
类型自省
类的实例在系统运行的时候可以知道自己是属于哪个类型的。定义在NSObject类中的isMemberOfClass:方法就可以检测它所发送消息的对象是哪个类型的实例:
if ([anObject isMemberOfClass:someClass] )
...
在NSObject类还定义了一个方法——isKindOfClass:具有更普遍的应用,用来检测它所发送消息的对象类型是否是某个类或则是它是某个类的子类实例:
if ([anObjectisKindOfClass:someClass] )
...
任何让isKindOfClass:消息返回Yes的对象,都可看做是他们用某个类及其子类静态定义的。
自省并不局限于显示一个对象类型,本章的后章节还会讨论通过“类对象”返回的方法,判断是否可以向一个对象发送消息,同时还可以知道其它的一些信息。
在Foundation框架中的NSObject有更多关于isKindOfClass:和isMemberOfClass:及其相关的类的介绍。
类对象
类的定义包含了大量的信息,大多数是关于类的实例的:
• 类的名字及其父类的名字
• 对实体变量的样本描述
• 方法的名称、方法的返回值、方法的参数声明
• 方法的实现
这些信息被编译并保持在数据结构体中,在系统运行时使用。编译器只创建唯一个用来代表这个类的对象——叫做“类对象”。“类对象”可以获取关于这个类的所有信息,也就是它可以了解一些实例的类信息。它可以去创建一个新的实例按照类预先定义的那样。
尽管一个“类对象”保持着一个类实例的原型,但它本身并不是一个类实例。它没有属于自己的实体变量,也不能执行类实例中的方法。然而,在定义类时,却可以专门为“类对象”定义一些方法——这些方法叫做“类方法”,它与类实例的方法相对应。一个“类对象”会从父类中继承“类方法”,就像类的实例可以继承实例方法一样。
在源代码中,“类对象”用类的名称代表。在如下的例子中,Rectangle类将返回一个类的版本号,用从NSObject类继承来的类方法version:
int versionNumber =[Rectangle version];
类名只能在消息表达式中作为“类对象”的替代。你也可以通过类的实例来获取一个“类对象”的id。这两种方法都可以获取类的信息:
id aClass = [anObject class];
id rectClass = [Rectangle class];
就像这例子展示的那样,所有的“类对象”都可以赋值给id类型。但是对象类型也有它独有的类型Class:
Class aClass = [anObject class];
Class rectClass = [Rectangle class];
所有的“类对象”都是Class类型,将类对象声明为Class类型,就相当于用类的名称来静态定义一个实例。
“类对象”与生俱来就是一个对象,它可以进行动态定义、发送消息、从其它的类继承方法等操作。它们的特别之处在于它们是由编译器创建的,和其它由类定义的实例相比没有属于自己的数据结构(实体变量),并且在系统运行时,它们作为创建实例的代理。
注意:编译器同时还会为每个类创建一个“metaclass 对象”。它用来描述“类对象”,就像“类对象”描述类的实例一样。但是当你发送信息给类的实例或者“类对象”时,metaclass对象 只会作为运行系统的内部应用。 |
创建实例
“类对象”一个首要的功能是创建类的实例。下面的代码就是让Rectangle类去创建一个新的Rectangle类实例并将它赋给myRectangle变量。
id myRectangle;
myRectangle = [Rectanglealloc];
alloc方法用来为新的对象的实体变量分配内存空间,并将所有的实体变量初始化为0——除了变量isa(用来联系新的实例和类)。如果能对一个对象进行更加全面具体的初始化,那么对于这个对象是更加有意义的。实现这个功能要用init方法。在内存分配后紧接着进行初始化。
myRectangle = [[Rectangle alloc]init];
在我们让myRectangle接收任何消息之前,写上如上代码是非常必要的。alloc方法返回一个新的实例,实例再执行init方法去初始化它的状态。每个类至少有一个方法(如,alloc)用它来创建一个实例,还至少要有个方法(如,init)用来进行初始化。有的初始化方法带有参数,可以将一些特殊的值传给初始化方法,并且用关键字去标注参数(initWithPosition:size:,它就是一个用来创建一个新实例的初始化方法),像这样的方法名称都是以init开头。
用类对象定制创建
Objective-C将类当成对象,这并不是什么奇思妙想。这么做都是出于对设计的需要。比如说,你可以用类来定制一个对象,只要这个类在这个地位都是没有使用限制的。比如,在桌面应用程序中,有一个NSMatrix对象,它可以用来定制各种各样的NSCell对象。
NSMatrix对象有创建代表自己单元对象的责任和义务。在矩阵(matrix)初始化以后,需要一个新的单元格时,就会去创建。可见的矩阵也就是NSMatrix对象在程序运行时,可以放大或收缩,也可能与用户进行交互。当它在放大的时候矩阵就要不断的填充新的单元。
但是要填充哪一类型的单元对象呢?每一种矩阵只会展现一类NSCell,但是这有不同种类的NSCell。Figure1-3展现了由桌面程序提供的继承体系结构。所有的继承都来自于NSCell类:
当一个矩阵创建NSCell对象时,是创建用来展现按钮或者开关的NSButtonCell对象呢?还是一些用来填入或修改文本的NSTextFieldCell对象呢?还是一些其它的NSCell对象呢?NSMatrix必须考虑到任何类型的单元,甚至还包括没有创建出来的。
解决这个问题的一个办法是,将NSMatrix当成一个抽象类,当需要用它的时候,声明一个它的子类,子类实现创建新单元格的方法。由于子类各自实现这个方法,所有使用这些子类的用户一定要非常明确这个类所生成的单元对象就是他们所需要的。
但是这样做,会让本应该NSMatrix类做的事情却让别人做了,并且类的数量会不断的增多。由于程序不会只需要可以创建一种NSCell的NSMatrix类,这样程序对NSMatrix子类的使用会异常的复杂。每当你创建一个新的NSCell类型时,你都要创建一个新的NSMatrix子类。而且很有可能其它程序的程序员也应将创建了,完成了这项工作。所有这些问题交织在一起导致了对NSMatrix使用的失败。
一个更好的解决方法,也是非常适合NSMatrix使用的方法,就是允许NSMatrix对象用NSCell的“类对象”定制创建单元。它定义了一个setCellClass:方法,可以将各种它用来为NSMatrix填充的NSCell对象的“类对象”作为参数传入。
[myMatrixsetCellClass:[NSButtonCell class]];
当NSMatrix被初始化以后就可以用NSCell类对象去创建新的单元,并且在任何时候都可以调整自己,让自己包含更多的单元。要是没有类对象,也就不会将类当做参数传给消息并赋给一个变量,定制化创建将是非常困难的事情。
变量和类对象
当你定义一个新的类时,你就要定义实体变量。每一个类的实例都拥有一份属于它自己的实体变量副本——也就是每个实例都控制属于自己的数据。这里所提到的是实体变量,而不是“类变量”。类对象只可以使用记录了类定义信息的结构体数据。除此之外,类对象是不能访问任何的类实例中的实体变量的;也就是不能初始化,读取或修改实体变量。
如果想让一个类的所有实例都共享数据,你就要以某种形式定义类的外部变量。最简单的方法就是在类的实现文件中定义一个变量,就像下面的代码:
int MCLSGlobalVariable;
@implementation MyClass
// implementation continues
一个更加巧妙的实现方式是定义一个静态的变量,并且用类方法去管理它。将一个类设定为静态变量后,它的使用范围只能限定在这类中——也就是这个类的实现文件部分(不像实体变量,静态变量不能被继承,也不能被子类直接使用)。这种模式通常是用来定义一个共享的类实例(比如说单例模式,具体参照《Cocoa Fundamentals Guide》中的单例模式)。
static MyClass *MCLSSharedInstance;
@implementation MyClass
+ (MyClass *)sharedInstance
{
// check for existence of shared instan
// create if necessary
return MCLSSharedInstance;
}
// implementation continues
静态变量给“类对象”增加了很多的功能,让“类对象”不再仅仅是一个创建实例的“工厂”;这样它凭借着自己,使自己更加接近地成为一个完备和通用的对象。一个“类对象”可以用来调整它所创建的实例,从已创建的实例列表中分发实例,或者管理一些应用程序必不可少的流程。在这种情况下,当你需要一个类的唯一实例时,你可以将所有的对象状态放入静态变量中,只使用“类方法”。这样就省去了内存分配和初始化的步骤。
注意:除了使用静态变量还可以使用没有用static标识的外部变量,但是使用有使用范围限制的静态变量,更能体现数据独立封装的特性。 |
初始化一个类对象
如果你想使用一个“类对象”,除了给类对象分配内存,还要给类对象初始化,就像你使用其它的实例一样。尽管程序不会为“类对象”分配内存空间,但是Objective-C确实提供了为“类对象”分配内存的方法。
如果一个类充分的利用了静态变量或者外部变量,那么initialize方法确实是一个设置初始值的好地方。举例来说,一个类要维护一个实例数组,那么在initialize方法中,就可以建立一个数组,并且在数组中放入一两个创建好的实例作为默认使用。
运行系统会给每个“类对象”发送一个initialize消息,在类接收任何消息之前,其父类接收initialize消息之后。这就给类一个机会在被使用之前设置它的运行环境。如果没有初始化的需要,你就不需要写initialize方法作为消息的响应。
由于继承的关系,如果将initialize发送给一个没有实现initialize方法的类,那么它的父类会接收initialize消息,尽管父类已经接收过initialize消息。举个例子,类A实现了initialize方法,类B继承了类A但是没有实现initialize方法,当类B接收任何消息之前,运行系统会给B发送一个initialize消息。但是由于类B没有实现initialize方法,那么类A的initialize方法将被代替执行。所以类A要确保它的初始化逻辑在适当的时候只执行一次。
为了避免初始化逻辑执行多次,可用如下的代码:
+ (void)initialize
{
if (self == [ThisClass class]) {
// Perform initialization here.
...
}
}
注意:要记住运行系统会给每个类发送initialize消息。因此,在实现initialize方法中,你不可以向其父类发送initialize消息。 |
根类的方法
所有的对象无论是“类对象”还是类的实例都需要提供一个接口给运行系统。所有的类对象和实例都应该有自省的能力,能告诉运行系统它们在继承层次结构中的位置。这个接口的提供正是NSObject类的职责。
NSObject的方法不用非得实现两次—— 一次是提供运行的接口给实例对象,令一次是复制接口给“类对象”——“类对象”被赋予一个特殊的能力,可以执行定义在根类中的实例方法。当“类对象”接收到一个它不能用它的类方法响应的消息时,运行系统会判定它的根实例方法是否可以响应这个消息。“类对象”可执行的实例方法只能定义在根类当中,并且只能在没有响应的相应“类方法”才可以。
更多对“类对象”可以执行的根类实例方法的介绍,可以参照《Foundation framework reference》。
源代码中的类名
在源代码中,类名只能用在两个截然不同的环境当中。这两个环境反应了类有双重的职能——当做数据类型和对象。
• 类名可以为某一类对象当做类型名称,举例来说:
Rectangle*anObject;
在这里anObject被静态定义为指向Rectangle类型的指针。编译器希望它拥有Rectangle实例所拥有的以及被Rectangle继承 来的数据和方法。静态类型定义可以让编译器进行类型检测,使代码更加文档化——容易看懂。在“可静态行为”章节中 有更详细的介绍。
只有实例可以静态定义;“类对象”是不能的。由于它们不属于类的实例成员,但是它们有它们自己的类型Class。
• 作为消息表达式中的接收对象,类名就是指“类对象”。这个应用在前面的例子中已经讨论过。类名当做“类对象”只能在消息表达式中使用。在其它的任何环境中,你必须要求“类对象”返回它的id(通过发送class消息)。在如下的例子中Recangle类作为isKindOfClass:消息的参数:
if ( [anObjectisKindOfClass:[Rectangle class]] )
...
如果将Rectangle名当做参数传入将是不合法的。类名只能作为接收对象。
如果在编译的时候,你并不知道类名,但是这个名在一个字符串中,你可以使用NSClassFromString:方法返回类对象。
NSString*className;
...
if ( [anObjectisKindOfClass:NSClassFromString(className)] )
...
如果字符串中不是一个有效的类名,则会返回一个nil值。
类名和全局变量、函数存在于相同的命名空间中。类名在全局中是唯一的。
测试类的相等
比较两个“类对象”是否相等,可以直接比较它们的指针是否相同。不过你要获取正确的类。在Cocoa框架中有些灵活便捷的特性可以通过子类的继承获得(举例来说,key-value观测和Core Data——看“Key-ValueObservingProgramming Guide”和“Core Data ProgrammingGuide”)。当比较两个类是否相等时,你可以比较两个类的class返回值,而不必再使用那些低级别的函数。
[object class]!= object_getClass(object) != *((Class*)object)
你应该用如下的代码比较:
if([objectAclass] == [objectB class] { //...