从 C++ 到Objective-C
Objective-C 可以算作 Apple 平台上“唯一的”开发语言。很多 Objective-C 的教程往往直接从 Objective-C 开始讲起。不过,在我看来,这样做有时候是不合适的。很多程序员往往已经掌握了另外一种开发语言,如果对一门新语言的理解建立在他们已有的知识之上,更能 起到事半功倍的效果。既然名为 Objective-C,它与 C 语言的联系更加密切,然而它又是 Objective 的。与 C 语言联系密切,并且是 Objective 的,我们能够想到的另外一门语言就是 C++。C++ 的开发人员也更普遍,受众也会更多。于是就有了本系列,从 C++ 的角度来讲述 Objective-C 的相关知识。不过,相比 C++,C# 似乎更近一些。不过,我们还是还用 C++ 作为对比。这个系列不会作为一个完整的手册,仅仅是入门。本系列文章不会告诉你 Objective-C 里面的循环怎么写,而是通过与 C++ 的对比来学习 Objective-C 一些更为高级的内容,例如类的实现等等。如果要更好的使用 Objective-C,你需要阅读更多资料。但是,相信在本系列基础之上,你在阅读其他资料时应该会理解的更加透彻一些。
说明:本系列大致翻译来自《From C++ to Objective-C》,你可以在这里找到它的英文 pdf 版本。
下面来简单介绍一下 Objective-C。
要说 Objective-C,首先要从 Smalltalk 说起。Smalltalk 是第一个真正意义上的面向对象语言。Smalltalk 出现之后,很多人都希望能在 C 语言的基础之上增加面向对象的特性。于是就出现了两种新语言:C++ 和 Objective-C。C++ 不必多说,很多人都比较熟悉。Objective-C 则比较冷门。它完全借鉴了 Smalltalk 的思想,有着类似的语法和动态机制;相比来说,C++ 则更加静态一些,目的在于提供能好的性能。Objective-C 最新版本是 2.0.我们的这个系列就是以 Objective-C 2.0 为基础讲解。
Objective-C 是一门语言,而 Cocoa 是这门语言用于 MacOS X 开发的一个类库。它们的关系类似于 C++ 和 Qt,Java 和 Spring 一样。所以,我们完全可以不使用 Cocoa,只去用 Objective-C。例如 gcc 就是一个不使用 Cocoa 的编译器。不过在 MacOS X 平台,几乎所有的功能都要依赖 Cocoa 完成。我们这里只是做一个区别,应该分清 Objective-C 和 Cocoa 的关系。
从 C++到 Objective-C(2):语法概述
关键字
Objective-C 是 C 语言的超集。类似于 C++,良好的 C 源代码能够直接被 Objective-C 编译器编译。不同于 C++ 直接改变 C 语言的设计思路,Objective-C 仅仅是在 C 语言的基础上增加了一些概念。例如,对于类的概念,C++ 是增加了一个全新的关键字 class,把它作为语言内置的特性,而 Objective-C 则是将类转换成一个 struct 去处理。所以,为了避免冲突,Objective-C的关键字都是以 @ 开头。一个简单的关键字列表是:@class, @interface, @implementation, @public,@private, @protected, @try, @catch, @throw, @finally, @end, @protocol,@selector, @synchronized, @encode, @defs。Objective-C 2.0 又增加了 @optional, @required, @property, @dynamic, @synthesize 这几个。
另外的一些值同样也类似于关键字,有 nil 和 Nil, 类型 id, SEL 和 BOOL, 布尔变量 YES 和 NO。最后,特定上下文中会有一些关键字,分别是:in, out, inout, bycopy, byref, oneway 和 getter, setter, readwrite, readonly, assign,retain, copy, nonatomic 等。
很多继承自 NSObject 的函数很容易与关键字混淆。比如 alloc, release 和 autorelease 等。这些实际都是 NSObject 的函数。另外一个需要注意的是self 和 super。self 实际上是每一个函数的隐藏参数,而 super 是告知编译器使用 self 的另外语义。
注释
Objective-C 使用 // 和 /*…*/ 两种注释风格。
变量声明的位置
Objective-C 允许在代码块的中部声明变量,而不仅仅在块的最开始处。
新增的值和变量
BOOL, YES, NO
C++ 中使用 bool 表示布尔类型。Objective-C 中则是使用 BOOL,其值为 YES 和 NO。
nil, Nil 和 id
简单来说:
· 每一个对象都是 id 类型的。该类型可以作为一种弱类型使用。id 是一个指针,所以在使用时应注意是否需要再加 *。例如 id*foo = nil,实际是定义一个指针的指针;
· nil 等价于指向对象的 NULL 指针。nil 和NULL 不应该被混用。实际上,nil 并不简单是 NULL 指针;
· Nil 等价于指针 nil 的类。在 Objective-C 中,一个类也是一个对象(作为元类 Meta-Class 的实例)。nil 代表 NULL 指针,但它也是一个类的对象,nil 就是 Nil类的实例。C++ 没有对应的概念,不过,如果你熟悉 Java 的话,应该知道每一个类对象都对应一个 Class 实例,类似这个。
SEL
SEL 用于存储选择器 selector 的值。所谓选择器,就是不属于任何类实例对象的函数标识符。这些值可以由 @selector 获取。选择器可以当做函数指针,但实际上它并不是一个真正的指向函数的指针。
@encode
为了更好的互操作性,Objective-C 的数据类型,甚至自定义类型、函数或方法的元类型,都可以使用 ASCII 编码。@encode(aType) 可以返回该类型的 C 字符串(char *)的表示。
源文件
与 C++ 类似,Objective-C同样建议将声明和实现区分开。Objective-C 的头文件后缀名是 .h,源代码后缀名是 .m。Objective-C 使用 #import 引入其它头文件。与 #include 不同的是,#import 保证头文件只被引入一次。另外,#import 不仅仅针对 Objective-C 的头文件,即便是标准 C 的头文件,比如 stdlib.h,同样可以使用 #import 引入。
C++ | |
头文件 | 源文件 |
//In file Foo.h #ifndef __FOO_H__ //compilation guard #define __FOO_H__ // class Foo { ... }; #endif | //In file Foo.cpp #include "Foo.h" ... |
Objective-C | |
头文件 | 源文件 |
//In file Foo.h //class declaration, different from //the "interface" Java keyword @interface Foo : NSObject { ... } @end | //In file Foo.m #import "Foo.h" @implementation Foo ... @end |
NS 前缀
我们前面看到的类 NSObject,NSString 都有一个前缀 NS。这是 Cocoa 框架的前缀(Cocoa 开发公司是 NeXTStep)。
函数和方法的区别
Objective-C 并不是“使用方括号表示函数调用”的语言。一开始很容易把
[object doSomething];
理解成
object.doSomething();
但实际上并不是这么简单。Objective-C 是 C 语言的超集,因此,函数和 C 语言的声明、定义、调用是一致的。C 语言并没有方法这一概念,因此方法是使用特殊语法,也就是方括号。不仅仅是语法上的,语义上也是不同的:这并不是方法调用,而是发送一条消息。看上去并没有什么区别,实际上,这是 Objective-C 的强大之处。例如,这种语法允许你在运行时动态添加方法。
从 C++到 Objective-C(3):类和对象
既然是面向对象语言,类和对象显然是应该优先考虑的内容。鉴于本系列已经假定你已经熟悉 C++ 语言,自然就不会去解释类和对象的含义。我们直接从 Objecti-C 和 C++ 的区别开始说起。
Objetive-C 使用的是严格的对象模型,相比之下,C++ 的对象模型则更为松散。例如,在 Objective-C 中,所有的类都是对象,并且可以被动态管理:也就是说,你可以在运行时增加新的类,根据类的名字实例化一个类,以及调用类的方法。这比 C++ 的 RTTI 更加强大,而后者只不过是为一个“static”的语言增加的一点点功能而已。C++ 的 RTTI 在很多情况下是不被推荐使用的,因为它过于依赖编译器的实现,牺牲了跨平台的能力。
根类,id 类型,nil 和 Nil 的值
任何一个面向对象的语言都要管理很多类。同 Java 类似,Objective-C 有一个根类,所有的类都应该继承自这个根类(值得注意的是,在 Java 中,你声明一个类而不去显式指定它继承的父类,那么这个类就是 Object 类的直接子类;然而,在 Objective-C 中,单根类的子类必须被显式地说明);而 C++ 并没有这么一个类。Cocoa 中,这个根类就是 NSObject,它提供了很多运行时所必须的能力,例如内存分配等等。另外需要说明一点,单根类并不是 Objective-C 语言规范要求的,它只不过是根据面向对象理论实现的。因此,所有 Java 虚拟机的实现,这个单根类都是 Object,但是在Objective-C 中,这就是与类库相关的了:在Cocoa 中,这个单根类是 NSObject,而在 gcc 的实现里则是 Object。
严格说来,每一个类都应该是 NSObject 的子类(相比之下,Java 应该说,每一个类都必须是 Object 的子类),因此使用 NSObject * 类型应该可以指到所有类对象的指针。但是,实际上我们使用的是 id 类型。这个类型更加简短,更重要的是,id 类型是动态类型检查的,相比来说,NSObject * 则是静态类型检查。Objective-C 里面没有泛型,那么,我们就可以使用 id 很方便的实现类似泛型的机制了。在 Objective-C 里面,指向空的指针应该声明为 nil,不能是 NULL。这两者虽然很相似但并不可以互换。一个普通的 C 指针可以指向 NULL,但是Objective-C 的类指针必须指向 nil。正如前文所说,Objective-C 里面,类也是对象(元类 Meta-Class 的对象)。nil 所对应的类就是 Nil。
类声明
属性和方法
在 Objective-C 里面,属性 attributes 被称为实例数据 instance data,成员函数 member functions 被称为方法 methods。如果没有特殊说明,在后续文章中,这两组术语都会被混用,大家见谅。
C++ | Objective-C |
class Foo { double x; public: int f(int x); float g(int x, int y); }; int Foo::f(int x) {...} float Foo::g(int x, int y) {...} | @interface Foo : NSObject { double x; } -(int) f:(int)x; -(float) g:(int)x :(int)y; @end @implementation Foo -(int) f:(int)x {...} -(float) g:(int)x :(int)y {...} @end |
在 C++ 中,属性和成员函数都在类的花括号块中被声明。方法的实现类似于 C 语言,只不过需要有作用于指示符(Foo::)来说明这个函数属于哪个类。
Objective-C 中,属性和方法必须分开声明。属性在花括号中声明,方法要跟在下面。它们的实现要在 @implementation 块中。
这是与 C++ 的主要不同。在Objective-C 中,有些方法可以不被暴露在接口中,例如 private 的。而 C++ 中,即便是 private 函数,也能够在头文件中被看到。简单来说,这种分开式的声明可以避免 private 函数污染头文件。
实例方法以减号 – 开头,而 static 方法以 + 开头。注意,这并不是 UML 中的 private 和 public 的区别!参数的类型要在小括号中,参数之间使用冒号 : 分隔。
Objective-C 中,类声明的末尾不需要使用分号 ;。同时注意,Objective-C 的类声明关键字是 @interface,而不是 @class。@class 关键字只用于前向声明。最后,如果类里面没有任何数据,那么花括号可以被省略。
前向声明
为避免循环引用,C 语言有一个前向声明的机制,即仅仅告诉存在性,而不理会具体实现。C++ 使用 class 关键字实现前向声明。在 Objective-C 中则是使用 @class 关键字;另外,还可以使用 @protocol 关键字来声明一个协议(我们会在后面说到这个概念,类似于 Java 里面的 interface)。
C++ | |
//In file Foo.h #ifndef __FOO_H__ #define __FOO_H__ class Bar; //forward declaration class Foo { Bar* bar; public: void useBar(void); }; #endif | //In file Foo.cpp #include "Foo.h" #include "Bar.h" void Foo::useBar(void) { ... } |
Objective-C | |
//In file Foo.h @class Bar; //forward declaration @interface Foo : NSObject { Bar* bar; } -(void) useBar; @end | //In file Foo.m #import "Foo.h" #import "Bar.h" @implementation Foo -(void) useBar { ... } @end |
private,protected 和 public
访问可见性是面向对象语言的一个很重要的概念。它规定了在源代码级别哪些是可见的。可见性保证了类的封装性。
C++ | Objective-C |
class Foo { public: int x; int apple(); protected: int y; int pear(); private: int z; int banana(); }; | @interface Foo : NSObject { @public: int x; @protected: int y; @private: int z; } -(int) apple; -(int) pear; -(int) banana; @end |
在 C++ 中,属性和方法可以是 private,protected 和 public 的。默认是 private。
在 Objective-C 中,只有成员数据可以是 private,protected 和 public 的,默认是 protected 。方法只能是 public 的。然而,我们可以在 @implementation 块中实现一些方法,而不在 @interface 中声明;或者是使用分类机制(class categories)。这样做虽然不能阻止方法被调用,但是减少了暴露。不经过声明实现一些方法是 Objective-C 的一种特殊属性,有着特殊的目的。我们会在后面进行说明。
Objective-C 中的继承只能是 public 的,不可以是 private 和 protected 继承。这一点,Objective-C 更像 Java 而不是 C++。
static 属性
Objective-C 中不允许声明 static 属性。但是,我们有一些变通的方法:在实现文件中使用全局变量(也可以添加 static 关键字来控制可见性,类似 C 语言)。这样,类就可以通过方法访问到,而这样的全局变量的初始化可以在类的 initialize 方法中完成。
从 C++到 Objective-C(4):类和对象(续)
方法
Objective-C 中的方法与 C++ 的函数在语法方面风格迥异。下面,我们就来讲述 Objective-C 的方法。
原型、调用、实例方法和类方法
· 以 – 开头的是实例方法(多数情况下都应该是实例方法);以 + 开头的是类方法(相当于 C++ 里面的static 函数)。Objective-C的方法都是 public 的;
· 返回值和参数的类型都需要用小括号括起来;
· 参数之间使用冒号:分隔;
· 参数可以与一个标签 label 关联起来,所谓标签,就是在 : 之前的一个名字。标签被认为是方法名字的一部分。这使得方法比函数更易读。事实上,我们应该始终使用标签。注意,第一个参数没有标签,通常它的标签就是指的方法名;
· 方法名可以与属性名相同,这使 getter 方法变得很简单。
C++
// 原型
void Array::insertObject(void *anObject, unsigned int atIndex);
// shelf 是 Array 类的一个实例,book 是一个对象
shelf.insertObject(book, 2);
Objective-C(不带 label,即直接从 C++ 翻译来)
// 方法原型
// 方法名字是“insertObject::”
// 这里的冒号:用来分隔参数,成为方法名的一部分(注意,这不同于 C++ 的域指示符::)
-(void) insertObject:(id)anObject:(unsigned int)index
// shelf 是 Array 类的一个实例,book 是一个对象
[shelf insertObject:book:2];
Objective-C(带有 label)
// 方法原型。“index” 有一个标签“atIndex”
// 方法名为“insertObject:atIndex:”
// 这样的话,调用语句就很容易阅读了
-(void) insertObject:(id)anObject atIndex:(unsigned int)index
// shelf 是 Array 类的一个实例,book 是一个对象
[shelf insertObject:book:2]; // 错误!
[shelf insertObject:book atIndex:2]; // 正确
注意,方括号语法不应该读作“调用 shelf 对象的 insertObject 方法”,而应该是“向 shelf 对象发送一个 insertObject 消息”。这是Objective-C 的实现方式。你可以向任何对象发送任何消息。如果目标对象不能处理这个消息,它就会将消息忽略(这会引发一个异常,但不会终止程序)。如果接收到一个消息,目标对象能够处理,那么,目标对象就会调用相应的方法。如果编译器能够知道目标对象没有匹配的方法,那么编译器就会发出一个警告。鉴于 Objective-C 的前向机制,这并不会作为一个错误。如果目标对象是 id 类型,那么在编译期就不会有警告,但是运行期可能会有潜在的错误。
this,self 和 super
一个消息有两个特殊的目标对象:self 和 super。self 指当前对象(类似 C++ 的 this),super 指父对象。Objective-C 里面没有 this 指针,取而代之的是 self。
注意,self 不是一个关键字。实际上,它是每个消息接收时的隐藏参数,其值就是当前对象。它的值可以被改变,这一点不同于 C++ 的 this 指针。然而,这一点仅仅在构造函数中有用。
在方法中访问实例变量
同 C++ 一样,Objective-C在方法中也可以访问当前对象的实例变量。不同之处在于,C++ 需要使用 this->,而Objective-C 使用的是 self->。
C++ | Objective-C |
class Foo { int x; int y; void f(void); }; void Foo::f(void) { x = 1; int y; // 隐藏 this->y y = 2; // 使用局部变量 y this->y = 3; // 显式使用成员变量 } | @interface Foo : NSObject { int x; int y; } -(void) f; @end @implementation Foo -(void) f { x = 1; int y; // 隐藏 super->y y = 2; // 使用局部变量 y self->y = 3; // 显式使用成员变量 } @end |
原型的 id、签名和重载
函数就是一段能够被引用的代码,例如使用函数指针。一般的,方法名会作为引用方法的唯一 id,但是,这就需要小心有重载的情况。C++ 和 Objective-C 使用截然不同的两种方式去区分:前者使用参数类型,后者使用参数标签。
在 C++ 中,只要函数具有不同的参数类型,它们就可以具有相同的名字。const 也可以作为一种重载依据。
C++
int f(int);
int f(float); // 允许,float 和 int 是不同类型
class Foo
{
public:
int g(int);
int g(float); // 允许,float 和 int 是不同类型
int g(float) const; // 允许,const 可以作为重载依据
};
class Bar
{
public:
int g(int); // 允许,我们使用的是 Bar::,而不是 Foo::
}
在 Objective-C 中,所有的函数都是普通的 C 函数,不能被重载(除非指定使用 C99标准)。方法则具有不同的语法,重载的依据是 label。
Objective-C
int f(int);
int f(float); // 错误!C 函数不允许重载
@interface Foo : NSObject
{
}
-(int) g:(int) x;
-(int) g:(float) x; // 错误!类型不同不作为重载依据,同上一个没有区别
-(int) g:(int) x :(int) y; // 正确:两个匿名 label
-(int) g:(int) x :(float) y; // 错误:同上一个没有区别
-(int) g:(int) x andY:(int) y; // 正确:第二个 label 是 “andY”
-(int) g:(int) x andY:(float) y; // 错误:同上一个没有区别
-(int) g:(int) x andAlsoY:(int) y; // 正确:第二个 label 是 “andAlsoY”
@end
基于 label 的重载可以很明白地解释方法的名字,例如:
@interface Foo : NSObject {}
// 方法名是“g”
-(int) g;
// 方法名是“g:”
-(int) g:(float) x;
// 方法名是“g::”
-(int) g:(float) x :(float) y;
// 方法名是“g:andY:”
-(int) g:(float) x andY:(float) y;
// 方法名是“g:andZ:”
-(int) g:(float) x andZ:(float) z;
@end
显然,Objective-C 的方法使用 label 区分,而不是类型。利用这种机制,我们就可以使用选择器 selector 来指定一个方法,而不是“成员函数指针”。
从 C++到 Objective-C(5):类和对象(续二)
成员函数的指针:选择器
在 Objective-C 中,方法具有包含了括号和标签的特殊语法。普通的函数不能使用这种语法。在 Objective-C 和 C 语言中,函数指针具有相同的概念,但是对于成员函数指针则有所不同。
在 C++ 中,尽管语法很怪异,但确实兼容 C 语言的:成员函数指针也是基于类型的。
C++
class Foo
{
public:
int f(float x) {...}
};
Foo bar
int (Foo::*p_f)(float) = &Foo::f; // Foo::f 函数指针
(bar.*p_f)(1.2345); // 等价于 bar.f(1.2345);
在 Objective-C 中,引入了一个新的类型:指向成员函数的指针被称为选择器 selector。它的类型是 SEL,值通过 @selector 获得。@selector 接受方法名(包括 label)。使用类 NSInvocation 则可以通过选择器调用方法。大多时候,工具方法族 performSelector: (继承自 NSObject)更方便,约束也更大一些。其中最简单的三个是:
-(id) performSelector:(SEL)aSelector;
-(id) performSelector:(SEL)aSelector withObject:(id)anObjectAsParameter;
-(id) performSelector:(SEL)aSelector withObject:(id)anObjectAsParameter
withObject:(id)anotherObjectAsParameter;
这些方法的返回值同被调用的函数的返回值是一样的。对于那些参数不是对象的方法,应该使用该类型的包装类,如 NSNumber 等。NSInvocation 也有类似的功能,并且更为强大。
按照前面的说法,我们没有任何办法阻止在一个对象上面调用方法,即便该对象并没有实现这个方法。事实上,当消息被接收到之后,方法会被立即触发。但是,如果对象并不知道这个方法,一个可被捕获的异常将被抛除,应用程序并不会被终止。我们可以使用 respondsToSelector: 方法来检查对象是否可被触发方法。
最后,@selector 的值是在编译器决定的,因此它并不会减慢程序的运行效率。
Objective-C
@interface Slave : NSObject {}
-(void) readDocumentation:(Document*)document;
@end
// 假设 array[] 是包含 10 个 Slave 对象的数组,
// document 是一个 Document 指针
// 正常的方法调用是
for(i=0 ; i<10 ; ++i)
[array[i] readDocumentation:document];
// 下面使用 performSelector: 示例:
for(i=0 ; i<10 ; ++i)
[array[i] performSelector:@selector(readDocumentation:)
withObject:document];
// 选择器的类型是 SEL
// 下面代码并不比前面的高效,因为 @selector() 是在编译器计算的
SEL methodSelector = @selector(readDocumentation:);
for(i=0 ; i<10 ; ++i)
[slaves[i] performSelector:methodSelectorwithObject:document];
// 对于一个对象“foo”,它的类型是未知的(id)
// 这种测试并不是强制的,但是可以避免没有 readDocumentation: 方法时出现异常
if ([foo respondsToSelector:@selector(readDocumentation:)])
[foo performSelector:@selector(readDocumentation:) withObject:document];
因此,选择器可被用作函数参数。通用算法,例如排序,就可以使用这种技术实现。
严格说来,选择器并不是一个函数指针。它的底层实现是一个 C 字符串,在运行时被注册为方法的标识符。当类被加载之后,它的方法会被自动注册到一个表中,所以 @selector 可以很好的工作。根据这种实现,我们就可以使用 == 来判断内存地址是否相同,从而得出选择器是否相同,而无需使用字符串函数。
方法的真实地址,也就是看做 C 字符串的地址,其实可以看作是 IMP 类型(我们以后会有更详细的说明)。这种类型很少使用,除了在做优化的时候。例如虚调用实际使用选择器处理,而不是 IMP。等价于 C++ 函数指针的 Objective-C 的概念是选择器,也不是 IMP。
最后,你应该记得我们曾经说过 Objective-C 里面的 self 指针,类似于 C++ 的 this 指针,是作为每一个方法的隐藏参数传递的。其实这里还有第二个隐藏参数,就是 _cmd。_cmd 指的是当前方法。
@implementation Foo
-(void) f:(id)parameter // 等价于 C 函数 void f(id self, SEL _cmd,id parameter)
{
id currentObject = self;
SEL currentMethod = _cmd;
[currentObjectperformSelector:currentMethod
withObject:parameter]; // 递归调用
[self performSelector:_cmd withObject:parameter]; // 也是递归调用
}
@end
参数的默认值
Objective-C 不允许参数带有默认值。所以,如果某些参数是可选的,那么就应当创建多个方法的副本。在构造函数中,这一现象成为指定构造函数(designated initializer)。
可变参数
Objective-C 允许可变参数,语法同 C 语言一样,使用 … 作为最后一个参数。这实际很少用到,即是 Cocoa 里面很多方法都这么使用。
匿名参数
C++ 允许匿名参数,它可以将不使用的参数类型作为一种占位符。Objective-C 不允许匿名参数。
原型修饰符(const,static,virtual,”= 0″,friend,throw)
在 C++ 中,还有一些可以作为函数原型的修饰符,但在 Objective-C 中,这都是不允许的。以下是这个的列表:
· const:方法不能使用 const 修饰。既然没有了 const,也就不存在 mutable 了;
· static:用于区别实例方法和类方法的是原型前面的 – 和 +;
· virtual:Objective-C 中所有方法都是 virtual 的,因此没有必要使用这个修饰符。纯虚方法则是声明为一个典型的协议 protocol;
· friend:Objective-C 里面没有 friend 这个概念;
· throw:在 C++ 中,可以指定函数会抛除哪些异常,但是 Objective-C 不能这么做。
从 C++到 Objective-C(6):类和对象(续三)
消息和消息传输
给 nil 发送消息
默认情况下,给 nil 发送消息也是合法的,只不过这个消息被忽略掉了。这种机制可以避免很多检查指针是否为空的情况。不过,有些编译器,比如 GCC,也允许你通过编译参数的设置关闭这一特性。
将消息代理给未知对象
代理 delegation 是 Cocoa 框架中 UI 元素的一个很常见的部分。代理可以将消息转发给一个未知的对象。通过代理,一个对象可以将一些任务交给另外的对象。
// 设置一个辅助对象 assistant
-(void) setAssistant:(id)slave
{
[assistant autorelease];
assistant = [slave retain];
}
// 方法 performHardWork 使用代理
-(void) performHardWork:(id)task
{
// assistant 在编译期是未知的
// 我们首先要检查它是否能够响应消息
if ([assistant respondsToSelector:@selector(performHardWork:)])
[assistant performHardWork:task];
else
[self findAnotherAssistant];
}
转发:处理未知消息
在 C++ 中,如果对象函数没有实现,是不能通过编译的。Objective-C 则不同,你可以向对象发送任何消息。如果在运行时无法处理,这个消息就被忽略了(同时会抛出一个异常)。除了忽略它,另外的处理办法是将消息转发给另外的对象。
当编译器被告知对象类型时,它可以知道对象可以处理哪些消息,因此就可以知道消息发出后是否会失败,也就可以抛出异常。这也就是为什么消息在运行时被执行,但是编译时就可以发出警告。这并不会引发错误,同时还有另外的选择:调用 forwardInvocation: 方法。这个方法可以将消息进行转发。这个方法是 NSObject 的,默认不做任何操作。下面代码就是一种实现:
-(void) forwardInvocation:(NSInvocation*)anInvocation
{
// 如果该方法被调用,意味着我们无法处理这个消息
// 错误的选择器(也就是调用失败的那个方法名)可以通过
// 向 anInvocation 对象发送“selector” 获得
if ([anotherObject respondsToSelector:[anInvocation selector]])
[anInvocation invokeWithTarget:anotherObject];
else // 不要忘记调用父类的实现
[super forwardInvocation:anInvocation];
}
即是在最后,这个消息在 forwardInvocation: 中被处理,respondsToSelector: 还是会返回 NO。事实上,respondsToSelector:并不是用来检查 forwardInvocation: 是否被调用的。
使用这种转发机制有时候被认为是一种不好的习惯,因为它会隐藏掉本应引发错误的代码。事实上,一些很好的设计同样可以使用这种机制实现,例如 Cocoa 的 NSUndoManager。它允许一种对异常友好的语法:undo manager 可以记录方法调用历史,虽然它并不是那些调用的接收者。
向下转型
C++ 中,父类指针调用子类的函数时,需要有一个向下转型的操作(downcasting),使用dynamic_cast 关键字。在Objective-C 中,这是不必要的。因为你可以将任何消息发送给任何对象。但是,为了避免编译器的警告,我们也可以使用简单的转型操作。Objective-C 中没有类似 C++ 的专门的向下转型的操作符,使用 C 风格的转型语法就可以了。
// NSMutableString 是 NSString 的子类
// 允许字符串修改的操作
// "appendString:" 仅在 NSMutableString 中实现
NSMutableString* mutableString = ... 初始化可变字符串 ...
NSString* string = mutableString;// 传给 NSString 指针
// 这些调用都是合法的
[string appendString:@"foo"]; // 有编译器警告
[(NSMutableString*)string appendString:@"foo"]; // 无警告
[(id)string appendString:@"; // 无警告
从 C++到 Objective-C(7):继承
简单继承
Objective-C 也有继承的概念,但是不能多重继承。不过,它也有别的途径实现类似多重继承的机制,这个我们后面会讲到。
C++ | Objective-C |
class Foo : public Bar, protected Wiz { } | @interface Foo : Bar // 单继承 // 如果要同时“继承” Wiz,需要使用另外的技术 { } @end |
在 C++ 中,一个类可以继承自一个或多个类,使用 public、protected 以及 private 修饰符。子类的函数如果要调用父类的版本,需要使用 :: 运算符,例如 Bar::,Wiz:: 等。
在 Objective-C中,一个类只能继承一个父类,并且只能是 public 的(这和 Java 是一致的)。同样类似 Java,如果你要在子类中调用父类的函数,需要使用 super。
多重继承
Java 同样不允许多重继承。但是它提供了 interface 来模拟多重继承。类似的,Objective-C 也有同样的机制,这就是协议 protocol 和分类 categories。我们将在后面的内容详细讲述这两种技术。
虚拟性
虚方法
在 Objective-C 中,所有方法都是虚的,因此,没有 virtual 关键字或其等价物。
虚方法重定义
在 Objective-C 中,你可以定义一个没有在 @interface 块里面声明的方法。但这并不是一种替代 private 的机制,因为这种方法实际是能够被调用的(回想下,Objective-C 中方法的调用是在运行期决定的)。不过,这确实能够把接口定义变得稍微干净了一些。
这并不是一种坏习惯,因为有时你不得不重定义父类的函数。由于所有方法都是虚的,你无需像 C++ 一样在声明中显式写明哪些函数是 virtual 的,这种做法就成为一种隐式的重定义。很多继承西 NSObject 的方法都是是用这种方法重定义的。例如构造方法 init,析构方法 dealloc,view 类的 drawRect: 等等。这样的话,接口就变得更简洁,更易于阅读。不好之处就是,你不能知道究竟哪些方法被重定义了。
纯虚方法则是使用正式协议 formal protocols 来实现。
虚继承
Objective-C 中不允许多重继承,因此也就没有虚继承的问题。
协议
Java 和 C# 使用接口 interface 的概念来弥补多重继承的不足。Objective-C 也使用了类似的机制,成为协议 protocol。在 C++ 中,这种概念是使用抽象类。协议并不是真正的类:它只能声明方法,不能添加数据。有两种类型的协议:正式的 formal 和非正式的 informal。
正式协议
正式协议的方法,所有实现这个协议的类都必须实现。这就是一种验证,也就是说,只要这个类说实现这个协议,那么它肯定可以处理协议中规定的方法。一个类可以实现任意多个协议。
C++
class MouseListener
{
public:
virtual bool mousePressed(void) = 0; // 纯虚方法
virtual bool mouseClicked(void) = 0; // 纯虚方法
};
class KeyboardListener
{
public:
virtual bool keyPressed(void) = 0; // 纯虚方法
};
class Foo : public MouseListener, public KeyboardListener {...}
// Foo 必须实现 mousePressed, mouseClicked 和 keyPressed
// 然后 Foo 就可以作为鼠标和键盘的事件监听器
Objective-C
@protocol MouseListener
-(BOOL) mousePressed;
-(BOOL) mouseClicked;
@end
@protocol KeyboardListener
-(BOOL) keyPressed;
@end
@interface Foo : NSObject <MouseListener, KeyboardListener>
{
...
}
@end
// Foo 必须实现 mousePressed, mouseClicked 和 keyPressed
// 然后 Foo 就可以作为鼠标和键盘的事件监听器
C++ 中,协议可以由抽象类和纯虚函数实现。C++ 的抽象类要比 Objective-C 的协议强大的多,因为抽象类可以带有数据。
Objective-C 中,协议是一个特殊的概念,使用尖括号 <…> 表明。注意,尖括号在 Objective-C 中不是模板的意思,Objective-C 中没有类似 C++ 模板的概念。
一个类也可以不经过协议声明,直接实现协议规定的方法。此时,conformsToProtocol: 方法依然返回 NO。出于性能考虑,conformsToProtocol: 方法只检查类接口的声明,不会一个方法一个方法的对比着检查。conformsToProtocol: 的返回值并不会作为是否调用方法的依据。下面是这个方法的原型:
-(BOOL) conformsToProtocol:(Protocol*)protocol
// Protocol 对象可以由 @protocol(协议名) 返回
实现了正式协议的对象的类型同协议本身是兼容的。这一机制可以作为协议的筛选操作。例如:
// 下面方法是 Cocoa 提供的标准方法
// 方法参数可以是任意类型 id,但是必须兼容 NSDraggingInfo 协议
-(NSDragOperation) draggingEntered:(id )sender;
可选方法
有时我们需要这么一种机制:我们的类需要实现一部分协议中规定的方法,而不是整个协议。例如在 Cocoa 中,代理的概念被广泛使用:一个类可以给定一个辅助类,由这个辅助类去完成部分任务。
一种实现是将一个协议分割成很多小的协议,然后这个类去实现一个协议的集合。不过这并不具有可操作性。更好的解决方案是使用非正式协议。在 Objective-C 1.0 中就有非正式协议了,Objective-C 2.0 则提出了新的关键字 @optional 和 @required,用以区分可选方法和必须方法。
@protocol Slave
@required // 必须部分
-(void) makeCoffee;
-(void) duplicateDocument:(Document*)document count:(int)count;
@optional // 可选部分
-(void) sweep;
@required // 又是一个必须部分
-(void) bringCoffee;
@end
非正式协议
非正式协议并不是真正的协议,它对代码没有约束力。非正式协议允许开发者将一些方法进行归类,从而可以更好的组织代码。所以,非正式协议并不是协议的宽松版本。另外一个相似的概念就是分类。
让我们想象一个文档管理的服务。假设有绿色、蓝色和红色三种文档,一个类只能处理蓝色文档,而 Slave 类使用三个协议 manageBlueDocuments, manageGreenDocuments 和 manageRedDocuments。Slave 可以加入一个分类 DocumentsManaging,用来声明它能够完成的任务。分类名在小括号中被指定:
@interface Slave (DocumentsManaging)
-(void) manageBlueDocuments:(BlueDocument*)document;
-(void) trashBlueDocuments:(BlueDocument*)document;
@end
任何类都可以加入 DocumentsManaging 分类,加入相关的处理方法:
@interface PremiumSlave (DocumentsManaging)
-(void) manageBlueDocuments:(BlueDocument*)document;
-(void) manageRedDocuments:(RedDocument*)document;
@end
另一个开发者就可以浏览源代码,找到了 DocumentsManaging 分类。如果他觉得这个分类中有些方法可能对自己,就会检查究竟哪些能够使用。即便他不查看源代码,也可以在运行时指定:
if([mySlave respondsToSelector:@selector(manageBlueDocuments:)])
[mySlave manageBlueDocuments:document];
严格说来,除了原型部分,非正式协议对编译器没有什么意义,因为它并不能约束代码。不过,非正式协议可以形成很好的自解释性代码,让 API 更具可读性。
从 C++到 Objective-C(8):继承(续)
Protocol 对象
运行时,协议就像是类对象,其类型是 Protocol*。例如,conformsToProtocol: 方法就需要接受一个 Protocol* 类型的参数。@protocol 关键字不仅用于声明协议,还可以用于根据协议名返回 Protocol* 对象。
Protocol* myProtocol = @protocol(协议名)
远程对象的消息传递
由于 Objective-C 的动态机制,远程对象之间的消息传递变得很简单。所谓远程对象,是指两个或多个处于不同程序,甚至不同机器,但是可以通过代理完成同一任务,或者交换信息的对象。正式协议就是一种可以确保对象提供了这种服务的有效手段。正式协议还提供了很多额外的关键字,可以更好的说明各种参数。这些关键字分别是 in, out, inout, bycopy, byref 和 oneway。这些关键字仅对远程对象有效,并且仅可以在协议中使用。出了协议,它们就不被认为是关键字。这些关键字被插入到在协议中声明的方法原型之中,提供它们所修饰的参数的额外信息。它们可以告知,哪些是输入参数,哪些是输出参数,哪些使用复制传值,哪些使用引用传值,方法是否是同步的等等。以下是详细说明:
· in:参数是输入参数;
· out:参数是输出参数;
· inout:参数即是输入参数,又是输出参数;
· bycopy:复制传值;
· byref:引用传值;
· oneway:方法是异步的,也就是不会立即返回,因此它的返回值必须是 void。
例如,下面就是一个返回对象的异步方法:
-(oneway void) giveMeAnObjectWhenAvailable:(bycopy out id *)anObject;
默认情况下,参数都被认为是 inout 的。如果参数由 const 修饰,则被当做 in 参数。为参数选定是 in 还是 out,可以作为一种优化手段。参数默认都是传引用的,方法都是同步的(也就是不加 oneway)。对于传值的参数,也就是非指针类型的,out 和 inout 都是没有意义的,只有 in 是正确的选择。
分类
创建类的分类 categories,可以将一个很大的类分割成若干小部分。每个分类都是类的一部分,一个类可以使用任意多个分类,但都不可以添加实例数据。分类的好处是:
· 对于精益求精的开发者,分类提供了一种划分方法的机制。对于一个很大的类,它可以将其划分成不同的角色;
· 分类允许分开编译,也就是说,同一个类也可以进行多人的分工合作;
· 如果把分类的声明放在实现文件(.m)中,那么这个分类就只在文件作用域中可见(虽然这并没有调用上的限制,如果你知道方法原型,依然可以调用)。这样的分类可以取一个合适的名字,比如 FooPrivateAPI;
· 一个类可以在不同程序中有不同的扩展,而不需要丢弃通用代码。所有的类都可以被扩展,甚至是 Cocoa 中的类。
最后一点尤其重要。很多开发人员都希望标准类能够提供一些对他们而言很有用的方法。这并不是一个很困难的问题,使用继承即可实现。但是,在单继承的环境下,这会造成出现很多的子类。仅仅为了一个方法就去继承显得有些得不偿失。分类就可以很好的解决这个问题:
C++ | Objective-C |
class MyString : public string { public: // 统计元音的数目 int vowelCount(void); }; int MyString::vowelCount(void) { ... } | @interface NSString (VowelsCounting) // 注意并没有使用 {} -(int) vowelCount; // 统计元音的数目 @end @implementation NSString (VowelsCounting) -(int) vowelCount { ... } @end |
在 C++ 中,这是一个全新的类,可以自由使用。
在 Objective-C 中,NSString 是 Cocoa 框架的一个标准类。它是使用分类机制进行的扩展,只能在当前程序中使用。注意此时并没有新增加类。每一个 NSString 对象都可以从这个扩展获得统计元音数目的能力,甚至常量字符串也可以。同时注意,分类不能增加实例数据,因此没有花括号块。
分类也可以使匿名的,更适合于 private 的实现:
@interface NSString ()
// 注意并没有使用 {}
-(int) myPrivateMethod;
@end
@implementation NSString ()
-(int) myPrivateMethod
{
...
}
@end
混合使用协议、分类和子类
混合使用协议、分类和子类的唯一限制在于,你不能同时声明子类和分类。不过,你可以使用两步来绕过这一限制:
@interface Foo1 : SuperClass //ok
@end
@interface Foo2 (Category) //ok
@end
// 下面代码会有编译错误
@interface Foo3 (Category) : SuperClass
@end
// 一种解决方案
@interface Foo3 : SuperClass // 第一步
@end
@interface Foo3 (Category) // 第二步
@end
从 C++到 Objective-C(9):实例化
类的实例化位导致两个问题:构造函数、析构函数和赋值运算符如何实现,以及如何分配内存。
在 C++ 中,变量默认是“自动的”:除非被声明为 static,否则变量仅在自己的定义块中有意义。动态分配的内存可以一直使用,直到调用了 free() 或者 delete。C++ 中,所有对象都遵循这一规则。
然而在Objective-C 中,所有对象都是动态分配的。其实这也是符合逻辑的,因为 C++ 更加 static,而Objective-C 则更加动态。除非能够在运行时动态分配内存,否则 Objective-C 实现不了这么多动态的特性。
构造函数和初始化函数
分配 allocation 和初始化 initialization 的区别
在 C++ 中,内存分配和对象初始化都是在构造函数中完成的。在 Objective-C 中,这是两个不同的函数。
内存分配由类方法 alloc 完成,此时将初始化所有的实例数据。实例数据将被初始化为 0,除了一个名为 isa 的 NSObject 的指针。这个指针将在运行时指向对象的实际类型。实例数据根据传入的参数初始化为某一特定的值,这一过程将在一个实例方法 instance method 中完成。这个方法通常命名为 init。因此,构造过程被明确地分为两步:内存分配和初始化。alloc 消息被发送给类,而 init 消息则被发送给由 alloc 创建出来的新的对象。初始化过程不是可选的,alloc 之后应该跟着 init,之后,父类的 init 也会被调用,直到 NSObject 的 init 方法。这一方法完成了很多重要的工作。
在 C++ 中,构造函数的名字是规定好的,必须与类名一致。在 Objective-C 中,初始化方法与普通方法没有什么区别。你可以用任何名字,只不过通常都是选用 init 这个名字。然而,我们还是强烈建议,初始化方法名字一定要用 init 或者 init 开头的字符串。
使用 alloc 和 init
调用 alloc 之后将返回一个新的对象,并且应该给这个对象发送一个 init 消息。init 调用之后也会返回一个对象。通常,这就是初始化完成的对象。有时候,如果使用单例模式,init 可能会返回另外的对象(单例模式要求始终返回同一对象)。因此,init 的返回值不应该被忽略。通常,alloc 和 init 都会在一行上。
C++
Foo* foo = new Foo;
Objective-C
Foo* foo1 = [Foo alloc];
[foo1 init]; // 这是不好的行为:应该使用 init 的返回值
Foo* foo2 = [Foo alloc];
foo2 = [foo2 init]; // 正确,不过看上去很啰嗦
Foo* foo3 = [[Foo alloc] init]; // 正确,这才是通常的做法
为检查内存分配是否成功,C++ 可以判断 new 返回的指针是否是 0(如果使用的是 new(nothrow) 运算符)。在 Objective-C 中,检查返回值是否是 nil 就已经足够了。
初始化方法的正确示例代码
一个正确的初始化方法应该有如下特点:
· 名字以init 开始;
· 返回能够使用的对象;
· 调用父类的 init 方法,直到 NSObject 的init 方法被调用;
· 保存[super init...] 的返回值;
· 处理构造期间出现的任何错误,无论是自己的还是父类的。
下面是一些代码:
C++
class Point2D
{
public:
Point2D(int x, int y);
private:
int x;
int y;
};
Point2D::Point2D(int anX, int anY) {x = anX; y = anY;}
...
Point2D p1(3,4);
Point2D* p2 = new Point2D(5, 6);
Objective-C
@interface Point2D : NSObject
{
int x;
int y;
}
// 注意,在 Objective-C 中,id 类似于 void*
// (id) 就是对象的“一般”类型
-(id) initWithX:(int)anX andY:(int)anY;
@end
@implementation Point2D
-(id) initWithX:(int)anX andY:(int)anY
{
// 调用父类的初始化方法
if (!(self = [super init])) // 如果父类是 NSObject,必须进行 init 操作
return nil; // 如果父类 init 失败,返回 nil
// 父类调用成功,进行自己的初始化操作
self->x = anX;
self->y = anY;
return self; // 返回指向自己的指针
}
@end
...
Point2D* p1 = [[Point2D alloc] initWithX:3 andY:4];
从 C++到 Objective-C(10):实例化(续)
self = [super init...]
在上一篇提到的代码中,最不可思议的可能就是这句 self = [super init...]。回想一下,self 是每个方法的一个隐藏参数,指向当前对象。因此,这是一个局部变量。那么,为什么我们要改变一个局部变量的值呢?事实上,self 必须要改变。我们将在下面解释为什么要这样做。
[super init] 实际上返回不同于当前对象的另外一个对象。单例模式就是这样一种情况。然而, 有一个 API 可以用一个对象替换新分配的对象。Core Data(Apple 提供的 Cocoa 里面的一个 API)就是用了这种 API,对实例数据做一些特殊的操作,从而让这些数据能够和数据库的字段关联起来。当继承 NSManagedObject 类的时候,就需要仔细对待这种替换。在这种情形下,self 就要指向两个对象:一个是 alloc 返回的对象,一个是 [super init] 返回的对象。修改 self 的值对代码有一定的影响:每次访问实例数据的时候都是隐式的。正如下面的代码所示:
@interface B : A
{
int i;
}
@end
@implementation B
-(id) init
{
// 此时,self 指向 alloc 返回的值
// 假设 A 进行了替换操作,返回一个不同的 self
id newSelf = [super init];
NSLog(@"%d", i); // 输出 self->i 的值
self = newSelf; // 有人会认为 i 没有变化
NSLog(@"%d", i); // 事实上,此时的 self->i, 实际是 newSelf->i,
// 和之前的值可能不一样了
return self;
}
@end
...
B* b = [[B alloc] init];
self = [super init] 简洁明了,也不必担心以后会引入 bug。然而,我们应该注意旧的 self 指向的对象的命运:它必须被释放。第一规则很简单:谁替换 self 指针,谁就要负责处理旧的 self 指针。在这里,也就是 [super init] 负责完成这一操作。例如,如果你创建 NSManagedObject 子类(这个类会执行替换操作),你就不必担心旧的 self 指针。事实上,NSManagedObject 的开发者必须考虑这种处理。因此,如果你要创建一个执行替换操作的类,你必须知道如何在初始化过程中释放旧有对象。这种操作同错误处理很类似:如果因为非法参数、不可访问的资源造成构造失败,我们要如何处理?
初始化错误
初始化出错可能发生在三个地方:
1. 调用 [super init...] 之前:如果构造函数参数非法,那么初始化应该立即停止;
2. 调用 [super init...] 期间:如果父类调用失败,那么当前的初始化操作也应该停止;
3. 调用 [super init...] 之后:例如资源分配失败等。
在上面每一种情形中,只要失败,就应该返回 nil;相应的处理应该由发生错误的对象去完成。这里,我们主要关心的是1, 3情况。要释放当前对象,我们调用 [self release] 即可。
在调用 dealloc 之后,对象的析构才算完成。因此,dealloc 的实现必须同初始化方法兼容。事实上,alloc 将所有的实例数据初始化成 0 是相当有用的。
@interface A : NSObject {
unsigned int n;
}
-(id) initWithN:(unsigned int)value;
@end
@implementation A
-(id) initWithN:(unsigned int)value
{
// 第一种情况:参数合法吗?
if (value == 0) // 我们需要一个正值
{
[self release];
return nil;
}
// 第二种情况:父类调用成功吗?
if (!(self = [super init])) // 即是 self 被替换,它也是父类
return nil; // 错误发生时,谁负责释放 self?
// 第三种情况:初始化能够完成吗?
n = (int)log(value);
void* p = malloc(n); // 尝试分配资源
if (!p) // 如果分配失败,我们希望发生错误
{
[self release];
return nil;
}
}
@end
将构造过程合并为 alloc+init
有时候,alloc 和 init 被分割成两个部分显得很罗嗦。幸运的是,我们也可以将其合并在一起。这主要牵扯到 Objective-C 的内存管理机制。简单来说,作为一个构造函数,它的名字必须以类名开头,其行为类似 init,但要自己实现 alloc。然而,这个对象需要注册到 autorelease 池中,除非发送 retain 消息,否则其生命周期是有限制的。以下即是示例代码:
// 啰嗦的写法
NSNumber* tmp1 = [[NSNumber alloc] initWithFloat:0.0f];
...
[tmp1 release];
// 简洁一些
NSNumber* tmp2 = [NSNumber numberWithFloat:0.0f];
...
// 无需调用 release
从 C++到 Objective-C(11):实例化(续二)
默认构造函数:指定初始化函数
在 Objective-C 中,默认构造函数没有实在的意义,因为所有对象都是动态分配内存,也就是说,构造函数都是确定的。但是,一个常用的构造函数确实可以精简代码。事实上,一个正确的初始化过程通常类似于:
if (!(self = [super init])) // "init" 或其他父类恰当的函数
return nil;
// 父类初始化成功,继续其他操作……
return self;
剪贴复制代码是一个不良习惯。好的做法是,将共同代码放到一个独立的函数中,通常称为“指定初始化函数”。通常这种指定初始化函数会包含很多参数,因为 Objective-C 不允许参数有默认值。
-(id) initWithX:(int)x
{
return [self initWithX:x andY:0 andZ:0];
}
-(id) initWithX:(int)x andY:(int)y
{
return [self initWithX:x andY:y andZ:0];
}
// 指定初始化函数
-(id) initWithX:(int)x andY:(int)y andZ:(int)z
{
if (!(self = [super init]))
return nil;
self->x = x;
self->y = y;
self->z = z;
return self;
}
如果指定初始化函数没有最大数量的参数,那基本上就没什么用处:
// 以下代码就有很多重复部分
-(id) initWithX:(int)x // 指定初始化函数
{
if (!(self = [super init]))
return nil;
self->x = x;
return self;
}
-(id) initWithX:(int)x andY:(int)y
{
if (![self initWithX:x])
return nil;
self->y = y;
return self;
}
-(id) initWithX:(int)x andY:(int)y andZ:(int)z
{
if (![self initWithX:x])
return nil;
self->y = y;
self->z = z;
return self;
}
初始化列表和实例数据的默认值
Objective-C 中不存在 C++ 构造函数的初始化列表的概念。然而,不同于 C++,Objective-C的 alloc 会将所有实例数据初始化成 0,因此指针也会被初始化成 nil。C++ 中,对象属性不同于指针,但是在 Objective-C 中,所有对象都被当做指针处理。
虚构造函数
Objective-C 中存在虚构造函数。我们将在后面的章节中详细讲诉这个问题。
类构造函数
在 Objective-C 中,类本身就是对象,因此它也有自己的构造函数,并且也能够被重定义。它显然是一个类函数,继承自 NSObject,其原型是 +(void) initialize;。
第一次使用这个类或其子类的时候,这个函数将被自动调用。但这并不意味着,对于指定的类,这个函数只被调用一次。事实上,如果子类没有定义 +(void) initialize;,那么 Objective-C 将调用其父类的 +(void) initialize;。
析构函数
在 C++ 中,析构函数同构造函数一样,是一个特殊的函数。在 Objective-C 中,析构函数也是一个普通的实例函数,叫做 dealloc。C++ 中,当对象被释放时,析构函数将自动调用;Objective-C 也是类似的,但是释放对象的方式有所不同。
析构函数永远不应该被显式调用。在 C++ 中存在这么一种情况:开发者自己在析构时管理内存池。但是在 Objective-C 中没有这种限制。你可以在 Cocoa 中使用自定义的内存区域,但是这并不会影响平常的内存的分配、释放机制。
C++
class Point2D
{
public:
~Point2D();
};
Point2D::~Point2D() {}
Objective-C
@interface Point2D : NSObject
-(void) dealloc; // 该方法可以被重定义
@end
@implementation Point2D
// 在这个例子中,重定义并不需要
-(void) dealloc
{
[super dealloc]; // 不要忘记调用父类代码
}
@end
从 C++到 Objective-C(12):实例化(续三)
复制运算符
典型 cloning, copy, copyWithZone:, NSCopyObject()
在 C++ 中,定义复制运算符和相关的操作是很重要的。在 Objective-C 中,运算法是不允许重定义的,所能做的就是要求提供一个正确的复制函数。
克隆操作在 Cocoa 中要求使用 NSCopying 协议实现。该协议要求一个实现函数:
-(id) copyWithZone:(NSZone*)zone;
这个函数的参数是一个内存区,用于指明需要复制那一块内存。Cocoa 允许使用不同的自定义区块。大多数时候默认的区块就已经足够,没必要每次都单独指定。幸运的是,NSObject 有一个函数
-(id) copy;
封装了 copyWithZone:,直接使用默认的区块作为参数。但它实际相当于 NSCopying 所要求的函数。另外,NSCopyObject() 提供一个不同的实现,更简单但同样也需要注意。下面的代码没有考虑 NSCopyObject():
// 如果父类没有实现 copyWithZone:,并且没有使用 NSCopyObject()
-(id) copyWithZone:(NSZone*)zone
{
// 创建对象
Foo* clone = [[Foo allocWithZone:zone] init];
// 实例数据必须手动复制
clone->integer = self->integer; // "integer" 是 int 类型的
// 使用子对象类似的机制复制
clone->objectToClone = [self->objectToClone copyWithZone:zone];
// 有些子对象不能复制,但是可以共享
clone->objectToShare = [self->objectToShare retain];
// 如果有设置方法,也可以使用
[clone setObject:self->object];
return clone;
}
注意,我们使用的是 allocWithZone: 而不是 alloc。alloc 实际上封装了allocWithZone:,它传进的是默认的 zone。但是,我们应该注意父类的 copyWithZone: 的实现。
// 父类实现了 copyWithZone:,并且没有使用 NSCopyObject()
-(id) copyWithZone:(NSZone*)zone
{
Foo* clone = [super copyWithZone:zone]; // 创建新的对象
// 必须复制当前子类的实例数据
clone->integer = self->integer; // "integer" 是 int 类型的
// 使用子对象类似的机制复制
clone->objectToClone = [self->objectToClone copyWithZone:zone];
// 有些子对象不能复制,但是可以共享
clone->objectToShare = [self->objectToShare retain];
// 如果有设置方法,也可以使用
[clone setObject:self->object];
return clone;
}
NSCopyObject()
NSObject 事实上并没有实现 NSCopying 协议(注意函数的原型不同),因此我们不能简单地使用 [super copy...] 这样的调用,而是类似 [[... alloc] init] 这种标准调用。NSCopyObject() 允许更简单的代码,但是需要注意指针变量(包括对象)。这个函数创建一个对象的二进制格式的拷贝,其原型是:
// extraBytes 通常是 0,可以用于索引实例数据的空间
id NSCopyObject(id anObject, unsigned int extraBytes, NSZone *zone)
二进制复制可以复制非指针对象,但是对于指针对象,需要时刻记住它会创建一个指针所指向的数据的新的引用。通常的做法是在复制完之后重置指针。
// 如果父类没有实现 copyWithZone:
-(id) copyWithZone:(NSZone*)zone
{
Foo* clone = NSCopyObject(self, 0, zone); // 以二进制形式复制数据
// clone->integer = self->integer; // 不需要,因为二进制复制已经实现了
// 需要复制的对象成员必须执行真正的复制
clone->objectToClone = [self->objectToClone copyWithZone:zone];
// 共享子对象必须注册新的引用
[clone->objectToShare retain];
// 设置函数看上去应该调用 clone->object. 但实际上是不正确的,
// 因为这是指针值的二进制复制。
// 因此在使用 mutator 前必须重置指针
clone->object = nil;
[clone setObject:self->object];
return clone;
}
// 如果父类实现了 copyWithZone:
-(id) copyWithZone:(NSZone*)zone
{
Foo* clone = [super copyWithZone:zone];
// 父类实现 NSCopyObject() 了吗?
// 这对于知道如何继续下面的代码很重要
clone->integer = self->integer; // 仅在 NSCopyObject() 没有使用时调用
// 如果有疑问,一个需要复制的子对象必须真正的复制
clone->objectToClone = [self->objectToClone copyWithZone:zone];
// 不管 NSCopyObject() 是否实现,新的引用必须添加
clone->objectToShare = [self->objectToShare retain];
clone->object = nil; // 如果有疑问,最好重置
[clone setObject:self->object];
return clone;
}
Dummy-cloning,mutability, mutableCopy and mutableCopyWithZone:
如果需要复制不可改变对象,一个基本的优化是假装它被复制了,实际上是返回一个原始对象的引用。从这点上可以区分可变对象与不可变对象。
不可变对象的实例数据不能被修改,只有初始化过程能够给一个合法值。在这种情况下,使用“伪克隆”返回一个原始对象的引用就可以了,因为它本身和它的复制品都不能够被修改。此时,copyWithZone: 的一个比较好的实现是:
-(id) copyWithZone:(NSZone*)zone
{
// 返回自身,增加一个引用
return [self retain];
}
retain 操作意味着将其引用加 1。我们需要这么做,因为当原始对象被删除时,我们还会持有一个复制品的引用。
“伪克隆”并不是无关紧要的优化。创建一个新的对象需要进行内存分配,相对来说这是一个比较耗时的操作,如果可能的话应该注意避免这种情况。这就是为什么需要区别可变对象和不可变对象。因为不可变对象可以在复制操作上做文章。我们可以首先创建一个不可变类,然后再继承这个类增加可变操作。Cocoa 中很多类都是这么实现的,比如 NSMutableString 是 NSString 的子类;NSMutableArray是 NSArray 的子类;NSMutableData 是 NSData 的子类。
然而根据我们上面描述的内容,似乎无法从不可变对象安全地获取一个完全的克隆,因为不可变对象只能“伪克隆”自己。这个限制大大降低了不可变对象的可用性,因为它们从“真实的世界”隔离了出来。
除了 NSCopy 协议,还有一个另外的 NSMutableCopying 协议,其原型如下:
-(id) mutableCopyWithZone:(NSZone*)zone;
mutableCopyWithZone: 必须返回一个可变的克隆,其修改不能影响到原始对象。类似 NSObject 的 copy 函数,也有一个mutableCopy 函数使用默认区块封装了这个操作。mutableCopyWithZone:的实现类似前面的 copyWithZone: 的代码:
// 如果父类没有实现 mutableCopyWithZone:
-(id) mutableCopyWithZone:(NSZone*)zone
{
Foo* clone = [[Foo allocWithZone:zone] init]; // 或者可用 NSCopyObject()
clone->integer = self->integer;
// 类似 copyWithZone:,有些子对象需要复制,有些需要增加引用
// 可变子对象使用 mutableCopyWithZone: 克隆
//...
return clone;
}
不要忘记我们可以使用父类的 mutableCopyWithZone:
// 如果父类实现了 mutableCopyWithZone:
-(id) mutableCopyWithZone:(NSZone*)zone
{
Foo* clone = [super mutableCopyWithZone:zone];
//...
return clone;
}
从 C++ 到Objective-C(13):内存管理
new 和 delete
Objective-C 中没有 new 和 delete 这两个关键字(new 可以看作是一个函数,也就是 alloc+init)。它们实际是被 alloc 和 release 所取代。
引用计数
内存管理是一个语言很重要的部分。在 C 和 C++ 中,内存块有一次分配,并且要有一次释放。这块内存区可以被任意多个指针指向,但只能被其中一个指针释放。Objective-C 则使用引用计数。对象知道自己被引用了多少次,这就像狗和狗链的关系。如果对象是一条狗,每个人都可以拿狗链拴住它。如果有人不想再管它了,只要丢掉他手中的狗链就可以了。只要还有一条狗链,狗就必须在那里;但是只要所有的狗链都没有了,那么此时狗就自由了。换做技术上的术语,新创建的对象的引用计数器被设置为 1。如果代码需要引用这个对象,就可以发送一个 retain 消息,让计数器加 1。当代码不需要的时候则发送一个release 消息,让计数器减 1。
对象可以接收任意多的 retain 和 release 消息,只要计数器的值是正的。当计数器成 0 时,析构函数 dealloc 将被自动调用。此时再次发送 release 给这个对象就是非法的了,将引发一个内存错误。
这种技术并不同于 C++ STL 的 auto_ptr。Boost 库提供了一个类似的引用计数器,称为 shared_ptr,但这并不是标准库的一部分。
alloc, copy,mutableCopy, retain, release
明白了内存管理机制并不能很好的使用它。这一节的目的就是给出一些使用规则。这里先不解释 autorelease 关键字,因为它比较难理解。
基本规则是,所有使用 alloc,[mutable]copy[WithZone:] 或者是 retain 增加计数器的对象都要用 [auto]release 释放。事实上,有三种方法可以增加引用计数器,也就意味着仅仅有有限种情况下才要使用 release 释放对象:
· 使用alloc 显式实例化对象;
· 使用copy[WithZone:] 或者mutableCopy[WithZone:] 复制对象(不管这种克隆是不是伪克隆);
· 使用retain。
记住,默认情况下,给 nil 发送消息(例如 release)是合法的,不会引起任何后果。
autorelease
不一样的 autorelease
前面我们强调了,所有使用 alloc,[mutable]copy[WithZone:] 或者是 retain 增加计数器的对象都要用[auto]release 释放。事实上,这条规则不仅仅适用于alloc、retain 和 release。有些函数虽然不是构造函数,但也用于创建对象,例如 C++ 的二元加运算符(obj3 operator+(obj1, obj2))。在 C++ 中,返回值可以在栈上,以便在离开作用域的时候可以自动销毁。但在 Objective-C 中不存在这种对象。函数使用 alloc 分配的对象,直到将其返回栈之前不能释放。下面的代码将解释这种情况:
// 第一个例子
-(Point2D*) add:(Point2D*)p1 and:(Point2D*)p2
{
Point2D* result = [[Point2D alloc] initWithX:([p1 getX] + [p2 getX])
andY:([p1 getY] + [p2 getY])];
return result;
}
// 错误!这个函数使用了 alloc,所以它将对象的引用计数器加 1。
// 根据前面的说法,它应该被销毁。
// 但是这将引起内存泄露:
[calculator add:[calculator add:p1 and:p2] and:p3];
// 第一个算式是匿名的,没有办法 release。所以引起内存泄露。
// 第二个例子
-(Point2D*) add:(Point2D*)p1 and:(Point2D*)p2
{
return [[Point2D alloc] initWithX:([p1 getX] + [p2 getX])
andY:([p1 getY] + [p2 getY])];
}
// 错误!这段代码实际上和上面的一样,
// 不同之处在于仅仅减少了一个中间变量。
// 第三个例子
-(Point2D*) add:(Point2D*)p1 and:(Point2D*)p2
{
Point2D* result = [[Point2D alloc] initWithX:([p1 getX] + [p2 getX])
andY:([p1 getY] + [p2 getY])];
[result release];
return result;
}
// 错误!显然,这里仅仅是在对象创建出来之后立即销毁了。
这个问题看起来很棘手。如果没有 autorelease 的确如此。简单地说,给一个对象发送 autorelease 消息意味着告诉它,在“一段时间之后”销毁。但是这里的“一段时间之后”并不意味着“任何时间”。我们将在后面的章节中详细讲述这个问题。现在,我们有了上面这个问题的一种解决方案:
-(Point2D*) add:(Point2D*)p1 and:(Point2D*)p2
{
Point2D* result = [[Point2D alloc] initWithX:([p1 getX] + [p2 getX])
andY:([p1 getY] + [p2 getY])];
[result autorelease];
return result; // 更简短的代码是:return [result autorelease];
}
// 正确!result 将在以后自动释放
从 C++ 到Objective-C(14):内存管理(续)
autorelease 池
上一节中我们了解到 autorelease 的种种神奇之处:它能够在合适的时候自动释放分配的内存。但是如何才能让便以其之道什么时候合适呢?这种情况下,垃圾收集器是最好的选择。下面我们将着重讲解垃圾收集器的工作原理。不过,为了了解垃圾收集器,就不得不深入了解 autorelease 的机制。所以我们要从这里开始。当对象收到 autorelease 消息的时候,它会被注册到一个“autorelease 池”。当这个池被销毁时,其中的对象也就被实际的销毁。所以,现在的问题是,这个池如何管理?
答案是丰富多彩的:如果你使用 Cocoa 开发 GUI 界面,基本不需要做什么事情;否则的话,你应该自己创建和销毁这个池。
拥有图形界面的应用程序都有一个事件循环。这个循环将等待用户动作,使应用程序响应动作,然后继续等待下一个动作。当你使用 Cocoa 创建 GUI 程序时,这个 autorelease 池在事件循环的一次循环开始时被自动创建,然后在循环结束时自动销毁。这是合乎逻辑的:一般的,一个用户动作都会触发一系列任务,临时变量的创建和销毁一般不会影响到下一个事件。如果必须要有可持久化的数据,那么你就要手动地使用 retain 消息。
另一方面,如果没有 GUI,你必须自己建立 autorelease 池。当对象收到 autorelease 消息时,它能够找到最近的 autorelease 池。当池可以被清空时,你可以对这个池使用 release 消息。一般的,命令行界面的 Cocoa 程序都会有如下的代码:
int main(int argc, char* argv[])
{
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
//...
[pool release];
return 0;
}
注意在 Mac OS X 10.5 的 NSAutoreleasePool 类新增加了一个 drain 方法。这个方法等价于:当垃圾收集器可用时做 release 操作;否则则触发运行垃圾收集。这对编写在两种情况下都适用的代码时是很有用的。注意,这里实际上是说,现在有两种环境:引用计数和垃圾回收。Mac OS 的新版本都会支持垃圾收集器,但是 iOS 却不支持。在引用计数环境下,NSAutoreleasePool 的 release 方法会给池中的所有对象发送 release 消息,如果对象注册了多次,就会多次给它发 release。drain 和 release 在应用计数环境下是等价的。在垃圾收集的环境下,release 不做任何事情,drain 则会触发垃圾收集。
使用多个 autorelease 池
在一个程序中使用多个 autorelease 池也是可以的。对象收到 autorelease 消息时会注册到最近的池。因此,如果一个函数需要创建并使用很大数量临时对象,为了提高性能,可以创建一个局部的 autorelease 池。这种情况下,这些临时变量就可以及时的被销毁,从而在函数返回时就将内存释放出来。
autorelease 的注意点
使用 autorelease 可能会有一些误用情况,需要我们特别注意。
· 首先,非必要地发送多个 autorelease 类似发送多个 release 消息,在内存池清空时会引起内存错误;
· 其次,即使 release 可以由 autorelease 替代,也不能滥用 autorelease。因为 autorelease 要比正常的 release 消耗资源更多。另外,不必要的推迟 release 操作无疑会导致占用大量内存,容易引起内存泄露。
autorelease 和 retain
多亏了 autorelease,方法才能够创建能够自动释放的对象。但是,长时间持有对象是一种很常见的需求。在这种情形下,我们可以向对象发送 retain 消息,然后在后面手动的 release。这样,这个对象实际上可以从两个角度去看待:
· 从函数开发者的角度,对象的创建和释放都是有计划的;
· 从函数调用者的角度,使用了 retain 之后,对象的生命期变长了(使用 retain 将使其引用计数器加 1),为了让对象能够正确地被释放,调用者必须负责将计数器再减 1。
我们来理解一下这句话。对于一个函数的开发者,如果他不使用 autorelease,那么,他使用 alloc 创建了一个对象并返回出去,那么,他需要负责在合适的时候对这个对象做 release 操作。也就是说,从函数开发者的角度,这个对象的计数器始终是 1,一次 release 是能够被正常释放的。此时,函数调用者却使用 retain 将计数器加 1,但是开发者不知道对象的计数器已经变成 2 了,一次 release 不能释放对象。所以,调用者必须注意维护计数器,要调用一次 release 将其恢复至 1。
Convenience constructor,virtual constructor
将构造对象的过程分成 alloc 和 init 两个阶段,有时候显得很罗嗦。好在我们有一个 convenience constructor 的概念。这种构造函数应该使用类名做前缀,其行为类似 init,同时要实现 alloc。但是,它的返回对象需要注册到一个内部的 autorelease 池,如果没有给它发送 retain 消息时,这个对象始终是一个临时对象。例如:
// 啰嗦的写法
NSNumber* zero_a = [[NSNumber alloc] initWithFloat:0.0f];
...
[zero_a release];
...
// 简洁一些的
NSNumber* zero_b = [NSNumber numberWithFloat:0.0f];
...
// 不需要 release
根据我们前面对内存管理的介绍,这种构造函数的实现是基于 autorelease 的。但是其底层代码并不那么简单,因为这涉及到对 self 的正确使用。事实上,这种构造函数都是类方法,所以 self 指向的是 Class 类型的对象,就是元类类型的。在初始化方法,也就是一个实例方法中,self 指向的是这个类的对象的实例,也就是一个“普通的”对象。
编写错误的这种构造函数是很容易的。例如,我们要创建一个 Vehicle 类,包含一个 color 数据,编写如下的代码:
// The Vehicle class
@interface Vehicle : NSObject
{
NSColor* color;
}
-(void) setColor:(NSColor*)color;
// 简洁构造函数
+(id) vehicleWithColor:(NSColor*)color;
@end
其对应的实现是:
// 错误的实现
+(Vehicle*) vehicleWithColor:(NSColor*)color
{
// self 不能改变
self = [[self alloc] init]; // 错误!
[self setColor:color];
return [self autorelease];
}
记住我们前面所说的,这里的 self 指向的是 Class 类型的对象。
// 比较正确的实现
+(id) vehicleWithColor:(NSColor*)color
{
id newInstance = [[Vehicle alloc] init]; // 正确,但是忽略了有子类的情况
[newInstance setColor:color];
return [newInstance autorelease];
}
我们来改进一下。Objective-C 中,我们可以实现 virtual constructor。这种构造函数通过内省的机制来了解到自己究竟应该创建哪种类的对象,是这个类本身的还是其子类的。然后它直接创建正确的类的实例。我们可以使用一个 class 方法(注意,class 在 Objective-C 中不是关键字);这是 NSObject 的一个方法,返回当前对象的类对象(也就是 meta-class 对象)。
@implementation Vehicle
+(id) vehicleWithColor:(NSColor*)color
{
id newInstance = [[[self class] alloc] init]; // 完美!我们可以在运行时识别出类
[newInstance setColor:color];
return [newInstance autorelease];
}
@end
@interface Car : Vehicle {...}
@end
...
// 创建一个 red Car
id car = [Car vehicleWithColor:[NSColor redColor]];
类似于初始化函数的 init 前缀,这种简洁构造函数最好使用类名作前缀。不过也有些例外,例如 [NSColor redColor] 返回一个预定义的颜色,按照我们的约定,使用 [NSColor colorRed] 更合适一些。
最后,我们要重复一下,所有使用 alloc、[mutable]copy[WithZone:] 增加引用计数器值的对象,都必须相应地调用 [auto]release。当调用简洁构造函数时,你并没有显式调用 alloc,也就不应该调用 release。但是,在创建这种构造函数时,一定不要忘记使用 autorelease。
从 C++到 Objective-C(15):内存管理(续二)
Setters
如果不对 Objective-C 的内存管理机制有深刻的理解,是很难写出争取的 setter 的。假设一个类有一个名为 title 的 NSString 类型的属性,我们希望通过 setter 设置其值。这个例子虽然简单,但已经表现出 setter 所带来的主要问题:参数如何使用?不同于 C++,在 Objective-C 中,对象只能用指针引用,因此 setter 虽然只有一种原型,但是却可 以有很多种实现:可以直接指定,可以使用 retain 指定,或者使用 copy。每一种实现都有特定的目的,需要考虑你 set 新的值之后,新值和旧值之间的关系(是否相互影响等)。另外,每一种实现 都要求及时释放旧的资源,以避免内存泄露。直接指定(不完整的代码)
外面传进来的对象仅仅使用引用,不带有 retain。如果外部对象改变了,当前类也会知 道。也就是说,如果外部对象被释放掉,而当前类在使用时没有检查是否为 nil,那么当前类就会持有一个非法引用。
-(void) setString:(NSString*)newString
{
... 稍后解释内存方面的细节
self->string = newString; // 直接指定
}
使用 retain 指定(不完整的代码)
外部对象被引用,并且使用 retain 将其引用计数器加 1。外部对象的改变对于当前类也是可见的,不过,外部对象不能被释 放,因为当前类始终持有一个引用。
-(void) setString:(NSString*)newString
{
... 稍后解释内存方面的细节
self-> string = [newString retain]; // 使用 retain 指定
}
复制(不完整的代码)
外部对象实际没有被引用,使用的是其克隆。此时,外部对象的改变对于当前类是不可变的。也就是说,当前类持有的是这个对象的克隆, 这个对象的生命周期不会比持有者更长。
-(void) setString:(NSString*)newString
{
... 稍后解释内存方面的细节
self->string = [newString copy]; // 克隆
// 使用 NSCopying 协议
}
为了补充完整这些代码,我们需要考虑这个对象在前一时刻的状态:每一种情形下,setter 都需要释放掉旧的资源,然后建立新的。这些代码看起来比较麻烦。
直接指定( 完整代码)
这是最简单的情况。旧的引用实际上被替换成了新的。
-(void) setString:(NSString*)newString
{
// 没有强链接,旧值被改变了
self->string = newString; // 直接指定
}
使用 retain 指定(完整代码)
在这种情况下,旧值需要被释放,除非旧值和新值是一样的。
// ------ 不正确的实现 ------
-(void) setString:(NSString*)newString
{
self->string = [newString retain];
// 错误!内存泄露,没有引用指向旧的“string”,因此再也无法释放
}
-(void) setString:(NSString*)newString
{
[self->string release];
self->string = [newString retain];
// 错误!如果 newString == string(这是可能的),
// newString 引用是 1,那么在 [self->string release]之后
// 使用 newString 就是非法的,因为此时对象已经被释放
}
-(void) setString:(NSString*)newString
{
if (self->string != newString)
[self->string release]; // 正确:给 nil 发送 release 是安全的
self->string = [newString retain]; // 错误!应该在 if 里面
// 因为如果 string == newString,
// 计数器不会被增加
}
// ------ 正确的实现 ------
// 最佳实践:C++ 程序员一般都会“改变前检查”
-(void) setString:(NSString*)newString
{
// 仅在必要时修改
if (self->string != newString) {
[self->string release]; // 释放旧的
self->string = [newString retain]; // retain 新的
}
}
// 最佳实践:自动释放旧值
-(void) setString:(NSString*)newString
{
[self->string autorelease]; // 即使 string == newString 也没有关系,
// 因为 release 是被推迟的
self->string = [newString retain];
//... 因此这个 retain 要在 release 之前发生
}
// 最佳实践:先 retain 在 release
-(void) setString:(NSString*)newString
{
[self->newString retain]; // 引用计数器加 1(除了 nil)
[self->string release]; // release 时不会是 0
self->string = newString; // 这里就不应该再加 retain 了
}
复制(完整代码)
无论是典型的误用还是正确的解决方案,都和前面使用 retain 指定一样,只不过把 retain 换成 copy。
伪克隆
有些克隆是伪克隆,不过对结果没有影响。
从 C++到 Objective-C(16):内存管理(续三)
Getters
Objective-C 中,所有对象都是动态分配的,使用指针引用。一般的,getter 仅仅返回指针的值,而不应该复制对象。getter 的名字一般和数据成员的名字相同(这一点不同于 Java,JavaBean 规范要求以 get 开头),这并不会引起任何问题。如果是布尔变量,则使用 is 开头(类似 JavaBean 规范),这样可以让程序更具可读性。
@interface Button
{
NSString* label;
BOOL pressed;
}
-(NSString*) label;
-(void) setLabel:(NSString*)newLabel;
-(BOOL) isPressed;
@end
@implementation Button
-(NSString*) label
{
return label;
}
-(BOOL) isPressed
{
return pressed;
}
-(void) setLabel:(NSString*)newLabel {...}
@end
当返回实例数据指针时,外界就可以很轻松地修改其值。这可能是很多 getter 不希望的结果,因为这样一来就破坏了封装性。
@interface Button
{
NSMutableString* label;
}
-(NSString*) label;
@end
@implementation Button
-(NSString*) label
{
return label; // 正确,但知道内情的用户可以将其强制转换成 NSMutableString,
// 从而改变字符串的值
}
-(NSString*) label
{
// 解决方案 1 :
return [NSString stringWithString:label];
// 正确:实际返回一个新的不可变字符串
// 解决方案 2 :
return [[label copy] autorelease];
// 正确:返回一个不可变克隆,其值是一个 NSString(注意不是 mutableCopy)
}
@end
循环 retain
必须紧身避免出现循环 retain。如果对象 A retain 对象 B,B 和 C 相互 retain,那么 B 和 C 就陷入了循环 retain:
A → B ↔ C
如果 A release B,B 不会真正释放,因为 C 依然持有 B。C 也不能被释放,因为 B 持有 C。因为只有 A 能够引用到 B,所以一旦 A release B,就再也没有对象能够引用这个循环,这样就不可避免的造成内存泄露。这就是为什么在一个树结构中,一般是父节点 retain 子节点,而子节点不 retain 父节点。
垃圾收集器
Objective-C 2.0 实现了一个垃圾收集器。换句话说,你可以将所有内存管理交给垃圾收集器,再也不用关心什么 retain、release 之类。但是,不同于 Java,Objective-C 的垃圾收集器是可选的:你可以选择关闭它,从而自己管理对象的生命周期;或者你选择打开,从而减少很多可能有 bug 的代码。垃圾收集器是以一个程序为单位的,因此,打开或者关闭都会影响到整个应用程序。
如果开启垃圾收集器,retain、release 和autorelease 都被重定义成什么都不做。因此,在没有垃圾收集器情况下编写的代码可以不做任何改变地移植到有垃圾收集器的环境下,理论上只要重新编译一遍就可以了。“理论上”意思是,很多情况下涉及到资源释放处理的时候还是需要特别谨慎地对待。因此,编写同时满足两种情况的代码是不大容易的,一般开发者都会选择重新编写。下面,我们将逐一解释这两者之间的区别,这些都是需要特别注意的地方。
finalize
在有垃圾收集器的环境下,对象的析构顺序是未定义的,因此使用 dealloc 就不大适合了。NSObject 增加了一个 finalize 方法,将析构过程分解为两步:资源释放和有效回收。一个好的 finalize 方法是相当精妙的,需要很好的设计。
weak, strong
很少会见到 __weak 和 __strong 出现在声明中,但我们需要对它们有一定的了解。
默认情况下,一个指针都会使用 __strong 属性,表明这是一个强引用。这意味着,只要引用存在,对象就不能被销毁。这是一种所期望的行为:当所有(强)引用都去除时,对象才能被收集和释放。不过,有时我们却希望禁用这种行为:一些集合类不应该增加其元素的引用,因为这会引起对象无法释放。在这种情况下,我们需要使用弱引用(不用担心,内置的集合类就是这么干的),使用 __weak 关键字。NSHashTable 就是一个例子。当被引用的对象消失时,弱引用会自动设置为 nil。Cocoa 的Notification Center 就是这么一个例子,虽然这已经超出纯Objective-C 的语言范畴。
NSMakeCollectable()
Cocoa 并不是 Mac OS X 唯一的 API。Core Foundation 就是另外一个。它们是兼容的,可以共享数据和对象。但是 Core Foudation 是由纯 C 编写的。或许你会认为,Objective-C 的垃圾收集器不能处理 Core Foundation 的指针。但实际上是可以的。感兴趣的话可以关注一下 NSMakeCollectable 的文档。
AutoZone
由 Apple 开发的 Objective-C 垃圾收集器叫做 AutoZone。这是一个公开的开源库,我们可以看到起源代码。不过在 Mac OS X 10.6 中,垃圾收集器可能有了一些变化。这里对此不再赘述。
从 C++ 到Objective-C(17):异常处理和多线程
异常处理
比起 C++ 来,Objective-C中的异常处理更像 Java,这主要是因为 Objective-C 有一个 @finally 关键字。Java 中也有一个类似的 finally 关键字,但 C++ 中则没有。finally 是 try()…catch() 块的一个可选附加块,其中的代码是必须执行的,不管有没有捕获到异常。这种设计可以很方便地写出简短干净的代码,比如资源释放等。除此之外,Objective-C 中的 @try…@catch…@finally 是很经典的设计,同大多数语言没有什么区别。但是,不同于 C++ 的还有一点,Objective-C 只有对象可以被抛除。
不带 finally | 带有 finally |
BOOL problem = YES; @try { dangerousAction(); problem = NO; } @catch (MyException* e) { doSomething(); cleanup(); } @catch (NSException* e) { doSomethingElse(); cleanup(); // 重新抛出异常 @throw } if (!problem) cleanup(); | @try { dangerousAction(); } @catch (MyException* e) { doSomething(); } @catch (NSException* e) { doSomethingElse(); @throw // 重新抛出异常 } @finally { cleanup(); } |
严格说来,@finally 不是必要的,但是确实是处理异常强有力的工具。正如前面的例子所示,我们也可以在 @catch 中将异常重新抛出。事实上,@finally 在 @try 块运行结束之后才会执行。对此我们将在下面进行解释。
int f(void)
{
printf("f: 1-you see me\n");
// 注意看输出的字符串,体会异常处理流程
@throw [NSException exceptionWithName:@"panic"
reason:@"you don’t really want to known"
userInfo:nil];
printf("f: 2-you never see me\n");
}
int g(void)
{
printf("g: 1-you see me\n");
@try {
f();
printf("g: 2-you do not see me (in this example)\n");
} @catch(NSException* e) {
printf("g: 3-you see me\n");
@throw;
printf("g: 4-you never see me\n");
} @finally {
printf("g: 5-you see me\n");
}
printf("g: 6-you do not see me (in this example)\n");
}
最后一点,C++ 的 catch(…) 可以捕获任意值,但是Objective-C 中是不可以的。事实上,只有对象可以被抛出,也就是说,我们可以始终使用 id 捕获异常。
另外注意,Cocoa 中有一个 NSException 类,推荐使用此类作为一切异常类的父类。因此,catch(NSException *e) 相当于 C++ 的 catch(…)。
多线程
线程安全
在Objective-C 中可以很清晰地使用 POSIX APIs 2 实现多线程。Cocoa 提供了自己的类管理多线程。有一点是需要注意的:多个线程同时访问同一个内存区域时,可能会导致不可预料的结果。POSIX APIs 和 Cocoa 都提供了锁和互斥对象。Objective-C提供了一个关键字 @synchronized,与 Java 的同名关键字是一样的。
@synchronized
由 @synchronized(…) 包围的块会自动加锁,保证一次只有一个线程使用。在处理并发时,这并不是最好的解决方案,但却是对大多数关键块的最简单、最轻量、最方便的解决方案。@synchonized 要求使用一个对象作为参数(可以是任何对象,比如 self),将这个对象作为锁使用。
@implementation MyClass
-(void) criticalMethod:(id) anObject {
@synchronized(self) {
// 这段代码对其他 @synchronized(self) 都是互斥的
// self 是同一个对象
}
@synchronized(anObject) {
// 这段代码对其他 @synchronized(anObject) 都是互斥的
// anObject 是同一个对象
}
}
@end
从 C++ 到Objective-C(18):字符串和 C++ 特性
字符串
Objective-C 中唯一的 static 对象
在 C 语言中,字符串就是字符数组,使用char* 指针。处理这种数据非常困难,并且可能引起很多 bug。C++ 的 string 类是一种解脱。在 Objective-C 中,前面我们曾经介绍过,所有对象都不是自动的,都要在运行时分配内存。唯一不符合的就是 static 字符串。这导致可以使用 static 的 C 字符串作为 NSString 的参数。不过这并不是一个好的主意,可能会引起内存浪费。幸运的是,我们也有 static 的 Objective-C 字符串。在使用引号标记的 C 字符串前面加上 @ 符号,就构成了 static 的 Objective-C 字符串。
NSString* notHandy = [[NSString alloc] initWithUTF8String:"helloWorld"];
NSString* stillNotHandy = // initWithFormat 类似 sprintf()
[[NSString alloc] initWithFormat:@"%s", "helloWorld"];
NSString* handy = @"hello world";
另外,static 字符串可以同普通对象一样作为参数使用。
int size = [@"hello" length];
NSString* uppercaseHello = [@"hello" uppercaseString];
NSString 和编码
NSString 对象非常有用,因为它增加了很多好用的方法,并且支持不同的编码,如 ASCII、UNICODE、ISO Latin 1等。因此,翻译和本地化应用程序也变得很简单。
对象描述,%@扩展,NSString 转 C 字符串
在 Java 中,每一个对象都继承自 Object,因此都有一个 toString 方法,用于使用字符串形式描述对象本身。这种功能对于调试非常有用。Objective-C 中,类似的方法叫做 description,返回一个 NSString 对象。
C 语言的 printf 函数不能输出 NSString。我们可以使用 NSLog 获得类似的功能。NSLog 类似于 printf,可以向控制台输出格式化字符串。需要注意的是,NSString 的格式化符号是 %@,不是 %s。事实上,%@ 可以用于任意对象,因为它实际是调用的 -(NSString*) description。
NSString 可以使用 UTF8String 方法转换成 C 风格字符串。
char* name = "Spot";
NSString* action1 = @"running";
printf("My name is %s, I like %s, and %s...\n",
name, [action1 UTF8String], [@"running again" UTF8String]);
NSLog(@"My name is %s, I like %@ and %@\n",
name, action1, @"running again");
C++ 特性
现在,你已经了解到 C++ 的面向对象概念在 Objective-C 中的描述。但是,另外一些 C++ 的概念并没有涉及。这些概念并不相关面向对象,而是关于一些代码编写的问题。
引用
Objective-C 中不存在引用(&)的概念。由于Objective-C 使用引用计数器和autorelease 管理内存,这使得引用没有多大用处。既然对象都是动态分配的,它们唯一的引用就是指针。
内联
Objective-C 不支持内联 inline。对于方法而言,这是合理的,因为Objective-C 的动态性使得“冻结”某些代码变得很困难。但是,内联对某些用 C 编写的函数,比如 max(), min() 还是比较有用的。这一问题在 Objective-C++ (这是另外一种类似的语言)中得到解决。
无论如何,GCC 编译器还是提供了一个非标准关键字 __inline 或者 __inline__,允许在 C 或者 Objective-C 中使用内联。另外,GCC 也可以编译 C99 代码,在 C99 中,同样提供了内联关键字 inline(这下就是标准的了)。因此,在基于 C99的 Objective-C 代码中是可以使用内联的。如果不是为了使用而使用内联,而是关心性能,那么你应该考虑 IMP 缓存。
模板
模板是独立于继承和虚函数的另外一种机制,主要为性能设计,已经超出了纯粹的面向对象模型(你注意到使用模板可以很巧妙的访问到 private 变量吗?)。Objective-C 不支持模板,因为其独特的方法名规则和选择器使得模板很难实现。
运算符重载
Objective-C 不支持运算符重载。
友元
Objective-C 没有友元的概念。事实上,在 C++ 中,友元很大程度上是为了实现运算符重载。Java 中包的概念在一定程度上类似友元,这可以使用分类来处理。
const 方法
Objective-C 中方法不能用 const 修饰。因此也就不存在 mutable 关键字。
初始化列表
Objective-C 中没有初始化列表的概念。
从 C++ 到Objective-C(19):STL 和 Cocoa
C++ 标准库是其强大的一个原因。即使它还有一些不足,但是已经能够算作是比较完备的了。这并不是语言的一部分,而是属于一种扩展,其他语言也有类似的部分。在 Objective-C 中,你不得不在 Cocoa 里面寻找容器、遍历器或者其他一些真正可以使用的算法。
容器
Cocoa 的容器比 C++ 更加面向对象,它不使用模板实现,只能存放对象。现在可用的容器有:
· NSArray 和 NSMutableArray:有序集合;
· NSSet 和 NSMutableSet:无序集合;
· NSDictionary和 NSMutableDictionary:键值对形式的关联集合;
· NSHashTable:使用弱引用的散列表(Objective-C 2.0 新增)。
你可能会发现这其中并没有 NSList 或者 NSQueue。事实上,这些容器都可以由 NSArray 实现。
不同于 C++ 的 vector<T>,Objective-C 的 NSArray 真正隐藏了它的内部实现,仅能够使用访问器获取其内容。因此,NSArray 没有义务为内存单元优化其内容。NSArray的实现有一些妥协,以便 NSArray 能够像数组或者列表一样使用。既然 Objective-C 的容器只能存放指针,单元维护就会比较有效率了。
NSHashTable 等价于 NSSet,但它使用的是弱引用(我们曾在前面的章节中讲到过)。这对于垃圾收集器很有帮助。
遍历器
经典的枚举
纯面向对象的实现让 Objective-C 比 C++ 更容易实现遍历器。NSEnumerator就是为了这个设计的:
NSArray* array = [NSArray arrayWithObjects:object1, object2, object3, nil];
NSEnumerator* enumerator = [array objectEnumerator];
NSString* aString = @"foo";
id anObject = [enumerator nextObject];
while (anObject != nil)
{
[anObject doSomethingWithString:aString];
anObject = [enumerator nextObject];
}
容器的 objectEnumerator 方法返回一个遍历器。遍历器可以使用 nextObject 移动自己。这种行为更像 Java 而不是 C++。当遍历器到达容器末尾时,nextObject 返回 nil。下面是最普通的使用遍历器的语法,使用的 C 语言风格的简写:
NSArray* array = [NSArray arrayWithObjects:object1, object2, object3, nil];
NSEnumerator* enumerator = [array objectEnumerator];
NSString* aString = @"foo";
id anObject = nil;
while ((anObject = [enumerator nextObject])) {
[anObject doSomethingWithString:aString];
}
// 双括号能够防止 gcc 发出警告
快速枚举
Objective-C 2.0 提供了一个使用遍历器的新语法,隐式使用 NSEnumerator(其实和一般的 NSEnumerator 没有什么区别)。它的具体形式是:
NSArray* someContainer = ...;
for(id object in someContainer) { // 每一个对象都是用 id 类型
...
}
for(NSString* object in someContainer) { // 每一个对象都是 NSString
...// 开发人员需要处理不是 NSString* 的情况
}
函数对象
使用选择器
Objective-C 的选择器很强大,因而大大减少了函数对象的使用。事实上,弱类型允许用户无需关心实际类型就可以发送消息。例如,下面的代码同前面使用遍历器的是等价的:
NSArray* array = [NSArray arrayWithObjects:object1, object2, object3, nil];
NSString* aString = @"foo";
[array makeObjectsPerformSelector:@selector(doSomethingWithString:)
withObject:aString];
在这段代码中,每个对象不一定非得是 NSString 类型,并且对象也不需要必须实现了 doSomethingWithString: 方法(这会引发一个异常:selector not recognized)。
IMP 缓存
我们在这里不会详细解释这个问题,但是的确可以获得 C 函数的内存地址。通过仅查找一次函数地址,可以优化同一个选择器的多次调用。这被称为 IMP 缓存,因为 Objective-C 用于方法实现的数据类型就是 IMP。
调用 class_getMethodImplementation() 就可以获得这么一个指针。但是请注意,这是指向实现方法的真实的指针,因此不能有虚调用。它的使用一般在需要很好的时间优化的场合,并且必须非常小心。
算法
STL 中那一大堆通用算法在 Objective-C 中都没有对等的实现。相反,你应该仔细查找下各个容器中有没有你需要的算法。
从 C++ 到Objective-C(20):隐式代码
本章中心是两个能够让代码更简洁的特性。它们的目的截然不同:键值对编码可以通过选择第一个符合条件的实现而解决间接方法调用;属性则可以让编译器帮我们生成部分代码。键值对编码实际上是 Cocoa 引入的,而属性则是 Objective-C 2.0 语言新增加的。键值对编码(KVC)
原则
键值对编码意思是,能够通过数据成员的名字来访问到它的值。这种语法很类似于关联数组(在 Cocoa 中就是 NSDictionary),数据成员的名字就是这里的键。NSObject 有一个 valueForKey: 和 setValue:forKey: 方法。如果数据成员就是对象自己,寻值过程就会向下深入下去,此时,这个键应该是一个路径,使用点号 . 分割,对应的方法是 valueForKeyPath: 和 setValue:forKeyPath:。
@interface A {
NSString* foo;
}
... // 其它代码
@end
@interface B {
NSString* bar;
A* myA;
}
... // 其它代码
@end
@implementation B
...
// 假设 A 类型的对象 a,B 类型的对象 b
A* a = ...;
B* b = ...;
NSString* s1 = [a valueForKey:@"foo"]; // 正确
NSString* s2 = [b valueForKey:@"bar"]; // 正确
NSString* s3 = [b valueForKey:@"myA"]; // 正确
NSString* s4 = [b valueForKeyPath:@"myA.foo"]; // 正确
NSString* s5 = [b valueForKey:@"myA.foo"]; // 错误
NSString* s6 = [b valueForKeyPath:@"bar"]; // 正确
...
@end
这种语法能够让我们对不同的类使用相同的代码来处理同名数据。注意,这里的数据成员的名字都是使用的字符串的形式。这种使用方法的最好的用处在于将数据(名字)绑定到一些触发器(尤其是方法调用)上,例如键值对观察(Key-Value Observing, KVO)等。
拦截
通过 valueForKey: 或者 setValue:forKey: 访问数据不是原子操作。这个操作本质上还是一个方法调用。事实上,这种访问当某些方式实现的情况下才是可用的,例如使用属性自动添加的代码等等,或者显式允许直接访问数据。
Apple 的文档对 valueForKey: 和 setValue:forKey: 的使用有清晰的文档:
对于 valueForKey:@”foo” 的调用:
· 如果有方法名为 getFoo,则调用 getFoo;
· 否则,如果有方法名为 foo,则调用 foo(这是对常见的情况);
· 否则,如果有方法名为 isFoo,则调用 isFoo(主要是布尔值的时候);
· 否则,如果类的 accessInstanceVariablesDirectly 方法返回 YES,则尝试访问 _foo 数据成员(如果有的话),否则寻找 _isFoo,然后是 foo,然后是 isFoo;
· 如果前一个步骤成功,则返回对应的值;
· 如果失败,则调用 valueForUndefinedKey:,这个方法的默认实现是抛出一个异常。
对于 forKey:@”foo” 的调用:
· 如果有方法名为 setFoo:,则调用 setFoo:;
· 否则,如果类的 accessInstanceVariablesDirectly 返回 YES,则尝试直接写入数据成员 _foo(如果存在的话),否则寻找 _isFoo,然后是 foo,然后是 isFoo;
· 如果失败,则调用 setValue:forUndefinedKey:,其默认实现是抛出一个异常。
注意 valueForKey: 和 setValue:forKey: 的调用可以用于触发任何相关方法。如果没有这个名字的数据成员,则就是一个虚假的调用。例如, 在字符串变量上调用 valueForKey:@”length” 等价于直接调用 length 方法,因为这是 KVC 能够找到的第一个匹配。但是,KVC 的性能不如直接调用方法,所以应当尽量避免。
原型
使用 KVC 有一定的方法原型的要求:getters 不能有参数,并且要返回一个对象;setters 需要有一个对象作为参数,不能有返回值。参数的类型不是很重要的,因为你可以使用 id 作为参数类型。注意,struct 和原生类型(int,float 等)都是支持的:Objective-C 有一个自动装箱机制,可以将这些原生类型封装成 NSNumber 或者 NSValue 对象。因此,valueForKey: 返回值都是一个对象。如果需要向 setValue:forKey: 传入 nil,需要使用 setNilValueForKey:。
高级特性
有几点细节需要注意,尽管在这里并不会很详细地讨论这个问题:
1. keypath 可以包含计算值,例如求和、求平均、最大值、最小值等;使用 @ 标记;
2. 注意方法一致性,例如 valueForKey: 或者 setValue:forKey: 以及关联数组集合中常见的 objectForKey: 和 setObject:forKey:。这里,同样使用 @ 进行区分。
从 C++到 Objective-C(21):隐式代码(续)
属性
使用属性
在定义类时有一个属性的概念。我们使用关键字 @property 来标记一个属性,告诉编译器自动生成访问代码。属性的主要意义在于节省开发代码量。
访问属性的语法比方法调用简单,因此即使我们需要编写代码时,我们也可以使用属性。访问属性同方法调用的性能是一样的,因为属性的使用在编译期实际就是换成了方法调用。大多数时候,属性用于封装成员变量。但是,我们也可以提供一个“假”的属性,看似是访问一个数据成员,但实际不是;换句话说,看起来像是从对象外部调用一个属性,但实际上其实现要比一个值的管理操作要复杂得多。
属性的描述
对属性的描述实际上是要告诉编译器如何生成访问器的代码:
· 属性从外界是只读的吗?
· 如果数据成员是原生类型,可选余地不大;如果是对象,那么使用 copy 封装的话,是要用强引用还是弱引用?
· 属性是线程安全的吗?
· 访问器的名字是什么?
· 属性应该关联到哪一个数据成员?
· 应该自动生成哪一个访问器,哪一个则留给开发人员?
我们需要两个步骤来回答这些问题:
· 在类的@interface 块中,属性的声明需要提供附属参数;
· 在类的@implementation 块中,访问器可以隐式生成,也可以指定一个实现。
属性访问器是有严格规定的:getter 要求必须返回所期望的类型(或者是相容类型);setter 必须返回 void,并且只能有一个期望类型的参数。访问器的名字也是规定好的:对于数据 foo,getter 的名字是 foo,setter 的名字是 setFoo:。当然,我们也可以指定自定义的名字,但是不同于前面所说的键值对编码,这个名字必须在编译期确定,因为属性的使用被设计成要和方法的直接调用一样的性能。因此,如果类型是不相容的,是不会有装箱机制的。
以下是带有注释的例子,先来有一个大体的了解。
@interface class Car : NSObject
{
NSString* registration;
Person* driver;
}
// registration 是只读的,使用 copy 设置
@property NSString* (readonly, copy) registration;
// driver 使用弱引用(没有 retain),可以被修改
@property Person* (assign) driver;
@end
...
@implementation
// 开发者没有提供,由编译期生成 registration 的代码
@synthesize registration;
// 开发者提供了 driver 的 getter/setter 实现
@dynamic driver;
// 该方法将作为 @dynamic driver 的 getter
-(Person*) driver {
...
}
// 该方法将作为 @dynamic driver 的 setter
-(void) setDriver:(Person*)value {
...
}
@end
属性的参数
属性的声明使用一下模板:
@property type name;
或者
@property(attributes) type name;
如果没有给出属性的参数,那么将使用默认值;否则将使用给出的参数值。这些参数值可以是:
· readwrite(默认)或者 readonly:设置属性是可读写的(拥有 getter/setter)或是只读的(只有 getter);
· assign(默认),retain 或 copy:设置属性的存储方式;
· nonatomic:不生成线程安全的代码,默认是生成的(没有 atomic 关键字);
· getter=…,setter=…:改变访问器默认的名字。
对于 setter,默认行为是 assign;retain 或者 copy 用于数据成员被修改时的操作。在一个 -(void) setFoo:(Foo*)value 方法中,会因此生成三种不同的语句:
· self->foo= value ; // 简单赋值
· self->foo= [value retain]; // 赋值,同时引用计数器加 1
· self->foo= [value copy]; // 对象拷贝(必须满足协议 NSCopying)
在有垃圾收集器的环境下,retain 同 assign 没有区别,但是可以加上 __weak 或者 __strong。
@property(copy,getter=getS,setter=setF:) __weak NSString* s; // 复杂声明
注意不要忘记 setter 的冒号 : 。
从 C++到 Objective-C(22):隐式代码(续二)
属性的自定义实现
上一章中我们提到的代码中有两个关键字 @synthesize 和 @dynamic。@dynamic 意思是由开发人员提供相应的代码:对于只读属性需要提供 setter,对于读写属性需要提供 setter 和 getter。@synthesize 意思是,除非开发人员已经做了,否则由编译器生成相应的代码,以满足属性声明。对于上次的例子,如果开发人员提供了 -(NSString*)registration,编译器就会选择这个实现,不会用新的覆盖。因此,我们可以让编译器帮我们生成代码,以简化我们自己的代码输入量。最后,如果编译期没有找到访问器,而且没有使用 @synthesize 声明,那么它就会在运行时添加进来。这同样可以实现属性的访问,但是即使这样,访问器的名字也需要在编译期决定。如果运行期没有找到访问器,就会触发一个异常,但程序不会停止,正如同方法的缺失。当我们使用 @synthesize 时,编译器会被要求绑定某一特定的数据成员,并不一定是一样的名字。
@interface A : NSObject {
int _foo;
}
@property int foo;
@end
@implementation A
@synthesize foo=_foo; // 绑定 "_foo" 而不是 "foo"
@end
访问属性的语法
为获取或设置属性,我们使用点号:这同简单的 C 结构是一致的,也是在 keypath 中使用的语法,其性能与普通方法调用没有区别。
@interface A : NSObject {
int i;
}
@property int i;
@end
@interface B : NSObject {
A* myA;
}
@property(retain) A* a;
@end
...
A* a = ...
B* b = ...;
a.i = 1; // 等价于 [a setI:1];
b.myA.i = 1;// 等价于 [[b myA] setI:1];
请注意上面例子中 A 类的使用。self->i 和 self.i 是有很大区别的:self->i 直接访问数据成员,而 self.i 则是使用属性机制,是一个方法调用。
高级细节
64 位编译器上,Objective-C 运行时环境与 32 位有一些不同。关联到 @property 的实例数据可能被忽略掉,例如被视为隐式的。更多细节请阅读 Apple 的文档。
从 C++到 Objective-C(23):动态
RTTI (Run-Time Type Information)
RTTI 即运行时类型信息,能够在运行的时候知道需要的类型信息。C++ 有时被认为是一个“假的”面向对象语言。相比 Objective-C,C++ 显得非常静态。这有利于在运行时获得最好的性能。C++ 使用 typeinfo 库提供运行时信息,但这不是安全的,因为这个库依赖于编译器的实现。一般来说,查找对象的类型是一个很少见的请求,因为语言是强类型的,一般在编译时就已经确定其类型了;但是,有时候这种能力对于容器很常用。我们可以使用 dynamic_cast 和 typeid 运算符,但是程序交互则会在一定程度上受限。那么,如何由名字获知这个对象的类型呢?Objective-C 语言可以很容易地实现这种操作。类也是对象,它们继承它们的行为。
class,superclass, isMemberOfClass, isKindOfClass
对象在运行时获取其类型的能力称为内省。内省可以有多种方法实现。
isMemberOfClass: 可以用于回答这种问题:“我是给定类(不包括子类)的实例吗?”,而 isKindOfClass: 则是“我是给定类或其子类的实例吗?”使用这种方法需要一个“假”关键字的 class(注意,不是 @class,@class 是用于前向声明的)。事实上,class是 NSObject 的一个方法,返回一个 Class 对象。这个对象是元类的一个实例。请注意,nil 值的类是 Nil。
BOOL test = [self isKindOfClass:[Foo class]];
if (test)
printf("I am an instance of the Foo class\n");
注意,你可以使用 superclass 方法获取其父类。
conformsToProtocol
该方法用于确定一个对象是否和某一协议兼容。我们前面曾经介绍过这个方法。它并不是动态的。编译器仅仅检查每一个显式声明,而不会检查每一个方法。如果一个对象实现了给定协议的所有方法,但并没有显式声明说它实现了该协议,程序运行是正常的,但是 conformsToProtocol: 会返回 NO。
respondsToSelector,instancesRespondToSelector
respondsToSelector: 是一个实例方法,继承自 NSObject。该方法用于检查一个对象是否实现了给定的方法。这里如要使用 @selector。例如:
if ( [self respondsToSelector:@selector(work)] )
{
printf("I am not lazy.\n");
[self work];
}
如果要检查一个对象是否实现了给定的方法,而不检查继承的方法,可以使用类方法 instancesRespondToSelector:。例如:
if ([[self class] instancesRespondToSelector:@selector(findWork)])
{
printf("I can find a job without the help of my mother\n");
}
注意,respondsToSelector: 不能用于仅仅使用了前向声明的类。
强类型和弱类型 id
C++ 使用的是强类型:对象必须符合其类型,否则不能通过编译。在 Objective-C 中,这个限制就灵活得多了。如果一个对象与消息的目标对象不相容,编译器仅仅发出一个警告,而程序则继续运行。这个消息会被丢弃(引发一个异常),除非前面已经转发。如果这就是开发人员期望的,这个警告就是冗余的;在这种情形下,使用弱类型的 id 来替代其真实类型就可以消除警告。事实上,任何对象都是 id 类型的,并且可以处理任何消息。这种弱类型在使用代理的时候是必要的:代理对象不需要知道自己被使用了。例如:
-(void) setAssistant:(id)anObject
{
[assistant autorelease];
assistant = [anObject retain];
}
-(void) manageDocument:(Document*)document
{
if ([assistant respondToSelector:@(manageDocument:)])
[assistant manageDocument:document];
else
printf("Did you fill the blue form ?\n");
}
在 Cocoa 中,这种代理被大量用于图形用户界面的设计中。它可以很方便地把控制权由用户对象移交给工作对象。
运行时操作 Objective-C 类
通过添加头文件 <objc/objc-runtime.h>,我们可以调用很多工具函数,用于在运行时获取类信息、添加方法或实例变量。Objective-C 2.0 又引入了一些新函数,比 Objective-C 1.0 更加灵活(例如使用 class_addMethod(…) 替代 class_addMethods(…)),同时废弃了许多 1.0 的函数。这让我们可以很方便的在运行时修改类。
从 C++ 到Objective-C(24):结语
《从 C++ 到 Objective-C》系列已经结束。再次重申一下,本系列不是一个完整的 Objective-C 的教学文档,只是方便熟悉 C++ 或者类 C++ 的开发人员(例如广大的 Java 程序员)能够很快的使用 Objective-C 进行简单的开发。当然,目前 Objective-C 的最广泛应用在于 Apple 系列的开发,MacOS X、iOS 等。本系列仅仅介绍的是 Objective-C 语言本身,对于 Apple 系列的开发则没有很多的涉及。正如你仅仅知道 C++ 的语法,不了解各种各样的库是做不出什么东西的,学习 Objective-C 也不得不去了解 MacOS 或者 iOS 等更多的库的使用。这一点已经不在本系列的范畴内,这一点还请大家见谅。下面是本系列的目录:
1. 前言
2. 语法概述
3. 类和对象
4. 类和对象(续)
5. 类和对象(续二)
6. 类和对象(续三)
7. 继承
8. 继承(续)
9. 实例化
10. 实例化(续)
11. 实例化(续二)
12. 实例化(续三)
13. 内存管理
14. 内存管理(续)
15. 内存管理(续二)
16. 内存管理(续三)
17. 异常处理和多线程
18. 字符串和C++ 特性
19. STL 和 Cocoa
20. 隐式代码
21. 隐式代码(续)
22. 隐式代码(续二)
23. 动态
24. 结语