从 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++ | |
头文件 | 源文件 |
|
|
Objective-C | |
头文件 | 源文件 |
|
|
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 |
|
|
在 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++ | |
|
|
Objective-C | |
|
|
private,protected 和 public
访问可见性是面向对象语言的一个很重要的概念。它规定了在源代码级别哪些是可见的。可见性保证了类的封装性。
C++ | Objective-C |
|
|
在 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 |
|
|
原型的 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:@"; // 无警告