在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
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 引入。
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++ 中,属性和成员函数都在类的花括号块中被声明。方法的实现类似于 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)。
private,protected 和 public 访问可见性是面向对象语言的一个很重要的概念。它规定了在源代码级别哪些是可见的。可见性保证了类的封装性。
在 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->。
原型的 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++ 中,一个类可以继承自一个或多个类,使用 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 中,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 |
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论