从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 使用 // 和 /*…*/ 两种注释风格。 app

变量声明的位置

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.  结语

相关文章
相关标签/搜索