从 C++ 到 Objective-C(1):前言



DevBean 日期: 2011 年 03 月 18 日

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

{

  

  

  

  

};



int Foo::f(int x) {...}



float Foo::g(int x, int y) {...}
@interface Foo : NSObject

{

  

}



-(int)  

-(float) g:(int)x :(int)y;



@end



@implementation Foo



-(int)  

-(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

{

  

public:

  

};



#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

{

  

}



-(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:

  

  

protected:

  

  

private:

  

  

};
@interface Foo : NSObject

{

@public:

  

@protected:

  

@private:

  

}



-(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

{

  

  



   void 

};



void Foo::f(void)

{

  

  

  

  

}
@interface Foo : NSObject

{

  

  

}



-(void) f;

@end



@implementation Foo



-(void) f

{

  

   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:
 
   
  
 
   
  
 
   
  
 
   
};
 
   

 
   
class Bar
 
   
{
 
   
public:
 
   
  
 
   
}

在 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:
  
};

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)
  

// 下面使用 performSelector: 示例:
for(i=0 ; i<10 ; ++i)
   [array[i] performSelector:@selector(readDocumentation:)
                 

// 选择器的类型是 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:)])



因此,选择器可被用作函数参数。通用算法,例如排序,就可以使用这种技术实现。

严格说来,选择器并不是一个函数指针。它的底层实现是一个 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)
{
  
  
   [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
 
   
{
 
   
  
 
   
  
 
   
}
 
   
// 方法 performHardWork 使用代理
 
   
-(void) performHardWork:(id)task
 
   
{
 
   
// assistant 在编译期是未知的
 
   
// 我们首先要检查它是否能够响应消息
 
   
if ([assistant respondsToSelector:@selector(performHardWork:)])
 
   
  
 
   
else
 
   
  
 
   
}


转发:处理未知消息

在 C++ 中,如果对象函数没有实现,是不能通过编译的。Objective-C 则不同,你可以向对象发送任何消息。如果在运行时无法处理,这个消息就被忽略了(同时会抛出一个异常)。除了忽略它,另外的处理办法是将消息转发给另外的对象。

当编译器被告知对象类型时,它可以知道对象可以处理哪些消息,因此就可以知道消息发出后是否会失败,也就可以抛出异常。这也就是为什么消息在运行时被执行,但是编译时就可以发出警告。这并不会引发错误,同时还有另外的选择:调用 forwardInvocation: 方法。这个方法可以将消息进行转发。这个方法是 NSObject 的,默认不做任何操作。下面代码就是一种实现:


-(void) forwardInvocation:(NSInvocation*)anInvocation
 
   
{
 
   
  // 如果该方法被调用,意味着我们无法处理这个消息
 
   
  // 错误的选择器(也就是调用失败的那个方法名)可以通过
 
   
  // 向 anInvocation 对象发送“selector” 获得
 
   
  
 
   
      
 
   
   else // 不要忘记调用父类的实现
 
   
      
 
   
}


即是在最后,这个消息在 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:@"; // 无警告