从C++到objective-c[转]
2012-03-08 19:53
267 查看
来自:http://www.cnblogs.com/jacktu/archive/2011/11/06/2238353.html
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,你需要阅读更多资料。但是,相信在本系列基础之上,你在阅读其他资料时应该会理解的更加透彻一些。
说明:本系列大致翻译来自《FromC++toObjective-C》,你可以在这里找到它的英文pdf版本。
下面来简单介绍一下Objective-C。
要说Objective-C,首先要从Smalltalk说起。Smalltalk是第一个真正意义上的面向对象语言。Smalltalk出现之后,很多人都希望能在C语言的基础之上增加面向对象的特性。于是就出现了两种新语言:C++和Objective-C。C++不必多说,很多人都比较熟悉。Objective-C则比较冷门。它完全借鉴了Smalltalk的思想,有着类似的语法和动态机制;相比来说,C++则更加静态一些,目的在于提供能好的性能。Objective-C最新版本是2.0.我们的这个系列就是以Objective-C2.0为基础讲解。
Objective-C是一门语言,而Cocoa是这门语言用于MacOSX开发的一个类库。它们的关系类似于C++和Qt,Java和Spring一样。所以,我们完全可以不使用Cocoa,只去用Objective-C。例如gcc就是一个不使用Cocoa的编译器。不过在MacOSX平台,几乎所有的功能都要依赖Cocoa完成。我们这里只是做一个区别,应该分清Objective-C和Cocoa的关系。
另外的一些值同样也类似于关键字,有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的另外语义。
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*)的表示。
理解成
但实际上并不是这么简单。Objective-C是C语言的超集,因此,函数和C语言的声明、定义、调用是一致的。C语言并没有方法这一概念,因此方法是使用特殊语法,也就是方括号。不仅仅是语法上的,语义上也是不同的:这并不是方法调用,而是发送一条消息。看上去并没有什么区别,实际上,这是Objective-C的强大之处。例如,这种语法允许你在运行时动态添加方法。
Objetive-C使用的是严格的对象模型,相比之下,C++的对象模型则更为松散。例如,在Objective-C中,所有的类都是对象,并且可以被动态管理:也就是说,你可以在运行时增加新的类,根据类的名字实例化一个类,以及调用类的方法。这比C++的RTTI更加强大,而后者只不过是为一个“static”的语言增加的一点点功能而已。C++的RTTI在很多情况下是不被推荐使用的,因为它过于依赖编译器的实现,牺牲了跨平台的能力。
严格说来,每一个类都应该是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被称为实例数据instancedata,成员函数memberfunctions被称为方法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中声明;或者是使用分类机制(classcategories)。这样做虽然不能阻止方法被调用,但是减少了暴露。不经过声明实现一些方法是Objective-C的一种特殊属性,有着特殊的目的。我们会在后面进行说明。
Objective-C中的继承只能是public的,不可以是private和protected继承。这一点,Objective-C更像Java而不是C++。
static属性
Objective-C中不允许声明static属性。但是,我们有一些变通的方法:在实现文件中使用全局变量(也可以添加static关键字来控制可见性,类似C语言)。这样,类就可以通过方法访问到,而这样的全局变量的初始化可以在类的initialize方法中完成。
原型、调用、实例方法和类方法
·以–开头的是实例方法(多数情况下都应该是实例方法);以+开头的是类方法(相当于C++里面的static函数)。Objective-C的方法都是public的;
·返回值和参数的类型都需要用小括号括起来;
·参数之间使用冒号:分隔;
·参数可以与一个标签label关联起来,所谓标签,就是在:之前的一个名字。标签被认为是方法名字的一部分。这使得方法比函数更易读。事实上,我们应该始终使用标签。注意,第一个参数没有标签,通常它的标签就是指的方法名;
·方法名可以与属性名相同,这使getter方法变得很简单。
C++
Objective-C(不带label,即直接从C++翻译来)
Objective-C(带有label)
注意,方括号语法不应该读作“调用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++
在Objective-C中,所有的函数都是普通的C函数,不能被重载(除非指定使用C99标准)。方法则具有不同的语法,重载的依据是label。
Objective-C
基于label的重载可以很明白地解释方法的名字,例如:
显然,Objective-C的方法使用label区分,而不是类型。利用这种机制,我们就可以使用选择器selector来指定一个方法,而不是“成员函数指针”。
在Objective-C中,方法具有包含了括号和标签的特殊语法。普通的函数不能使用这种语法。在Objective-C和C语言中,函数指针具有相同的概念,但是对于成员函数指针则有所不同。
在C++中,尽管语法很怪异,但确实兼容C语言的:成员函数指针也是基于类型的。
C++
classFoo
{
public:
intf(floatx){...}
};
Foobar
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)aSelectorwithObject:(id)anObjectAsParameter;
-(id)performSelector:(SEL)aSelectorwithObject:(id)anObjectAsParameter
withObject:(id)anotherObjectAsParameter;
这些方法的返回值同被调用的函数的返回值是一样的。对于那些参数不是对象的方法,应该使用该类型的包装类,如NSNumber等。NSInvocation也有类似的功能,并且更为强大。
按照前面的说法,我们没有任何办法阻止在一个对象上面调用方法,即便该对象并没有实现这个方法。事实上,当消息被接收到之后,方法会被立即触发。但是,如果对象并不知道这个方法,一个可被捕获的异常将被抛除,应用程序并不会被终止。我们可以使用respondsToSelector:方法来检查对象是否可被触发方法。
最后,@selector的值是在编译器决定的,因此它并不会减慢程序的运行效率。
Objective-C
@interfaceSlave: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()是在编译器计算的
SELmethodSelector=@selector(readDocumentation:);
for(i=0;i<10;++i)
[slaves[i]performSelector:methodSelectorwithObject:document];
//对于一个对象“foo”,它的类型是未知的(id)
//这种测试并不是强制的,但是可以避免没有readDocumentation:方法时出现异常
if([foorespondsToSelector:@selector(readDocumentation:)])
[fooperformSelector:@selector(readDocumentation:)withObject:document];
因此,选择器可被用作函数参数。通用算法,例如排序,就可以使用这种技术实现。
严格说来,选择器并不是一个函数指针。它的底层实现是一个C字符串,在运行时被注册为方法的标识符。当类被加载之后,它的方法会被自动注册到一个表中,所以@selector可以很好的工作。根据这种实现,我们就可以使用==来判断内存地址是否相同,从而得出选择器是否相同,而无需使用字符串函数。
方法的真实地址,也就是看做C字符串的地址,其实可以看作是IMP类型(我们以后会有更详细的说明)。这种类型很少使用,除了在做优化的时候。例如虚调用实际使用选择器处理,而不是IMP。等价于C++函数指针的Objective-C的概念是选择器,也不是IMP。
最后,你应该记得我们曾经说过Objective-C里面的self指针,类似于C++的this指针,是作为每一个方法的隐藏参数传递的。其实这里还有第二个隐藏参数,就是_cmd。_cmd指的是当前方法。
@implementationFoo
-(void)f:(id)parameter//等价于C函数voidf(idself,SEL_cmd,idparameter)
{
idcurrentObject=self;
SELcurrentMethod=_cmd;
[currentObjectperformSelector:currentMethod
withObject:parameter];//递归调用
[selfperformSelector:_cmdwithObject:parameter];//也是递归调用
}
@end
参数的默认值
Objective-C不允许参数带有默认值。所以,如果某些参数是可选的,那么就应当创建多个方法的副本。在构造函数中,这一现象成为指定构造函数(designatedinitializer)。
可变参数
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不能这么做。
默认情况下,给nil发送消息也是合法的,只不过这个消息被忽略掉了。这种机制可以避免很多检查指针是否为空的情况。不过,有些编译器,比如GCC,也允许你通过编译参数的设置关闭这一特性。
将消息代理给未知对象
代理delegation是Cocoa框架中UI元素的一个很常见的部分。代理可以将消息转发给一个未知的对象。通过代理,一个对象可以将一些任务交给另外的对象。
转发:处理未知消息
在C++中,如果对象函数没有实现,是不能通过编译的。Objective-C则不同,你可以向对象发送任何消息。如果在运行时无法处理,这个消息就被忽略了(同时会抛出一个异常)。除了忽略它,另外的处理办法是将消息转发给另外的对象。
当编译器被告知对象类型时,它可以知道对象可以处理哪些消息,因此就可以知道消息发出后是否会失败,也就可以抛出异常。这也就是为什么消息在运行时被执行,但是编译时就可以发出警告。这并不会引发错误,同时还有另外的选择:调用forwardInvocation:方法。这个方法可以将消息进行转发。这个方法是NSObject的,默认不做任何操作。下面代码就是一种实现:
即是在最后,这个消息在forwardInvocation:中被处理,respondsToSelector:还是会返回NO。事实上,respondsToSelector:并不是用来检查forwardInvocation:是否被调用的。
使用这种转发机制有时候被认为是一种不好的习惯,因为它会隐藏掉本应引发错误的代码。事实上,一些很好的设计同样可以使用这种机制实现,例如Cocoa的NSUndoManager。它允许一种对异常友好的语法:undomanager可以记录方法调用历史,虽然它并不是那些调用的接收者。
向下转型
C++中,父类指针调用子类的函数时,需要有一个向下转型的操作(downcasting),使用dynamic_cast关键字。在Objective-C中,这是不必要的。因为你可以将任何消息发送给任何对象。但是,为了避免编译器的警告,我们也可以使用简单的转型操作。Objective-C中没有类似C++的专门的向下转型的操作符,使用C风格的转型语法就可以了。
在C++中,一个类可以继承自一个或多个类,使用public、protected以及private修饰符。子类的函数如果要调用父类的版本,需要使用::运算符,例如Bar::,Wiz::等。
在Objective-C中,一个类只能继承一个父类,并且只能是public的(这和Java是一致的)。同样类似Java,如果你要在子类中调用父类的函数,需要使用super。
在Objective-C中,所有方法都是虚的,因此,没有virtual关键字或其等价物。
虚方法重定义
在Objective-C中,你可以定义一个没有在@interface块里面声明的方法。但这并不是一种替代private的机制,因为这种方法实际是能够被调用的(回想下,Objective-C中方法的调用是在运行期决定的)。不过,这确实能够把接口定义变得稍微干净了一些。
这并不是一种坏习惯,因为有时你不得不重定义父类的函数。由于所有方法都是虚的,你无需像C++一样在声明中显式写明哪些函数是virtual的,这种做法就成为一种隐式的重定义。很多继承西NSObject的方法都是是用这种方法重定义的。例如构造方法init,析构方法dealloc,view类的drawRect:等等。这样的话,接口就变得更简洁,更易于阅读。不好之处就是,你不能知道究竟哪些方法被重定义了。
纯虚方法则是使用正式协议formalprotocols来实现。
虚继承
Objective-C中不允许多重继承,因此也就没有虚继承的问题。
正式协议
正式协议的方法,所有实现这个协议的类都必须实现。这就是一种验证,也就是说,只要这个类说实现这个协议,那么它肯定可以处理协议中规定的方法。一个类可以实现任意多个协议。
C++
Objective-C
C++中,协议可以由抽象类和纯虚函数实现。C++的抽象类要比Objective-C的协议强大的多,因为抽象类可以带有数据。
Objective-C中,协议是一个特殊的概念,使用尖括号<…>表明。注意,尖括号在Objective-C中不是模板的意思,Objective-C中没有类似C++模板的概念。
一个类也可以不经过协议声明,直接实现协议规定的方法。此时,conformsToProtocol:方法依然返回NO。出于性能考虑,conformsToProtocol:方法只检查类接口的声明,不会一个方法一个方法的对比着检查。conformsToProtocol:的返回值并不会作为是否调用方法的依据。下面是这个方法的原型:
实现了正式协议的对象的类型同协议本身是兼容的。这一机制可以作为协议的筛选操作。例如:
可选方法
有时我们需要这么一种机制:我们的类需要实现一部分协议中规定的方法,而不是整个协议。例如在Cocoa中,代理的概念被广泛使用:一个类可以给定一个辅助类,由这个辅助类去完成部分任务。
一种实现是将一个协议分割成很多小的协议,然后这个类去实现一个协议的集合。不过这并不具有可操作性。更好的解决方案是使用非正式协议。在Objective-C1.0中就有非正式协议了,Objective-C2.0则提出了新的关键字@optional和@required,用以区分可选方法和必须方法。
非正式协议
非正式协议并不是真正的协议,它对代码没有约束力。非正式协议允许开发者将一些方法进行归类,从而可以更好的组织代码。所以,非正式协议并不是协议的宽松版本。另外一个相似的概念就是分类。
让我们想象一个文档管理的服务。假设有绿色、蓝色和红色三种文档,一个类只能处理蓝色文档,而Slave类使用三个协议manageBlueDocuments,manageGreenDocuments和manageRedDocuments。Slave可以加入一个分类DocumentsManaging,用来声明它能够完成的任务。分类名在小括号中被指定:
任何类都可以加入DocumentsManaging分类,加入相关的处理方法:
另一个开发者就可以浏览源代码,找到了DocumentsManaging分类。如果他觉得这个分类中有些方法可能对自己,就会检查究竟哪些能够使用。即便他不查看源代码,也可以在运行时指定:
严格说来,除了原型部分,非正式协议对编译器没有什么意义,因为它并不能约束代码。不过,非正式协议可以形成很好的自解释性代码,让API更具可读性。
运行时,协议就像是类对象,其类型是Protocol*。例如,conformsToProtocol:方法就需要接受一个Protocol*类型的参数。@protocol关键字不仅用于声明协议,还可以用于根据协议名返回Protocol*对象。
远程对象的消息传递
由于Objective-C的动态机制,远程对象之间的消息传递变得很简单。所谓远程对象,是指两个或多个处于不同程序,甚至不同机器,但是可以通过代理完成同一任务,或者交换信息的对象。正式协议就是一种可以确保对象提供了这种服务的有效手段。正式协议还提供了很多额外的关键字,可以更好的说明各种参数。这些关键字分别是in,out,inout,bycopy,byref和oneway。这些关键字仅对远程对象有效,并且仅可以在协议中使用。出了协议,它们就不被认为是关键字。这些关键字被插入到在协议中声明的方法原型之中,提供它们所修饰的参数的额外信息。它们可以告知,哪些是输入参数,哪些是输出参数,哪些使用复制传值,哪些使用引用传值,方法是否是同步的等等。以下是详细说明:
·in:参数是输入参数;
·out:参数是输出参数;
·inout:参数即是输入参数,又是输出参数;
·bycopy:复制传值;
·byref:引用传值;
·oneway:方法是异步的,也就是不会立即返回,因此它的返回值必须是void。
例如,下面就是一个返回对象的异步方法:
默认情况下,参数都被认为是inout的。如果参数由const修饰,则被当做in参数。为参数选定是in还是out,可以作为一种优化手段。参数默认都是传引用的,方法都是同步的(也就是不加oneway)。对于传值的参数,也就是非指针类型的,out和inout都是没有意义的,只有in是正确的选择。
·对于精益求精的开发者,分类提供了一种划分方法的机制。对于一个很大的类,它可以将其划分成不同的角色;
·分类允许分开编译,也就是说,同一个类也可以进行多人的分工合作;
·如果把分类的声明放在实现文件(.m)中,那么这个分类就只在文件作用域中可见(虽然这并没有调用上的限制,如果你知道方法原型,依然可以调用)。这样的分类可以取一个合适的名字,比如FooPrivateAPI;
·一个类可以在不同程序中有不同的扩展,而不需要丢弃通用代码。所有的类都可以被扩展,甚至是Cocoa中的类。
最后一点尤其重要。很多开发人员都希望标准类能够提供一些对他们而言很有用的方法。这并不是一个很困难的问题,使用继承即可实现。但是,在单继承的环境下,这会造成出现很多的子类。仅仅为了一个方法就去继承显得有些得不偿失。分类就可以很好的解决这个问题:
在C++中,这是一个全新的类,可以自由使用。
在Objective-C中,NSString是Cocoa框架的一个标准类。它是使用分类机制进行的扩展,只能在当前程序中使用。注意此时并没有新增加类。每一个NSString对象都可以从这个扩展获得统计元音数目的能力,甚至常量字符串也可以。同时注意,分类不能增加实例数据,因此没有花括号块。
分类也可以使匿名的,更适合于private的实现:
在C++中,变量默认是“自动的”:除非被声明为static,否则变量仅在自己的定义块中有意义。动态分配的内存可以一直使用,直到调用了free()或者delete。C++中,所有对象都遵循这一规则。
然而在Objective-C中,所有对象都是动态分配的。其实这也是符合逻辑的,因为C++更加static,而Objective-C则更加动态。除非能够在运行时动态分配内存,否则Objective-C实现不了这么多动态的特性。
在C++中,内存分配和对象初始化都是在构造函数中完成的。在Objective-C中,这是两个不同的函数。
内存分配由类方法alloc完成,此时将初始化所有的实例数据。实例数据将被初始化为0,除了一个名为isa的NSObject的指针。这个指针将在运行时指向对象的实际类型。实例数据根据传入的参数初始化为某一特定的值,这一过程将在一个实例方法instancemethod中完成。这个方法通常命名为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++
Objective-C
为检查内存分配是否成功,C++可以判断new返回的指针是否是0(如果使用的是new(nothrow)运算符)。在Objective-C中,检查返回值是否是nil就已经足够了。
初始化方法的正确示例代码
一个正确的初始化方法应该有如下特点:
·名字以init开始;
·返回能够使用的对象;
·调用父类的init方法,直到NSObject的init方法被调用;
·保存[superinit...]的返回值;
·处理构造期间出现的任何错误,无论是自己的还是父类的。
下面是一些代码:
C++
Objective-C
在上一篇提到的代码中,最不可思议的可能就是这句self=[superinit...]。回想一下,self是每个方法的一个隐藏参数,指向当前对象。因此,这是一个局部变量。那么,为什么我们要改变一个局部变量的值呢?事实上,self必须要改变。我们将在下面解释为什么要这样做。
[superinit]实际上返回不同于当前对象的另外一个对象。单例模式就是这样一种情况。然而,有一个API可以用一个对象替换新分配的对象。CoreData(Apple提供的Cocoa里面的一个API)就是用了这种API,对实例数据做一些特殊的操作,从而让这些数据能够和数据库的字段关联起来。当继承NSManagedObject类的时候,就需要仔细对待这种替换。在这种情形下,self就要指向两个对象:一个是alloc返回的对象,一个是[superinit]返回的对象。修改self的值对代码有一定的影响:每次访问实例数据的时候都是隐式的。正如下面的代码所示:
@interfaceB:A
{
inti;
}
@end
@implementationB
-(id)init
{
//此时,self指向alloc返回的值
//假设A进行了替换操作,返回一个不同的self
idnewSelf=[superinit];
NSLog(@"%d",i);//输出self->i的值
self=newSelf;//有人会认为i没有变化
NSLog(@"%d",i);//事实上,此时的self->i,实际是newSelf->i,
//和之前的值可能不一样了
returnself;
}
@end
...
B*b=[[Balloc]init];
self=[superinit]简洁明了,也不必担心以后会引入bug。然而,我们应该注意旧的self指向的对象的命运:它必须被释放。第一规则很简单:谁替换self指针,谁就要负责处理旧的self指针。在这里,也就是[superinit]负责完成这一操作。例如,如果你创建NSManagedObject子类(这个类会执行替换操作),你就不必担心旧的self指针。事实上,NSManagedObject的开发者必须考虑这种处理。因此,如果你要创建一个执行替换操作的类,你必须知道如何在初始化过程中释放旧有对象。这种操作同错误处理很类似:如果因为非法参数、不可访问的资源造成构造失败,我们要如何处理?
初始化错误
初始化出错可能发生在三个地方:
1.调用[superinit...]之前:如果构造函数参数非法,那么初始化应该立即停止;
2.调用[superinit...]期间:如果父类调用失败,那么当前的初始化操作也应该停止;
3.调用[superinit...]之后:例如资源分配失败等。
在上面每一种情形中,只要失败,就应该返回nil;相应的处理应该由发生错误的对象去完成。这里,我们主要关心的是1,3情况。要释放当前对象,我们调用[selfrelease]即可。
在调用dealloc之后,对象的析构才算完成。因此,dealloc的实现必须同初始化方法兼容。事实上,alloc将所有的实例数据初始化成0是相当有用的。
@interfaceA:NSObject{
unsignedintn;
}
-(id)initWithN:(unsignedint)value;
@end
@implementationA
-(id)initWithN:(unsignedint)value
{
//第一种情况:参数合法吗?
if(value==0)//我们需要一个正值
{
[selfrelease];
returnnil;
}
//第二种情况:父类调用成功吗?
if(!(self=[superinit]))//即是self被替换,它也是父类
returnnil;//错误发生时,谁负责释放self?
//第三种情况:初始化能够完成吗?
n=(int)log(value);
void*p=malloc(n);//尝试分配资源
if(!p)//如果分配失败,我们希望发生错误
{
[selfrelease];
returnnil;
}
}
@end
将构造过程合并为alloc+init
有时候,alloc和init被分割成两个部分显得很罗嗦。幸运的是,我们也可以将其合并在一起。这主要牵扯到Objective-C的内存管理机制。简单来说,作为一个构造函数,它的名字必须以类名开头,其行为类似init,但要自己实现alloc。然而,这个对象需要注册到autorelease池中,除非发送retain消息,否则其生命周期是有限制的。以下即是示例代码:
//啰嗦的写法
NSNumber*tmp1=[[NSNumberalloc]initWithFloat:0.0f];
...
[tmp1release];
//简洁一些
NSNumber*tmp2=[NSNumbernumberWithFloat:0.0f];
...
//无需调用release
在Objective-C中,默认构造函数没有实在的意义,因为所有对象都是动态分配内存,也就是说,构造函数都是确定的。但是,一个常用的构造函数确实可以精简代码。事实上,一个正确的初始化过程通常类似于:
剪贴复制代码是一个不良习惯。好的做法是,将共同代码放到一个独立的函数中,通常称为“指定初始化函数”。通常这种指定初始化函数会包含很多参数,因为Objective-C不允许参数有默认值。
如果指定初始化函数没有最大数量的参数,那基本上就没什么用处:
初始化列表和实例数据的默认值
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中没有这种限制。你可以在Cocoa中使用自定义的内存区域,但是这并不会影响平常的内存的分配、释放机制。
C++
Objective-C
在C++中,定义复制运算符和相关的操作是很重要的。在Objective-C中,运算法是不允许重定义的,所能做的就是要求提供一个正确的复制函数。
克隆操作在Cocoa中要求使用NSCopying协议实现。该协议要求一个实现函数:
这个函数的参数是一个内存区,用于指明需要复制那一块内存。Cocoa允许使用不同的自定义区块。大多数时候默认的区块就已经足够,没必要每次都单独指定。幸运的是,NSObject有一个函数
封装了copyWithZone:,直接使用默认的区块作为参数。但它实际相当于NSCopying所要求的函数。另外,NSCopyObject()提供一个不同的实现,更简单但同样也需要注意。下面的代码没有考虑NSCopyObject():
注意,我们使用的是allocWithZone:而不是alloc。alloc实际上封装了allocWithZone:,它传进的是默认的zone。但是,我们应该注意父类的copyWithZone:的实现。
NSCopyObject()
NSObject事实上并没有实现NSCopying协议(注意函数的原型不同),因此我们不能简单地使用[supercopy...]这样的调用,而是类似[[...alloc]init]这种标准调用。NSCopyObject()允许更简单的代码,但是需要注意指针变量(包括对象)。这个函数创建一个对象的二进制格式的拷贝,其原型是:
二进制复制可以复制非指针对象,但是对于指针对象,需要时刻记住它会创建一个指针所指向的数据的新的引用。通常的做法是在复制完之后重置指针。
Dummy-cloning,mutability,mutableCopyandmutableCopyWithZone:
如果需要复制不可改变对象,一个基本的优化是假装它被复制了,实际上是返回一个原始对象的引用。从这点上可以区分可变对象与不可变对象。
不可变对象的实例数据不能被修改,只有初始化过程能够给一个合法值。在这种情况下,使用“伪克隆”返回一个原始对象的引用就可以了,因为它本身和它的复制品都不能够被修改。此时,copyWithZone:的一个比较好的实现是:
retain操作意味着将其引用加1。我们需要这么做,因为当原始对象被删除时,我们还会持有一个复制品的引用。
“伪克隆”并不是无关紧要的优化。创建一个新的对象需要进行内存分配,相对来说这是一个比较耗时的操作,如果可能的话应该注意避免这种情况。这就是为什么需要区别可变对象和不可变对象。因为不可变对象可以在复制操作上做文章。我们可以首先创建一个不可变类,然后再继承这个类增加可变操作。Cocoa中很多类都是这么实现的,比如NSMutableString是NSString的子类;NSMutableArray是NSArray的子类;NSMutableData是NSData的子类。
然而根据我们上面描述的内容,似乎无法从不可变对象安全地获取一个完全的克隆,因为不可变对象只能“伪克隆”自己。这个限制大大降低了不可变对象的可用性,因为它们从“真实的世界”隔离了出来。
除了NSCopy协议,还有一个另外的NSMutableCopying协议,其原型如下:
mutableCopyWithZone:必须返回一个可变的克隆,其修改不能影响到原始对象。类似NSObject的copy函数,也有一个mutableCopy函数使用默认区块封装了这个操作。mutableCopyWithZone:的实现类似前面的copyWithZone:的代码:
不要忘记我们可以使用父类的mutableCopyWithZone:
对象可以接收任意多的retain和release消息,只要计数器的值是正的。当计数器成0时,析构函数dealloc将被自动调用。此时再次发送release给这个对象就是非法的了,将引发一个内存错误。
这种技术并不同于C++STL的auto_ptr。Boost库提供了一个类似的引用计数器,称为shared_ptr,但这并不是标准库的一部分。
基本规则是,所有使用alloc,[mutable]copy[WithZone:]或者是retain增加计数器的对象都要用[auto]release释放。事实上,有三种方法可以增加引用计数器,也就意味着仅仅有有限种情况下才要使用release释放对象:
·使用alloc显式实例化对象;
·使用copy[WithZone:]或者mutableCopy[WithZone:]复制对象(不管这种克隆是不是伪克隆);
·使用retain。
记住,默认情况下,给nil发送消息(例如release)是合法的,不会引起任何后果。
前面我们强调了,所有使用alloc,[mutable]copy[WithZone:]或者是retain增加计数器的对象都要用[auto]release释放。事实上,这条规则不仅仅适用于alloc、retain和release。有些函数虽然不是构造函数,但也用于创建对象,例如C++的二元加运算符(obj3operator+(obj1,obj2))。在C++中,返回值可以在栈上,以便在离开作用域的时候可以自动销毁。但在Objective-C中不存在这种对象。函数使用alloc分配的对象,直到将其返回栈之前不能释放。下面的代码将解释这种情况:
这个问题看起来很棘手。如果没有autorelease的确如此。简单地说,给一个对象发送autorelease消息意味着告诉它,在“一段时间之后”销毁。但是这里的“一段时间之后”并不意味着“任何时间”。我们将在后面的章节中详细讲述这个问题。现在,我们有了上面这个问题的一种解决方案:
上一节中我们了解到autorelease的种种神奇之处:它能够在合适的时候自动释放分配的内存。但是如何才能让便以其之道什么时候合适呢?这种情况下,垃圾收集器是最好的选择。下面我们将着重讲解垃圾收集器的工作原理。不过,为了了解垃圾收集器,就不得不深入了解autorelease的机制。所以我们要从这里开始。当对象收到autorelease消息的时候,它会被注册到一个“autorelease池”。当这个池被销毁时,其中的对象也就被实际的销毁。所以,现在的问题是,这个池如何管理?
答案是丰富多彩的:如果你使用Cocoa开发GUI界面,基本不需要做什么事情;否则的话,你应该自己创建和销毁这个池。
拥有图形界面的应用程序都有一个事件循环。这个循环将等待用户动作,使应用程序响应动作,然后继续等待下一个动作。当你使用Cocoa创建GUI程序时,这个autorelease池在事件循环的一次循环开始时被自动创建,然后在循环结束时自动销毁。这是合乎逻辑的:一般的,一个用户动作都会触发一系列任务,临时变量的创建和销毁一般不会影响到下一个事件。如果必须要有可持久化的数据,那么你就要手动地使用retain消息。
另一方面,如果没有GUI,你必须自己建立autorelease池。当对象收到autorelease消息时,它能够找到最近的autorelease池。当池可以被清空时,你可以对这个池使用release消息。一般的,命令行界面的Cocoa程序都会有如下的代码:
intmain(intargc,char*argv[])
{
NSAutoreleasePool*pool=[[NSAutoreleasePoolalloc]init];
//...
[poolrelease];
return0;
}
注意在MacOSX10.5的NSAutoreleasePool类新增加了一个drain方法。这个方法等价于:当垃圾收集器可用时做release操作;否则则触发运行垃圾收集。这对编写在两种情况下都适用的代码时是很有用的。注意,这里实际上是说,现在有两种环境:引用计数和垃圾回收。MacOS的新版本都会支持垃圾收集器,但是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。
Convenienceconstructor,virtualconstructor
将构造对象的过程分成alloc和init两个阶段,有时候显得很罗嗦。好在我们有一个convenienceconstructor的概念。这种构造函数应该使用类名做前缀,其行为类似init,同时要实现alloc。但是,它的返回对象需要注册到一个内部的autorelease池,如果没有给它发送retain消息时,这个对象始终是一个临时对象。例如:
//啰嗦的写法
NSNumber*zero_a=[[NSNumberalloc]initWithFloat:0.0f];
...
[zero_arelease];
...
//简洁一些的
NSNumber*zero_b=[NSNumbernumberWithFloat:0.0f];
...
//不需要release
根据我们前面对内存管理的介绍,这种构造函数的实现是基于autorelease的。但是其底层代码并不那么简单,因为这涉及到对self的正确使用。事实上,这种构造函数都是类方法,所以self指向的是Class类型的对象,就是元类类型的。在初始化方法,也就是一个实例方法中,self指向的是这个类的对象的实例,也就是一个“普通的”对象。
编写错误的这种构造函数是很容易的。例如,我们要创建一个Vehicle类,包含一个color数据,编写如下的代码:
//TheVehicleclass
@interfaceVehicle:NSObject
{
NSColor*color;
}
-(void)setColor:(NSColor*)color;
//简洁构造函数
+(id)vehicleWithColor:(NSColor*)color;
@end
其对应的实现是:
//错误的实现
+(Vehicle*)vehicleWithColor:(NSColor*)color
{
//self不能改变
self=[[selfalloc]init];//错误!
[selfsetColor:color];
return[selfautorelease];
}
记住我们前面所说的,这里的self指向的是Class类型的对象。
//比较正确的实现
+(id)vehicleWithColor:(NSColor*)color
{
idnewInstance=[[Vehiclealloc]init];//正确,但是忽略了有子类的情况
[newInstancesetColor:color];
return[newInstanceautorelease];
}
我们来改进一下。Objective-C中,我们可以实现virtualconstructor。这种构造函数通过内省的机制来了解到自己究竟应该创建哪种类的对象,是这个类本身的还是其子类的。然后它直接创建正确的类的实例。我们可以使用一个class方法(注意,class在Objective-C中不是关键字);这是NSObject的一个方法,返回当前对象的类对象(也就是meta-class对象)。
@implementationVehicle
+(id)vehicleWithColor:(NSColor*)color
{
idnewInstance=[[[selfclass]alloc]init];//完美!我们可以在运行时识别出类
[newInstancesetColor:color];
return[newInstanceautorelease];
}
@end
@interfaceCar:Vehicle{...}
@end
...
//创建一个redCar
idcar=[CarvehicleWithColor:[NSColorredColor]];
类似于初始化函数的init前缀,这种简洁构造函数最好使用类名作前缀。不过也有些例外,例如[NSColorredColor]返回一个预定义的颜色,按照我们的约定,使用[NSColorcolorRed]更合适一些。
最后,我们要重复一下,所有使用alloc、[mutable]copy[WithZone:]增加引用计数器值的对象,都必须相应地调用[auto]release。当调用简洁构造函数时,你并没有显式调用alloc,也就不应该调用release。但是,在创建这种构造函数时,一定不要忘记使用autorelease。
如果不对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=[newStringretain];//使用retain指定
}
复制(不完整的代码)
外部对象实际没有被引用,使用的是其克隆。此时,外部对象的改变对于当前类是不可变的。也就是说,当前类持有的是这个对象的克隆,这个对象的生命周期不会比持有者更长。
-(void)setString:(NSString*)newString
{
...稍后解释内存方面的细节
self->string=[newStringcopy];//克隆
//使用NSCopying协议
}
为了补充完整这些代码,我们需要考虑这个对象在前一时刻的状态:每一种情形下,setter都需要释放掉旧的资源,然后建立新的。这些代码看起来比较麻烦。
直接指定(完整代码)
这是最简单的情况。旧的引用实际上被替换成了新的。
-(void)setString:(NSString*)newString
{
//没有强链接,旧值被改变了
self->string=newString;//直接指定
}
使用retain指定(完整代码)
在这种情况下,旧值需要被释放,除非旧值和新值是一样的。
//------不正确的实现------
-(void)setString:(NSString*)newString
{
self->string=[newStringretain];
//错误!内存泄露,没有引用指向旧的“string”,因此再也无法释放
}
-(void)setString:(NSString*)newString
{
[self->stringrelease];
self->string=[newStringretain];
//错误!如果newString==string(这是可能的),
//newString引用是1,那么在[self->stringrelease]之后
//使用newString就是非法的,因为此时对象已经被释放
}
-(void)setString:(NSString*)newString
{
if(self->string!=newString)
[self->stringrelease];//正确:给nil发送release是安全的
self->string=[newStringretain];//错误!应该在if里面
//因为如果string==newString,
//计数器不会被增加
}
//------正确的实现------
//最佳实践:C++程序员一般都会“改变前检查”
-(void)setString:(NSString*)newString
{
//仅在必要时修改
if(self->string!=newString){
[self->stringrelease];//释放旧的
self->string=[newStringretain];//retain新的
}
}
//最佳实践:自动释放旧值
-(void)setString:(NSString*)newString
{
[self->stringautorelease];//即使string==newString也没有关系,
//因为release是被推迟的
self->string=[newStringretain];
//...因此这个retain要在release之前发生
}
//最佳实践:先retain在release
-(void)setString:(NSString*)newString
{
[self->newStringretain];//引用计数器加1(除了nil)
[self->stringrelease];//release时不会是0
self->string=newString;//这里就不应该再加retain了
}
复制(完整代码)
无论是典型的误用还是正确的解决方案,都和前面使用retain指定一样,只不过把retain换成copy。
伪克隆
有些克隆是伪克隆,不过对结果没有影响。
Objective-C中,所有对象都是动态分配的,使用指针引用。一般的,getter仅仅返回指针的值,而不应该复制对象。getter的名字一般和数据成员的名字相同(这一点不同于Java,JavaBean规范要求以get开头),这并不会引起任何问题。如果是布尔变量,则使用is开头(类似JavaBean规范),这样可以让程序更具可读性。
当返回实例数据指针时,外界就可以很轻松地修改其值。这可能是很多getter不希望的结果,因为这样一来就破坏了封装性。
A→B↔C
如果AreleaseB,B不会真正释放,因为C依然持有B。C也不能被释放,因为B持有C。因为只有A能够引用到B,所以一旦AreleaseB,就再也没有对象能够引用这个循环,这样就不可避免的造成内存泄露。这就是为什么在一个树结构中,一般是父节点retain子节点,而子节点不retain父节点。
如果开启垃圾收集器,retain、release和autorelease都被重定义成什么都不做。因此,在没有垃圾收集器情况下编写的代码可以不做任何改变地移植到有垃圾收集器的环境下,理论上只要重新编译一遍就可以了。“理论上”意思是,很多情况下涉及到资源释放处理的时候还是需要特别谨慎地对待。因此,编写同时满足两种情况的代码是不大容易的,一般开发者都会选择重新编写。下面,我们将逐一解释这两者之间的区别,这些都是需要特别注意的地方。
finalize
在有垃圾收集器的环境下,对象的析构顺序是未定义的,因此使用dealloc就不大适合了。NSObject增加了一个finalize方法,将析构过程分解为两步:资源释放和有效回收。一个好的finalize方法是相当精妙的,需要很好的设计。
weak,strong
很少会见到__weak和__strong出现在声明中,但我们需要对它们有一定的了解。
默认情况下,一个指针都会使用__strong属性,表明这是一个强引用。这意味着,只要引用存在,对象就不能被销毁。这是一种所期望的行为:当所有(强)引用都去除时,对象才能被收集和释放。不过,有时我们却希望禁用这种行为:一些集合类不应该增加其元素的引用,因为这会引起对象无法释放。在这种情况下,我们需要使用弱引用(不用担心,内置的集合类就是这么干的),使用__weak关键字。NSHashTable就是一个例子。当被引用的对象消失时,弱引用会自动设置为nil。Cocoa的NotificationCenter就是这么一个例子,虽然这已经超出纯Objective-C的语言范畴。
NSMakeCollectable()
Cocoa并不是MacOSX唯一的API。CoreFoundation就是另外一个。它们是兼容的,可以共享数据和对象。但是CoreFoudation是由纯C编写的。或许你会认为,Objective-C的垃圾收集器不能处理CoreFoundation的指针。但实际上是可以的。感兴趣的话可以关注一下NSMakeCollectable的文档。
AutoZone
由Apple开发的Objective-C垃圾收集器叫做AutoZone。这是一个公开的开源库,我们可以看到起源代码。不过在MacOSX10.6中,垃圾收集器可能有了一些变化。这里对此不再赘述。
严格说来,@finally不是必要的,但是确实是处理异常强有力的工具。正如前面的例子所示,我们也可以在@catch中将异常重新抛出。事实上,@finally在@try块运行结束之后才会执行。对此我们将在下面进行解释。
最后一点,C++的catch(…)可以捕获任意值,但是Objective-C中是不可以的。事实上,只有对象可以被抛出,也就是说,我们可以始终使用id捕获异常。
另外注意,Cocoa中有一个NSException类,推荐使用此类作为一切异常类的父类。因此,catch(NSException*e)相当于C++的catch(…)。
在Objective-C中可以很清晰地使用POSIXAPIs2实现多线程。Cocoa提供了自己的类管理多线程。有一点是需要注意的:多个线程同时访问同一个内存区域时,可能会导致不可预料的结果。POSIXAPIs和Cocoa都提供了锁和互斥对象。Objective-C提供了一个关键字@synchronized,与Java的同名关键字是一样的。
@synchronized
由@synchronized(…)包围的块会自动加锁,保证一次只有一个线程使用。在处理并发时,这并不是最好的解决方案,但却是对大多数关键块的最简单、最轻量、最方便的解决方案。@synchonized要求使用一个对象作为参数(可以是任何对象,比如self),将这个对象作为锁使用。
在C语言中,字符串就是字符数组,使用char*指针。处理这种数据非常困难,并且可能引起很多bug。C++的string类是一种解脱。在Objective-C中,前面我们曾经介绍过,所有对象都不是自动的,都要在运行时分配内存。唯一不符合的就是static字符串。这导致可以使用static的C字符串作为NSString的参数。不过这并不是一个好的主意,可能会引起内存浪费。幸运的是,我们也有static的Objective-C字符串。在使用引号标记的C字符串前面加上@符号,就构成了static的Objective-C字符串。
另外,static字符串可以同普通对象一样作为参数使用。
NSString和编码
NSString对象非常有用,因为它增加了很多好用的方法,并且支持不同的编码,如ASCII、UNICODE、ISOLatin1等。因此,翻译和本地化应用程序也变得很简单。
对象描述,%@扩展,NSString转C字符串
在Java中,每一个对象都继承自Object,因此都有一个toString方法,用于使用字符串形式描述对象本身。这种功能对于调试非常有用。Objective-C中,类似的方法叫做description,返回一个NSString对象。
C语言的printf函数不能输出NSString。我们可以使用NSLog获得类似的功能。NSLog类似于printf,可以向控制台输出格式化字符串。需要注意的是,NSString的格式化符号是%@,不是%s。事实上,%@可以用于任意对象,因为它实际是调用的-(NSString*)description。
NSString可以使用UTF8String方法转换成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中没有初始化列表的概念。
·NSArray和NSMutableArray:有序集合;
·NSSet和NSMutableSet:无序集合;
·NSDictionary和NSMutableDictionary:键值对形式的关联集合;
·NSHashTable:使用弱引用的散列表(Objective-C2.0新增)。
你可能会发现这其中并没有NSList或者NSQueue。事实上,这些容器都可以由NSArray实现。
不同于C++的vector<T>,Objective-C的NSArray真正隐藏了它的内部实现,仅能够使用访问器获取其内容。因此,NSArray没有义务为内存单元优化其内容。NSArray的实现有一些妥协,以便NSArray能够像数组或者列表一样使用。既然Objective-C的容器只能存放指针,单元维护就会比较有效率了。
NSHashTable等价于NSSet,但它使用的是弱引用(我们曾在前面的章节中讲到过)。这对于垃圾收集器很有帮助。
纯面向对象的实现让Objective-C比C++更容易实现遍历器。NSEnumerator就是为了这个设计的:
容器的objectEnumerator方法返回一个遍历器。遍历器可以使用nextObject移动自己。这种行为更像Java而不是C++。当遍历器到达容器末尾时,nextObject返回nil。下面是最普通的使用遍历器的语法,使用的C语言风格的简写:
快速枚举
Objective-C2.0提供了一个使用遍历器的新语法,隐式使用NSEnumerator(其实和一般的NSEnumerator没有什么区别)。它的具体形式是:
Objective-C的选择器很强大,因而大大减少了函数对象的使用。事实上,弱类型允许用户无需关心实际类型就可以发送消息。例如,下面的代码同前面使用遍历器的是等价的:
在这段代码中,每个对象不一定非得是NSString类型,并且对象也不需要必须实现了doSomethingWithString:方法(这会引发一个异常:selectornotrecognized)。
IMP缓存
我们在这里不会详细解释这个问题,但是的确可以获得C函数的内存地址。通过仅查找一次函数地址,可以优化同一个选择器的多次调用。这被称为IMP缓存,因为Objective-C用于方法实现的数据类型就是IMP。
调用class_getMethodImplementation()就可以获得这么一个指针。但是请注意,这是指向实现方法的真实的指针,因此不能有虚调用。它的使用一般在需要很好的时间优化的场合,并且必须非常小心。
原则
键值对编码意思是,能够通过数据成员的名字来访问到它的值。这种语法很类似于关联数组(在Cocoa中就是NSDictionary),数据成员的名字就是这里的键。NSObject有一个valueForKey:和setValue:forKey:方法。如果数据成员就是对象自己,寻值过程就会向下深入下去,此时,这个键应该是一个路径,使用点号.分割,对应的方法是valueForKeyPath:和setValue:forKeyPath:。
@interfaceA{
NSString*foo;
}
...//其它代码
@end
@interfaceB{
NSString*bar;
A*myA;
}
...//其它代码
@end
@implementationB
...
//假设A类型的对象a,B类型的对象b
A*a=...;
B*b=...;
NSString*s1=[avalueForKey:@"foo"];//正确
NSString*s2=[bvalueForKey:@"bar"];//正确
NSString*s3=[bvalueForKey:@"myA"];//正确
NSString*s4=[bvalueForKeyPath:@"myA.foo"];//正确
NSString*s5=[bvalueForKey:@"myA.foo"];//错误
NSString*s6=[bvalueForKeyPath:@"bar"];//正确
...
@end
这种语法能够让我们对不同的类使用相同的代码来处理同名数据。注意,这里的数据成员的名字都是使用的字符串的形式。这种使用方法的最好的用处在于将数据(名字)绑定到一些触发器(尤其是方法调用)上,例如键值对观察(Key-ValueObserving,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:。这里,同样使用@进行区分。
在定义类时有一个属性的概念。我们使用关键字@property来标记一个属性,告诉编译器自动生成访问代码。属性的主要意义在于节省开发代码量。
访问属性的语法比方法调用简单,因此即使我们需要编写代码时,我们也可以使用属性。访问属性同方法调用的性能是一样的,因为属性的使用在编译期实际就是换成了方法调用。大多数时候,属性用于封装成员变量。但是,我们也可以提供一个“假”的属性,看似是访问一个数据成员,但实际不是;换句话说,看起来像是从对象外部调用一个属性,但实际上其实现要比一个值的管理操作要复杂得多。
属性的描述
对属性的描述实际上是要告诉编译器如何生成访问器的代码:
·属性从外界是只读的吗?
·如果数据成员是原生类型,可选余地不大;如果是对象,那么使用copy封装的话,是要用强引用还是弱引用?
·属性是线程安全的吗?
·访问器的名字是什么?
·属性应该关联到哪一个数据成员?
·应该自动生成哪一个访问器,哪一个则留给开发人员?
我们需要两个步骤来回答这些问题:
·在类的@interface块中,属性的声明需要提供附属参数;
·在类的@implementation块中,访问器可以隐式生成,也可以指定一个实现。
属性访问器是有严格规定的:getter要求必须返回所期望的类型(或者是相容类型);setter必须返回void,并且只能有一个期望类型的参数。访问器的名字也是规定好的:对于数据foo,getter的名字是foo,setter的名字是setFoo:。当然,我们也可以指定自定义的名字,但是不同于前面所说的键值对编码,这个名字必须在编译期确定,因为属性的使用被设计成要和方法的直接调用一样的性能。因此,如果类型是不相容的,是不会有装箱机制的。
以下是带有注释的例子,先来有一个大体的了解。
属性的参数
属性的声明使用一下模板:
或者
如果没有给出属性的参数,那么将使用默认值;否则将使用给出的参数值。这些参数值可以是:
·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=[valueretain];//赋值,同时引用计数器加1
·self->foo=[valuecopy];//对象拷贝(必须满足协议NSCopying)
在有垃圾收集器的环境下,retain同assign没有区别,但是可以加上__weak或者__strong。
注意不要忘记setter的冒号:。
上一章中我们提到的代码中有两个关键字@synthesize和@dynamic。@dynamic意思是由开发人员提供相应的代码:对于只读属性需要提供setter,对于读写属性需要提供setter和getter。@synthesize意思是,除非开发人员已经做了,否则由编译器生成相应的代码,以满足属性声明。对于上次的例子,如果开发人员提供了-(NSString*)registration,编译器就会选择这个实现,不会用新的覆盖。因此,我们可以让编译器帮我们生成代码,以简化我们自己的代码输入量。最后,如果编译期没有找到访问器,而且没有使用@synthesize声明,那么它就会在运行时添加进来。这同样可以实现属性的访问,但是即使这样,访问器的名字也需要在编译期决定。如果运行期没有找到访问器,就会触发一个异常,但程序不会停止,正如同方法的缺失。当我们使用@synthesize时,编译器会被要求绑定某一特定的数据成员,并不一定是一样的名字。
@interfaceA:NSObject{
int_foo;
}
@propertyintfoo;
@end
@implementationA
@synthesizefoo=_foo;//绑定"_foo"而不是"foo"
@end
访问属性的语法
为获取或设置属性,我们使用点号:这同简单的C结构是一致的,也是在keypath中使用的语法,其性能与普通方法调用没有区别。
@interfaceA:NSObject{
inti;
}
@propertyinti;
@end
@interfaceB:NSObject{
A*myA;
}
@property(retain)A*a;
@end
...
A*a=...
B*b=...;
a.i=1;//等价于[asetI:1];
b.myA.i=1;//等价于[[bmyA]setI:1];
请注意上面例子中A类的使用。self->i和self.i是有很大区别的:self->i直接访问数据成员,而self.i则是使用属性机制,是一个方法调用。
高级细节
64位编译器上,Objective-C运行时环境与32位有一些不同。关联到@property的实例数据可能被忽略掉,例如被视为隐式的。更多细节请阅读Apple的文档。
class,superclass,isMemberOfClass,isKindOfClass
对象在运行时获取其类型的能力称为内省。内省可以有多种方法实现。
isMemberOfClass:可以用于回答这种问题:“我是给定类(不包括子类)的实例吗?”,而isKindOfClass:则是“我是给定类或其子类的实例吗?”使用这种方法需要一个“假”关键字的class(注意,不是@class,@class是用于前向声明的)。事实上,class是NSObject的一个方法,返回一个Class对象。这个对象是元类的一个实例。请注意,nil值的类是Nil。
注意,你可以使用superclass方法获取其父类。
conformsToProtocol
该方法用于确定一个对象是否和某一协议兼容。我们前面曾经介绍过这个方法。它并不是动态的。编译器仅仅检查每一个显式声明,而不会检查每一个方法。如果一个对象实现了给定协议的所有方法,但并没有显式声明说它实现了该协议,程序运行是正常的,但是conformsToProtocol:会返回NO。
respondsToSelector,instancesRespondToSelector
respondsToSelector:是一个实例方法,继承自NSObject。该方法用于检查一个对象是否实现了给定的方法。这里如要使用@selector。例如:
如果要检查一个对象是否实现了给定的方法,而不检查继承的方法,可以使用类方法instancesRespondToSelector:。例如:
注意,respondsToSelector:不能用于仅仅使用了前向声明的类。
强类型和弱类型id
C++使用的是强类型:对象必须符合其类型,否则不能通过编译。在Objective-C中,这个限制就灵活得多了。如果一个对象与消息的目标对象不相容,编译器仅仅发出一个警告,而程序则继续运行。这个消息会被丢弃(引发一个异常),除非前面已经转发。如果这就是开发人员期望的,这个警告就是冗余的;在这种情形下,使用弱类型的id来替代其真实类型就可以消除警告。事实上,任何对象都是id类型的,并且可以处理任何消息。这种弱类型在使用代理的时候是必要的:代理对象不需要知道自己被使用了。例如:
在Cocoa中,这种代理被大量用于图形用户界面的设计中。它可以很方便地把控制权由用户对象移交给工作对象。
运行时操作Objective-C类
通过添加头文件<objc/objc-runtime.h>,我们可以调用很多工具函数,用于在运行时获取类信息、添加方法或实例变量。Objective-C2.0又引入了一些新函数,比Objective-C1.0更加灵活(例如使用class_addMethod(…)替代class_addMethods(…)),同时废弃了许多1.0的函数。这让我们可以很方便的在运行时修改类。
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.结语
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,你需要阅读更多资料。但是,相信在本系列基础之上,你在阅读其他资料时应该会理解的更加透彻一些。
说明:本系列大致翻译来自《FromC++toObjective-C》,你可以在
下面来简单介绍一下Objective-C。
要说Objective-C,首先要从Smalltalk说起。Smalltalk是第一个真正意义上的面向对象语言。Smalltalk出现之后,很多人都希望能在C语言的基础之上增加面向对象的特性。于是就出现了两种新语言:C++和Objective-C。C++不必多说,很多人都比较熟悉。Objective-C则比较冷门。它完全借鉴了Smalltalk的思想,有着类似的语法和动态机制;相比来说,C++则更加静态一些,目的在于提供能好的性能。Objective-C最新版本是2.0.我们的这个系列就是以Objective-C2.0为基础讲解。
Objective-C是一门语言,而Cocoa是这门语言用于MacOSX开发的一个类库。它们的关系类似于C++和Qt,Java和Spring一样。所以,我们完全可以不使用Cocoa,只去用Objective-C。例如gcc就是一个不使用Cocoa的编译器。不过在MacOSX平台,几乎所有的功能都要依赖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-C2.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,NOC++中使用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++ | |
头文件 | 源文件 |
//InfileFoo.h #ifndef__FOO_H__//compilationguard #define__FOO_H__// classFoo { ... }; #endif | //InfileFoo.cpp #include"Foo.h" ... |
Objective-C | |
头文件 | 源文件 |
//InfileFoo.h //classdeclaration,differentfrom //the"interface"Javakeyword @interfaceFoo:NSObject { ... } @end | //InfileFoo.m #import"Foo.h" @implementationFoo ... @end |
NS前缀
我们前面看到的类NSObject,NSString都有一个前缀NS。这是Cocoa框架的前缀(Cocoa开发公司是NeXTStep)。函数和方法的区别
Objective-C并不是“使用方括号表示函数调用”的语言。一开始很容易把[objectdoSomething];
理解成
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被称为实例数据instancedata,成员函数memberfunctions被称为方法methods。如果没有特殊说明,在后续文章中,这两组术语都会被混用,大家见谅。
C++ | Objective-C |
classFoo { doublex; public: intf(intx); floatg(intx,inty); }; intFoo::f(intx){...} floatFoo::g(intx,inty){...} | @interfaceFoo:NSObject { doublex; } -(int)f:(int)x; -(float)g:(int)x:(int)y; @end @implementationFoo -(int)f:(int)x{...} -(float)g:(int)x:(int)y{...} @end |
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++ | |
//InfileFoo.h #ifndef__FOO_H__ #define__FOO_H__ classBar;//forwarddeclaration classFoo { Bar*bar; public: voiduseBar(void); }; #endif | //InfileFoo.cpp #include"Foo.h" #include"Bar.h" voidFoo::useBar(void) { ... } |
Objective-C | |
//InfileFoo.h @classBar;//forwarddeclaration @interfaceFoo:NSObject { Bar*bar; } -(void)useBar; @end | //InfileFoo.m #import"Foo.h" #import"Bar.h" @implementationFoo -(void)useBar { ... } @end |
访问可见性是面向对象语言的一个很重要的概念。它规定了在源代码级别哪些是可见的。可见性保证了类的封装性。
C++ | Objective-C |
classFoo { public: intx; intapple(); protected: inty; intpear(); private: intz; intbanana(); }; | @interfaceFoo:NSObject { @public: intx; @protected: inty; @private: intz; } -(int)apple; -(int)pear; -(int)banana; @end |
在Objective-C中,只有成员数据可以是private,protected和public的,默认是protected。方法只能是public的。然而,我们可以在@implementation块中实现一些方法,而不在@interface中声明;或者是使用分类机制(classcategories)。这样做虽然不能阻止方法被调用,但是减少了暴露。不经过声明实现一些方法是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++
//原型
voidArray::insertObject(void*anObject,unsignedintatIndex);
//shelf是Array类的一个实例,book是一个对象
shelf.insertObject(book,2);
Objective-C(不带label,即直接从C++翻译来)
//方法原型
//方法名字是“insertObject::”
//这里的冒号:用来分隔参数,成为方法名的一部分(注意,这不同于C++的域指示符::)
-(void)insertObject:(id)anObject:(unsignedint)index
//shelf是Array类的一个实例,book是一个对象
[shelfinsertObject:book:2];
Objective-C(带有label)
//方法原型。“index”有一个标签“atIndex”
//方法名为“insertObject:atIndex:”
//这样的话,调用语句就很容易阅读了
-(void)insertObject:(id)anObjectatIndex:(unsignedint)index
//shelf是Array类的一个实例,book是一个对象
[shelfinsertObject:book:2];//错误!
[shelfinsertObject:bookatIndex: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 |
classFoo { intx; inty; voidf(void); }; voidFoo::f(void) { x=1; inty;//隐藏this->y y=2;//使用局部变量y this->y=3;//显式使用成员变量 } | @interfaceFoo:NSObject { intx; inty; } -(void)f; @end @implementationFoo -(void)f { x=1; inty;//隐藏super->y y=2;//使用局部变量y self->y=3;//显式使用成员变量 } @end |
函数就是一段能够被引用的代码,例如使用函数指针。一般的,方法名会作为引用方法的唯一id,但是,这就需要小心有重载的情况。C++和Objective-C使用截然不同的两种方式去区分:前者使用参数类型,后者使用参数标签。
在C++中,只要函数具有不同的参数类型,它们就可以具有相同的名字。const也可以作为一种重载依据。
C++
intf(int);
intf(float);//允许,float和int是不同类型
classFoo
{
public:
intg(int);
intg(float);//允许,float和int是不同类型
intg(float)const;//允许,const可以作为重载依据
};
classBar
{
public:
intg(int);//允许,我们使用的是Bar::,而不是Foo::
}
在Objective-C中,所有的函数都是普通的C函数,不能被重载(除非指定使用C99标准)。方法则具有不同的语法,重载的依据是label。
Objective-C
intf(int);
intf(float);//错误!C函数不允许重载
@interfaceFoo: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)xandY:(int)y;//正确:第二个label是“andY”
-(int)g:(int)xandY:(float)y;//错误:同上一个没有区别
-(int)g:(int)xandAlsoY:(int)y;//正确:第二个label是“andAlsoY”
@end
基于label的重载可以很明白地解释方法的名字,例如:
@interfaceFoo:NSObject{}
//方法名是“g”
-(int)g;
//方法名是“g:”
-(int)g:(float)x;
//方法名是“g::”
-(int)g:(float)x:(float)y;
//方法名是“g:andY:”
-(int)g:(float)xandY:(float)y;
//方法名是“g:andZ:”
-(int)g:(float)xandZ:(float)z;
@end
显然,Objective-C的方法使用label区分,而不是类型。利用这种机制,我们就可以使用选择器selector来指定一个方法,而不是“成员函数指针”。
从C++到Objective-C(5):类和对象(续二)
成员函数的指针:选择器在Objective-C中,方法具有包含了括号和标签的特殊语法。普通的函数不能使用这种语法。在Objective-C和C语言中,函数指针具有相同的概念,但是对于成员函数指针则有所不同。
在C++中,尽管语法很怪异,但确实兼容C语言的:成员函数指针也是基于类型的。
C++
classFoo
{
public:
intf(floatx){...}
};
Foobar
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)aSelectorwithObject:(id)anObjectAsParameter;
-(id)performSelector:(SEL)aSelectorwithObject:(id)anObjectAsParameter
withObject:(id)anotherObjectAsParameter;
这些方法的返回值同被调用的函数的返回值是一样的。对于那些参数不是对象的方法,应该使用该类型的包装类,如NSNumber等。NSInvocation也有类似的功能,并且更为强大。
按照前面的说法,我们没有任何办法阻止在一个对象上面调用方法,即便该对象并没有实现这个方法。事实上,当消息被接收到之后,方法会被立即触发。但是,如果对象并不知道这个方法,一个可被捕获的异常将被抛除,应用程序并不会被终止。我们可以使用respondsToSelector:方法来检查对象是否可被触发方法。
最后,@selector的值是在编译器决定的,因此它并不会减慢程序的运行效率。
Objective-C
@interfaceSlave: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()是在编译器计算的
SELmethodSelector=@selector(readDocumentation:);
for(i=0;i<10;++i)
[slaves[i]performSelector:methodSelectorwithObject:document];
//对于一个对象“foo”,它的类型是未知的(id)
//这种测试并不是强制的,但是可以避免没有readDocumentation:方法时出现异常
if([foorespondsToSelector:@selector(readDocumentation:)])
[fooperformSelector:@selector(readDocumentation:)withObject:document];
因此,选择器可被用作函数参数。通用算法,例如排序,就可以使用这种技术实现。
严格说来,选择器并不是一个函数指针。它的底层实现是一个C字符串,在运行时被注册为方法的标识符。当类被加载之后,它的方法会被自动注册到一个表中,所以@selector可以很好的工作。根据这种实现,我们就可以使用==来判断内存地址是否相同,从而得出选择器是否相同,而无需使用字符串函数。
方法的真实地址,也就是看做C字符串的地址,其实可以看作是IMP类型(我们以后会有更详细的说明)。这种类型很少使用,除了在做优化的时候。例如虚调用实际使用选择器处理,而不是IMP。等价于C++函数指针的Objective-C的概念是选择器,也不是IMP。
最后,你应该记得我们曾经说过Objective-C里面的self指针,类似于C++的this指针,是作为每一个方法的隐藏参数传递的。其实这里还有第二个隐藏参数,就是_cmd。_cmd指的是当前方法。
@implementationFoo
-(void)f:(id)parameter//等价于C函数voidf(idself,SEL_cmd,idparameter)
{
idcurrentObject=self;
SELcurrentMethod=_cmd;
[currentObjectperformSelector:currentMethod
withObject:parameter];//递归调用
[selfperformSelector:_cmdwithObject:parameter];//也是递归调用
}
@end
参数的默认值
Objective-C不允许参数带有默认值。所以,如果某些参数是可选的,那么就应当创建多个方法的副本。在构造函数中,这一现象成为指定构造函数(designatedinitializer)。
可变参数
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
{
[assistantautorelease];
assistant=[slaveretain];
}
//方法performHardWork使用代理
-(void)performHardWork:(id)task
{
//assistant在编译期是未知的
//我们首先要检查它是否能够响应消息
if([assistantrespondsToSelector:@selector(performHardWork:)])
[assistantperformHardWork:task];
else
[selffindAnotherAssistant];
}
转发:处理未知消息
在C++中,如果对象函数没有实现,是不能通过编译的。Objective-C则不同,你可以向对象发送任何消息。如果在运行时无法处理,这个消息就被忽略了(同时会抛出一个异常)。除了忽略它,另外的处理办法是将消息转发给另外的对象。
当编译器被告知对象类型时,它可以知道对象可以处理哪些消息,因此就可以知道消息发出后是否会失败,也就可以抛出异常。这也就是为什么消息在运行时被执行,但是编译时就可以发出警告。这并不会引发错误,同时还有另外的选择:调用forwardInvocation:方法。这个方法可以将消息进行转发。这个方法是NSObject的,默认不做任何操作。下面代码就是一种实现:
-(void)forwardInvocation:(NSInvocation*)anInvocation
{
//如果该方法被调用,意味着我们无法处理这个消息
//错误的选择器(也就是调用失败的那个方法名)可以通过
//向anInvocation对象发送“selector”获得
if([anotherObjectrespondsToSelector:[anInvocationselector]])
[anInvocationinvokeWithTarget:anotherObject];
else//不要忘记调用父类的实现
[superforwardInvocation:anInvocation];
}
即是在最后,这个消息在forwardInvocation:中被处理,respondsToSelector:还是会返回NO。事实上,respondsToSelector:并不是用来检查forwardInvocation:是否被调用的。
使用这种转发机制有时候被认为是一种不好的习惯,因为它会隐藏掉本应引发错误的代码。事实上,一些很好的设计同样可以使用这种机制实现,例如Cocoa的NSUndoManager。它允许一种对异常友好的语法:undomanager可以记录方法调用历史,虽然它并不是那些调用的接收者。
向下转型
C++中,父类指针调用子类的函数时,需要有一个向下转型的操作(downcasting),使用dynamic_cast关键字。在Objective-C中,这是不必要的。因为你可以将任何消息发送给任何对象。但是,为了避免编译器的警告,我们也可以使用简单的转型操作。Objective-C中没有类似C++的专门的向下转型的操作符,使用C风格的转型语法就可以了。
//NSMutableString是NSString的子类
//允许字符串修改的操作
//"appendString:"仅在NSMutableString中实现
NSMutableString*mutableString=...初始化可变字符串...
NSString*string=mutableString;//传给NSString指针
//这些调用都是合法的
[stringappendString:@"foo"];//有编译器警告
[(NSMutableString*)stringappendString:@"foo"];//无警告
[(id)stringappendString:@";//无警告
从C++到Objective-C(7):继承
简单继承
Objective-C也有继承的概念,但是不能多重继承。不过,它也有别的途径实现类似多重继承的机制,这个我们后面会讲到。C++ | Objective-C |
classFoo:publicBar, protectedWiz { } | @interfaceFoo:Bar//单继承 //如果要同时“继承”Wiz,需要使用另外的技术 { } @end |
在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:等等。这样的话,接口就变得更简洁,更易于阅读。不好之处就是,你不能知道究竟哪些方法被重定义了。
纯虚方法则是使用正式协议formalprotocols来实现。
虚继承
Objective-C中不允许多重继承,因此也就没有虚继承的问题。
协议
Java和C#使用接口interface的概念来弥补多重继承的不足。Objective-C也使用了类似的机制,成为协议protocol。在C++中,这种概念是使用抽象类。协议并不是真正的类:它只能声明方法,不能添加数据。有两种类型的协议:正式的formal和非正式的informal。正式协议
正式协议的方法,所有实现这个协议的类都必须实现。这就是一种验证,也就是说,只要这个类说实现这个协议,那么它肯定可以处理协议中规定的方法。一个类可以实现任意多个协议。
C++
classMouseListener
{
public:
virtualboolmousePressed(void)=0;//纯虚方法
virtualboolmouseClicked(void)=0;//纯虚方法
};
classKeyboardListener
{
public:
virtualboolkeyPressed(void)=0;//纯虚方法
};
classFoo:publicMouseListener,publicKeyboardListener{...}
//Foo必须实现mousePressed,mouseClicked和keyPressed
//然后Foo就可以作为鼠标和键盘的事件监听器
Objective-C
@protocolMouseListener
-(BOOL)mousePressed;
-(BOOL)mouseClicked;
@end
@protocolKeyboardListener
-(BOOL)keyPressed;
@end
@interfaceFoo: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-C1.0中就有非正式协议了,Objective-C2.0则提出了新的关键字@optional和@required,用以区分可选方法和必须方法。
@protocolSlave
@required//必须部分
-(void)makeCoffee;
-(void)duplicateDocument:(Document*)documentcount:(int)count;
@optional//可选部分
-(void)sweep;
@required//又是一个必须部分
-(void)bringCoffee;
@end
非正式协议
非正式协议并不是真正的协议,它对代码没有约束力。非正式协议允许开发者将一些方法进行归类,从而可以更好的组织代码。所以,非正式协议并不是协议的宽松版本。另外一个相似的概念就是分类。
让我们想象一个文档管理的服务。假设有绿色、蓝色和红色三种文档,一个类只能处理蓝色文档,而Slave类使用三个协议manageBlueDocuments,manageGreenDocuments和manageRedDocuments。Slave可以加入一个分类DocumentsManaging,用来声明它能够完成的任务。分类名在小括号中被指定:
@interfaceSlave(DocumentsManaging)
-(void)manageBlueDocuments:(BlueDocument*)document;
-(void)trashBlueDocuments:(BlueDocument*)document;
@end
任何类都可以加入DocumentsManaging分类,加入相关的处理方法:
@interfacePremiumSlave(DocumentsManaging)
-(void)manageBlueDocuments:(BlueDocument*)document;
-(void)manageRedDocuments:(RedDocument*)document;
@end
另一个开发者就可以浏览源代码,找到了DocumentsManaging分类。如果他觉得这个分类中有些方法可能对自己,就会检查究竟哪些能够使用。即便他不查看源代码,也可以在运行时指定:
if([mySlaverespondsToSelector:@selector(manageBlueDocuments:)])
[mySlavemanageBlueDocuments: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。
例如,下面就是一个返回对象的异步方法:
-(onewayvoid)giveMeAnObjectWhenAvailable:(bycopyoutid*)anObject;
默认情况下,参数都被认为是inout的。如果参数由const修饰,则被当做in参数。为参数选定是in还是out,可以作为一种优化手段。参数默认都是传引用的,方法都是同步的(也就是不加oneway)。对于传值的参数,也就是非指针类型的,out和inout都是没有意义的,只有in是正确的选择。
分类
创建类的分类categories,可以将一个很大的类分割成若干小部分。每个分类都是类的一部分,一个类可以使用任意多个分类,但都不可以添加实例数据。分类的好处是:·对于精益求精的开发者,分类提供了一种划分方法的机制。对于一个很大的类,它可以将其划分成不同的角色;
·分类允许分开编译,也就是说,同一个类也可以进行多人的分工合作;
·如果把分类的声明放在实现文件(.m)中,那么这个分类就只在文件作用域中可见(虽然这并没有调用上的限制,如果你知道方法原型,依然可以调用)。这样的分类可以取一个合适的名字,比如FooPrivateAPI;
·一个类可以在不同程序中有不同的扩展,而不需要丢弃通用代码。所有的类都可以被扩展,甚至是Cocoa中的类。
最后一点尤其重要。很多开发人员都希望标准类能够提供一些对他们而言很有用的方法。这并不是一个很困难的问题,使用继承即可实现。但是,在单继承的环境下,这会造成出现很多的子类。仅仅为了一个方法就去继承显得有些得不偿失。分类就可以很好的解决这个问题:
C++ | Objective-C |
classMyString:publicstring { public: //统计元音的数目 intvowelCount(void); }; intMyString::vowelCount(void) { ... } | @interfaceNSString(VowelsCounting) //注意并没有使用{} -(int)vowelCount;//统计元音的数目 @end @implementationNSString(VowelsCounting) -(int)vowelCount { ... } @end |
在Objective-C中,NSString是Cocoa框架的一个标准类。它是使用分类机制进行的扩展,只能在当前程序中使用。注意此时并没有新增加类。每一个NSString对象都可以从这个扩展获得统计元音数目的能力,甚至常量字符串也可以。同时注意,分类不能增加实例数据,因此没有花括号块。
分类也可以使匿名的,更适合于private的实现:
@interfaceNSString()
//注意并没有使用{}
-(int)myPrivateMethod;
@end
@implementationNSString()
-(int)myPrivateMethod
{
...
}
@end
混合使用协议、分类和子类
混合使用协议、分类和子类的唯一限制在于,你不能同时声明子类和分类。不过,你可以使用两步来绕过这一限制:@interfaceFoo1:SuperClass//ok
@end
@interfaceFoo2(Category)//ok
@end
//下面代码会有编译错误
@interfaceFoo3(Category):SuperClass
@end
//一种解决方案
@interfaceFoo3:SuperClass//第一步
@end
@interfaceFoo3(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的指针。这个指针将在运行时指向对象的实际类型。实例数据根据传入的参数初始化为某一特定的值,这一过程将在一个实例方法instancemethod中完成。这个方法通常命名为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=newFoo;
Objective-C
Foo*foo1=[Fooalloc];
[foo1init];//这是不好的行为:应该使用init的返回值
Foo*foo2=[Fooalloc];
foo2=[foo2init];//正确,不过看上去很啰嗦
Foo*foo3=[[Fooalloc]init];//正确,这才是通常的做法
为检查内存分配是否成功,C++可以判断new返回的指针是否是0(如果使用的是new(nothrow)运算符)。在Objective-C中,检查返回值是否是nil就已经足够了。
初始化方法的正确示例代码
一个正确的初始化方法应该有如下特点:
·名字以init开始;
·返回能够使用的对象;
·调用父类的init方法,直到NSObject的init方法被调用;
·保存[superinit...]的返回值;
·处理构造期间出现的任何错误,无论是自己的还是父类的。
下面是一些代码:
C++
classPoint2D
{
public:
Point2D(intx,inty);
private:
intx;
inty;
};
Point2D::Point2D(intanX,intanY){x=anX;y=anY;}
...
Point2Dp1(3,4);
Point2D*p2=newPoint2D(5,6);
Objective-C
@interfacePoint2D:NSObject
{
intx;
inty;
}
//注意,在Objective-C中,id类似于void*
//(id)就是对象的“一般”类型
-(id)initWithX:(int)anXandY:(int)anY;
@end
@implementationPoint2D
-(id)initWithX:(int)anXandY:(int)anY
{
//调用父类的初始化方法
if(!(self=[superinit]))//如果父类是NSObject,必须进行init操作
returnnil;//如果父类init失败,返回nil
//父类调用成功,进行自己的初始化操作
self->x=anX;
self->y=anY;
returnself;//返回指向自己的指针
}
@end
...
Point2D*p1=[[Point2Dalloc]initWithX:3andY:4];
从C++到Objective-C(10):实例化(续)
self=[superinit...]在上一篇提到的代码中,最不可思议的可能就是这句self=[superinit...]。回想一下,self是每个方法的一个隐藏参数,指向当前对象。因此,这是一个局部变量。那么,为什么我们要改变一个局部变量的值呢?事实上,self必须要改变。我们将在下面解释为什么要这样做。
[superinit]实际上返回不同于当前对象的另外一个对象。单例模式就是这样一种情况。然而,有一个API可以用一个对象替换新分配的对象。CoreData(Apple提供的Cocoa里面的一个API)就是用了这种API,对实例数据做一些特殊的操作,从而让这些数据能够和数据库的字段关联起来。当继承NSManagedObject类的时候,就需要仔细对待这种替换。在这种情形下,self就要指向两个对象:一个是alloc返回的对象,一个是[superinit]返回的对象。修改self的值对代码有一定的影响:每次访问实例数据的时候都是隐式的。正如下面的代码所示:
@interfaceB:A
{
inti;
}
@end
@implementationB
-(id)init
{
//此时,self指向alloc返回的值
//假设A进行了替换操作,返回一个不同的self
idnewSelf=[superinit];
NSLog(@"%d",i);//输出self->i的值
self=newSelf;//有人会认为i没有变化
NSLog(@"%d",i);//事实上,此时的self->i,实际是newSelf->i,
//和之前的值可能不一样了
returnself;
}
@end
...
B*b=[[Balloc]init];
self=[superinit]简洁明了,也不必担心以后会引入bug。然而,我们应该注意旧的self指向的对象的命运:它必须被释放。第一规则很简单:谁替换self指针,谁就要负责处理旧的self指针。在这里,也就是[superinit]负责完成这一操作。例如,如果你创建NSManagedObject子类(这个类会执行替换操作),你就不必担心旧的self指针。事实上,NSManagedObject的开发者必须考虑这种处理。因此,如果你要创建一个执行替换操作的类,你必须知道如何在初始化过程中释放旧有对象。这种操作同错误处理很类似:如果因为非法参数、不可访问的资源造成构造失败,我们要如何处理?
初始化错误
初始化出错可能发生在三个地方:
1.调用[superinit...]之前:如果构造函数参数非法,那么初始化应该立即停止;
2.调用[superinit...]期间:如果父类调用失败,那么当前的初始化操作也应该停止;
3.调用[superinit...]之后:例如资源分配失败等。
在上面每一种情形中,只要失败,就应该返回nil;相应的处理应该由发生错误的对象去完成。这里,我们主要关心的是1,3情况。要释放当前对象,我们调用[selfrelease]即可。
在调用dealloc之后,对象的析构才算完成。因此,dealloc的实现必须同初始化方法兼容。事实上,alloc将所有的实例数据初始化成0是相当有用的。
@interfaceA:NSObject{
unsignedintn;
}
-(id)initWithN:(unsignedint)value;
@end
@implementationA
-(id)initWithN:(unsignedint)value
{
//第一种情况:参数合法吗?
if(value==0)//我们需要一个正值
{
[selfrelease];
returnnil;
}
//第二种情况:父类调用成功吗?
if(!(self=[superinit]))//即是self被替换,它也是父类
returnnil;//错误发生时,谁负责释放self?
//第三种情况:初始化能够完成吗?
n=(int)log(value);
void*p=malloc(n);//尝试分配资源
if(!p)//如果分配失败,我们希望发生错误
{
[selfrelease];
returnnil;
}
}
@end
将构造过程合并为alloc+init
有时候,alloc和init被分割成两个部分显得很罗嗦。幸运的是,我们也可以将其合并在一起。这主要牵扯到Objective-C的内存管理机制。简单来说,作为一个构造函数,它的名字必须以类名开头,其行为类似init,但要自己实现alloc。然而,这个对象需要注册到autorelease池中,除非发送retain消息,否则其生命周期是有限制的。以下即是示例代码:
//啰嗦的写法
NSNumber*tmp1=[[NSNumberalloc]initWithFloat:0.0f];
...
[tmp1release];
//简洁一些
NSNumber*tmp2=[NSNumbernumberWithFloat:0.0f];
...
//无需调用release
从C++到Objective-C(11):实例化(续二)
默认构造函数:指定初始化函数在Objective-C中,默认构造函数没有实在的意义,因为所有对象都是动态分配内存,也就是说,构造函数都是确定的。但是,一个常用的构造函数确实可以精简代码。事实上,一个正确的初始化过程通常类似于:
if(!(self=[superinit]))//"init"或其他父类恰当的函数
returnnil;
//父类初始化成功,继续其他操作……
returnself;
剪贴复制代码是一个不良习惯。好的做法是,将共同代码放到一个独立的函数中,通常称为“指定初始化函数”。通常这种指定初始化函数会包含很多参数,因为Objective-C不允许参数有默认值。
-(id)initWithX:(int)x
{
return[selfinitWithX:xandY:0andZ:0];
}
-(id)initWithX:(int)xandY:(int)y
{
return[selfinitWithX:xandY:yandZ:0];
}
//指定初始化函数
-(id)initWithX:(int)xandY:(int)yandZ:(int)z
{
if(!(self=[superinit]))
returnnil;
self->x=x;
self->y=y;
self->z=z;
returnself;
}
如果指定初始化函数没有最大数量的参数,那基本上就没什么用处:
//以下代码就有很多重复部分
-(id)initWithX:(int)x//指定初始化函数
{
if(!(self=[superinit]))
returnnil;
self->x=x;
returnself;
}
-(id)initWithX:(int)xandY:(int)y
{
if(![selfinitWithX:x])
returnnil;
self->y=y;
returnself;
}
-(id)initWithX:(int)xandY:(int)yandZ:(int)z
{
if(![selfinitWithX:x])
returnnil;
self->y=y;
self->z=z;
returnself;
}
初始化列表和实例数据的默认值
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++
classPoint2D
{
public:
~Point2D();
};
Point2D::~Point2D(){}
Objective-C
@interfacePoint2D:NSObject
-(void)dealloc;//该方法可以被重定义
@end
@implementationPoint2D
//在这个例子中,重定义并不需要
-(void)dealloc
{
[superdealloc];//不要忘记调用父类代码
}
@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=[[FooallocWithZone:zone]init];
//实例数据必须手动复制
clone->integer=self->integer;//"integer"是int类型的
//使用子对象类似的机制复制
clone->objectToClone=[self->objectToClonecopyWithZone:zone];
//有些子对象不能复制,但是可以共享
clone->objectToShare=[self->objectToShareretain];
//如果有设置方法,也可以使用
[clonesetObject:self->object];
returnclone;
}
注意,我们使用的是allocWithZone:而不是alloc。alloc实际上封装了allocWithZone:,它传进的是默认的zone。但是,我们应该注意父类的copyWithZone:的实现。
//父类实现了copyWithZone:,并且没有使用NSCopyObject()
-(id)copyWithZone:(NSZone*)zone
{
Foo*clone=[supercopyWithZone:zone];//创建新的对象
//必须复制当前子类的实例数据
clone->integer=self->integer;//"integer"是int类型的
//使用子对象类似的机制复制
clone->objectToClone=[self->objectToClonecopyWithZone:zone];
//有些子对象不能复制,但是可以共享
clone->objectToShare=[self->objectToShareretain];
//如果有设置方法,也可以使用
[clonesetObject:self->object];
returnclone;
}
NSCopyObject()
NSObject事实上并没有实现NSCopying协议(注意函数的原型不同),因此我们不能简单地使用[supercopy...]这样的调用,而是类似[[...alloc]init]这种标准调用。NSCopyObject()允许更简单的代码,但是需要注意指针变量(包括对象)。这个函数创建一个对象的二进制格式的拷贝,其原型是:
//extraBytes通常是0,可以用于索引实例数据的空间
idNSCopyObject(idanObject,unsignedintextraBytes,NSZone*zone)
二进制复制可以复制非指针对象,但是对于指针对象,需要时刻记住它会创建一个指针所指向的数据的新的引用。通常的做法是在复制完之后重置指针。
//如果父类没有实现copyWithZone:
-(id)copyWithZone:(NSZone*)zone
{
Foo*clone=NSCopyObject(self,0,zone);//以二进制形式复制数据
//clone->integer=self->integer;//不需要,因为二进制复制已经实现了
//需要复制的对象成员必须执行真正的复制
clone->objectToClone=[self->objectToClonecopyWithZone:zone];
//共享子对象必须注册新的引用
[clone->objectToShareretain];
//设置函数看上去应该调用clone->object.但实际上是不正确的,
//因为这是指针值的二进制复制。
//因此在使用mutator前必须重置指针
clone->object=nil;
[clonesetObject:self->object];
returnclone;
}
//如果父类实现了copyWithZone:
-(id)copyWithZone:(NSZone*)zone
{
Foo*clone=[supercopyWithZone:zone];
//父类实现NSCopyObject()了吗?
//这对于知道如何继续下面的代码很重要
clone->integer=self->integer;//仅在NSCopyObject()没有使用时调用
//如果有疑问,一个需要复制的子对象必须真正的复制
clone->objectToClone=[self->objectToClonecopyWithZone:zone];
//不管NSCopyObject()是否实现,新的引用必须添加
clone->objectToShare=[self->objectToShareretain];
clone->object=nil;//如果有疑问,最好重置
[clonesetObject:self->object];
returnclone;
}
Dummy-cloning,mutability,mutableCopyandmutableCopyWithZone:
如果需要复制不可改变对象,一个基本的优化是假装它被复制了,实际上是返回一个原始对象的引用。从这点上可以区分可变对象与不可变对象。
不可变对象的实例数据不能被修改,只有初始化过程能够给一个合法值。在这种情况下,使用“伪克隆”返回一个原始对象的引用就可以了,因为它本身和它的复制品都不能够被修改。此时,copyWithZone:的一个比较好的实现是:
-(id)copyWithZone:(NSZone*)zone
{
//返回自身,增加一个引用
return[selfretain];
}
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=[[FooallocWithZone:zone]init];//或者可用NSCopyObject()
clone->integer=self->integer;
//类似copyWithZone:,有些子对象需要复制,有些需要增加引用
//可变子对象使用mutableCopyWithZone:克隆
//...
returnclone;
}
不要忘记我们可以使用父类的mutableCopyWithZone:
//如果父类实现了mutableCopyWithZone:
-(id)mutableCopyWithZone:(NSZone*)zone
{
Foo*clone=[supermutableCopyWithZone:zone];
//...
returnclone;
}
从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++的二元加运算符(obj3operator+(obj1,obj2))。在C++中,返回值可以在栈上,以便在离开作用域的时候可以自动销毁。但在Objective-C中不存在这种对象。函数使用alloc分配的对象,直到将其返回栈之前不能释放。下面的代码将解释这种情况:
//第一个例子
-(Point2D*)add:(Point2D*)p1and:(Point2D*)p2
{
Point2D*result=[[Point2Dalloc]initWithX:([p1getX]+[p2getX])
andY:([p1getY]+[p2getY])];
returnresult;
}
//错误!这个函数使用了alloc,所以它将对象的引用计数器加1。
//根据前面的说法,它应该被销毁。
//但是这将引起内存泄露:
[calculatoradd:[calculatoradd:p1and:p2]and:p3];
//第一个算式是匿名的,没有办法release。所以引起内存泄露。
//第二个例子
-(Point2D*)add:(Point2D*)p1and:(Point2D*)p2
{
return[[Point2Dalloc]initWithX:([p1getX]+[p2getX])
andY:([p1getY]+[p2getY])];
}
//错误!这段代码实际上和上面的一样,
//不同之处在于仅仅减少了一个中间变量。
//第三个例子
-(Point2D*)add:(Point2D*)p1and:(Point2D*)p2
{
Point2D*result=[[Point2Dalloc]initWithX:([p1getX]+[p2getX])
andY:([p1getY]+[p2getY])];
[resultrelease];
returnresult;
}
//错误!显然,这里仅仅是在对象创建出来之后立即销毁了。
这个问题看起来很棘手。如果没有autorelease的确如此。简单地说,给一个对象发送autorelease消息意味着告诉它,在“一段时间之后”销毁。但是这里的“一段时间之后”并不意味着“任何时间”。我们将在后面的章节中详细讲述这个问题。现在,我们有了上面这个问题的一种解决方案:
-(Point2D*)add:(Point2D*)p1and:(Point2D*)p2
{
Point2D*result=[[Point2Dalloc]initWithX:([p1getX]+[p2getX])
andY:([p1getY]+[p2getY])];
[resultautorelease];
returnresult;//更简短的代码是:return[resultautorelease];
}
//正确!result将在以后自动释放
从C++到Objective-C(14):内存管理(续)
autorelease池上一节中我们了解到autorelease的种种神奇之处:它能够在合适的时候自动释放分配的内存。但是如何才能让便以其之道什么时候合适呢?这种情况下,垃圾收集器是最好的选择。下面我们将着重讲解垃圾收集器的工作原理。不过,为了了解垃圾收集器,就不得不深入了解autorelease的机制。所以我们要从这里开始。当对象收到autorelease消息的时候,它会被注册到一个“autorelease池”。当这个池被销毁时,其中的对象也就被实际的销毁。所以,现在的问题是,这个池如何管理?
答案是丰富多彩的:如果你使用Cocoa开发GUI界面,基本不需要做什么事情;否则的话,你应该自己创建和销毁这个池。
拥有图形界面的应用程序都有一个事件循环。这个循环将等待用户动作,使应用程序响应动作,然后继续等待下一个动作。当你使用Cocoa创建GUI程序时,这个autorelease池在事件循环的一次循环开始时被自动创建,然后在循环结束时自动销毁。这是合乎逻辑的:一般的,一个用户动作都会触发一系列任务,临时变量的创建和销毁一般不会影响到下一个事件。如果必须要有可持久化的数据,那么你就要手动地使用retain消息。
另一方面,如果没有GUI,你必须自己建立autorelease池。当对象收到autorelease消息时,它能够找到最近的autorelease池。当池可以被清空时,你可以对这个池使用release消息。一般的,命令行界面的Cocoa程序都会有如下的代码:
intmain(intargc,char*argv[])
{
NSAutoreleasePool*pool=[[NSAutoreleasePoolalloc]init];
//...
[poolrelease];
return0;
}
注意在MacOSX10.5的NSAutoreleasePool类新增加了一个drain方法。这个方法等价于:当垃圾收集器可用时做release操作;否则则触发运行垃圾收集。这对编写在两种情况下都适用的代码时是很有用的。注意,这里实际上是说,现在有两种环境:引用计数和垃圾回收。MacOS的新版本都会支持垃圾收集器,但是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。
Convenienceconstructor,virtualconstructor
将构造对象的过程分成alloc和init两个阶段,有时候显得很罗嗦。好在我们有一个convenienceconstructor的概念。这种构造函数应该使用类名做前缀,其行为类似init,同时要实现alloc。但是,它的返回对象需要注册到一个内部的autorelease池,如果没有给它发送retain消息时,这个对象始终是一个临时对象。例如:
//啰嗦的写法
NSNumber*zero_a=[[NSNumberalloc]initWithFloat:0.0f];
...
[zero_arelease];
...
//简洁一些的
NSNumber*zero_b=[NSNumbernumberWithFloat:0.0f];
...
//不需要release
根据我们前面对内存管理的介绍,这种构造函数的实现是基于autorelease的。但是其底层代码并不那么简单,因为这涉及到对self的正确使用。事实上,这种构造函数都是类方法,所以self指向的是Class类型的对象,就是元类类型的。在初始化方法,也就是一个实例方法中,self指向的是这个类的对象的实例,也就是一个“普通的”对象。
编写错误的这种构造函数是很容易的。例如,我们要创建一个Vehicle类,包含一个color数据,编写如下的代码:
//TheVehicleclass
@interfaceVehicle:NSObject
{
NSColor*color;
}
-(void)setColor:(NSColor*)color;
//简洁构造函数
+(id)vehicleWithColor:(NSColor*)color;
@end
其对应的实现是:
//错误的实现
+(Vehicle*)vehicleWithColor:(NSColor*)color
{
//self不能改变
self=[[selfalloc]init];//错误!
[selfsetColor:color];
return[selfautorelease];
}
记住我们前面所说的,这里的self指向的是Class类型的对象。
//比较正确的实现
+(id)vehicleWithColor:(NSColor*)color
{
idnewInstance=[[Vehiclealloc]init];//正确,但是忽略了有子类的情况
[newInstancesetColor:color];
return[newInstanceautorelease];
}
我们来改进一下。Objective-C中,我们可以实现virtualconstructor。这种构造函数通过内省的机制来了解到自己究竟应该创建哪种类的对象,是这个类本身的还是其子类的。然后它直接创建正确的类的实例。我们可以使用一个class方法(注意,class在Objective-C中不是关键字);这是NSObject的一个方法,返回当前对象的类对象(也就是meta-class对象)。
@implementationVehicle
+(id)vehicleWithColor:(NSColor*)color
{
idnewInstance=[[[selfclass]alloc]init];//完美!我们可以在运行时识别出类
[newInstancesetColor:color];
return[newInstanceautorelease];
}
@end
@interfaceCar:Vehicle{...}
@end
...
//创建一个redCar
idcar=[CarvehicleWithColor:[NSColorredColor]];
类似于初始化函数的init前缀,这种简洁构造函数最好使用类名作前缀。不过也有些例外,例如[NSColorredColor]返回一个预定义的颜色,按照我们的约定,使用[NSColorcolorRed]更合适一些。
最后,我们要重复一下,所有使用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=[newStringretain];//使用retain指定
}
复制(不完整的代码)
外部对象实际没有被引用,使用的是其克隆。此时,外部对象的改变对于当前类是不可变的。也就是说,当前类持有的是这个对象的克隆,这个对象的生命周期不会比持有者更长。
-(void)setString:(NSString*)newString
{
...稍后解释内存方面的细节
self->string=[newStringcopy];//克隆
//使用NSCopying协议
}
为了补充完整这些代码,我们需要考虑这个对象在前一时刻的状态:每一种情形下,setter都需要释放掉旧的资源,然后建立新的。这些代码看起来比较麻烦。
直接指定(完整代码)
这是最简单的情况。旧的引用实际上被替换成了新的。
-(void)setString:(NSString*)newString
{
//没有强链接,旧值被改变了
self->string=newString;//直接指定
}
使用retain指定(完整代码)
在这种情况下,旧值需要被释放,除非旧值和新值是一样的。
//------不正确的实现------
-(void)setString:(NSString*)newString
{
self->string=[newStringretain];
//错误!内存泄露,没有引用指向旧的“string”,因此再也无法释放
}
-(void)setString:(NSString*)newString
{
[self->stringrelease];
self->string=[newStringretain];
//错误!如果newString==string(这是可能的),
//newString引用是1,那么在[self->stringrelease]之后
//使用newString就是非法的,因为此时对象已经被释放
}
-(void)setString:(NSString*)newString
{
if(self->string!=newString)
[self->stringrelease];//正确:给nil发送release是安全的
self->string=[newStringretain];//错误!应该在if里面
//因为如果string==newString,
//计数器不会被增加
}
//------正确的实现------
//最佳实践:C++程序员一般都会“改变前检查”
-(void)setString:(NSString*)newString
{
//仅在必要时修改
if(self->string!=newString){
[self->stringrelease];//释放旧的
self->string=[newStringretain];//retain新的
}
}
//最佳实践:自动释放旧值
-(void)setString:(NSString*)newString
{
[self->stringautorelease];//即使string==newString也没有关系,
//因为release是被推迟的
self->string=[newStringretain];
//...因此这个retain要在release之前发生
}
//最佳实践:先retain在release
-(void)setString:(NSString*)newString
{
[self->newStringretain];//引用计数器加1(除了nil)
[self->stringrelease];//release时不会是0
self->string=newString;//这里就不应该再加retain了
}
复制(完整代码)
无论是典型的误用还是正确的解决方案,都和前面使用retain指定一样,只不过把retain换成copy。
伪克隆
有些克隆是伪克隆,不过对结果没有影响。
从C++到Objective-C(16):内存管理(续三)
GettersObjective-C中,所有对象都是动态分配的,使用指针引用。一般的,getter仅仅返回指针的值,而不应该复制对象。getter的名字一般和数据成员的名字相同(这一点不同于Java,JavaBean规范要求以get开头),这并不会引起任何问题。如果是布尔变量,则使用is开头(类似JavaBean规范),这样可以让程序更具可读性。
@interfaceButton
{
NSString*label;
BOOLpressed;
}
-(NSString*)label;
-(void)setLabel:(NSString*)newLabel;
-(BOOL)isPressed;
@end
@implementationButton
-(NSString*)label
{
returnlabel;
}
-(BOOL)isPressed
{
returnpressed;
}
-(void)setLabel:(NSString*)newLabel{...}
@end
当返回实例数据指针时,外界就可以很轻松地修改其值。这可能是很多getter不希望的结果,因为这样一来就破坏了封装性。
@interfaceButton
{
NSMutableString*label;
}
-(NSString*)label;
@end
@implementationButton
-(NSString*)label
{
returnlabel;//正确,但知道内情的用户可以将其强制转换成NSMutableString,
//从而改变字符串的值
}
-(NSString*)label
{
//解决方案1:
return[NSStringstringWithString:label];
//正确:实际返回一个新的不可变字符串
//解决方案2:
return[[labelcopy]autorelease];
//正确:返回一个不可变克隆,其值是一个NSString(注意不是mutableCopy)
}
@end
循环retain
必须紧身避免出现循环retain。如果对象Aretain对象B,B和C相互retain,那么B和C就陷入了循环retain:A→B↔C
如果AreleaseB,B不会真正释放,因为C依然持有B。C也不能被释放,因为B持有C。因为只有A能够引用到B,所以一旦AreleaseB,就再也没有对象能够引用这个循环,这样就不可避免的造成内存泄露。这就是为什么在一个树结构中,一般是父节点retain子节点,而子节点不retain父节点。
垃圾收集器
Objective-C2.0实现了一个垃圾收集器。换句话说,你可以将所有内存管理交给垃圾收集器,再也不用关心什么retain、release之类。但是,不同于Java,Objective-C的垃圾收集器是可选的:你可以选择关闭它,从而自己管理对象的生命周期;或者你选择打开,从而减少很多可能有bug的代码。垃圾收集器是以一个程序为单位的,因此,打开或者关闭都会影响到整个应用程序。如果开启垃圾收集器,retain、release和autorelease都被重定义成什么都不做。因此,在没有垃圾收集器情况下编写的代码可以不做任何改变地移植到有垃圾收集器的环境下,理论上只要重新编译一遍就可以了。“理论上”意思是,很多情况下涉及到资源释放处理的时候还是需要特别谨慎地对待。因此,编写同时满足两种情况的代码是不大容易的,一般开发者都会选择重新编写。下面,我们将逐一解释这两者之间的区别,这些都是需要特别注意的地方。
finalize
在有垃圾收集器的环境下,对象的析构顺序是未定义的,因此使用dealloc就不大适合了。NSObject增加了一个finalize方法,将析构过程分解为两步:资源释放和有效回收。一个好的finalize方法是相当精妙的,需要很好的设计。
weak,strong
很少会见到__weak和__strong出现在声明中,但我们需要对它们有一定的了解。
默认情况下,一个指针都会使用__strong属性,表明这是一个强引用。这意味着,只要引用存在,对象就不能被销毁。这是一种所期望的行为:当所有(强)引用都去除时,对象才能被收集和释放。不过,有时我们却希望禁用这种行为:一些集合类不应该增加其元素的引用,因为这会引起对象无法释放。在这种情况下,我们需要使用弱引用(不用担心,内置的集合类就是这么干的),使用__weak关键字。NSHashTable就是一个例子。当被引用的对象消失时,弱引用会自动设置为nil。Cocoa的NotificationCenter就是这么一个例子,虽然这已经超出纯Objective-C的语言范畴。
NSMakeCollectable()
Cocoa并不是MacOSX唯一的API。CoreFoundation就是另外一个。它们是兼容的,可以共享数据和对象。但是CoreFoudation是由纯C编写的。或许你会认为,Objective-C的垃圾收集器不能处理CoreFoundation的指针。但实际上是可以的。感兴趣的话可以关注一下NSMakeCollectable的文档。
AutoZone
由Apple开发的Objective-C垃圾收集器叫做AutoZone。这是一个公开的开源库,我们可以看到起源代码。不过在MacOSX10.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 |
BOOLproblem=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(); } |
intf(void)
{
printf("f:1-youseeme\n");
//注意看输出的字符串,体会异常处理流程
@throw[NSExceptionexceptionWithName:@"panic"
reason:@"youdon’treallywanttoknown"
userInfo:nil];
printf("f:2-youneverseeme\n");
}
intg(void)
{
printf("g:1-youseeme\n");
@try{
f();
printf("g:2-youdonotseeme(inthisexample)\n");
}@catch(NSException*e){
printf("g:3-youseeme\n");
@throw;
printf("g:4-youneverseeme\n");
}@finally{
printf("g:5-youseeme\n");
}
printf("g:6-youdonotseeme(inthisexample)\n");
}
最后一点,C++的catch(…)可以捕获任意值,但是Objective-C中是不可以的。事实上,只有对象可以被抛出,也就是说,我们可以始终使用id捕获异常。
另外注意,Cocoa中有一个NSException类,推荐使用此类作为一切异常类的父类。因此,catch(NSException*e)相当于C++的catch(…)。
多线程
线程安全在Objective-C中可以很清晰地使用POSIXAPIs2实现多线程。Cocoa提供了自己的类管理多线程。有一点是需要注意的:多个线程同时访问同一个内存区域时,可能会导致不可预料的结果。POSIXAPIs和Cocoa都提供了锁和互斥对象。Objective-C提供了一个关键字@synchronized,与Java的同名关键字是一样的。
@synchronized
由@synchronized(…)包围的块会自动加锁,保证一次只有一个线程使用。在处理并发时,这并不是最好的解决方案,但却是对大多数关键块的最简单、最轻量、最方便的解决方案。@synchonized要求使用一个对象作为参数(可以是任何对象,比如self),将这个对象作为锁使用。
@implementationMyClass
-(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=[[NSStringalloc]initWithUTF8String:"helloWorld"];
NSString*stillNotHandy=//initWithFormat类似sprintf()
[[NSStringalloc]initWithFormat:@"%s","helloWorld"];
NSString*handy=@"helloworld";
另外,static字符串可以同普通对象一样作为参数使用。
intsize=[@"hello"length];
NSString*uppercaseHello=[@"hello"uppercaseString];
NSString和编码
NSString对象非常有用,因为它增加了很多好用的方法,并且支持不同的编码,如ASCII、UNICODE、ISOLatin1等。因此,翻译和本地化应用程序也变得很简单。
对象描述,%@扩展,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("Mynameis%s,Ilike%s,and%s...\n",
name,[action1UTF8String],[@"runningagain"UTF8String]);
NSLog(@"Mynameis%s,Ilike%@and%@\n",
name,action1,@"runningagain");
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-C2.0新增)。
你可能会发现这其中并没有NSList或者NSQueue。事实上,这些容器都可以由NSArray实现。
不同于C++的vector<T>,Objective-C的NSArray真正隐藏了它的内部实现,仅能够使用访问器获取其内容。因此,NSArray没有义务为内存单元优化其内容。NSArray的实现有一些妥协,以便NSArray能够像数组或者列表一样使用。既然Objective-C的容器只能存放指针,单元维护就会比较有效率了。
NSHashTable等价于NSSet,但它使用的是弱引用(我们曾在前面的章节中讲到过)。这对于垃圾收集器很有帮助。
遍历器
经典的枚举纯面向对象的实现让Objective-C比C++更容易实现遍历器。NSEnumerator就是为了这个设计的:
NSArray*array=[NSArrayarrayWithObjects:object1,object2,object3,nil];
NSEnumerator*enumerator=[arrayobjectEnumerator];
NSString*aString=@"foo";
idanObject=[enumeratornextObject];
while(anObject!=nil)
{
[anObjectdoSomethingWithString:aString];
anObject=[enumeratornextObject];
}
容器的objectEnumerator方法返回一个遍历器。遍历器可以使用nextObject移动自己。这种行为更像Java而不是C++。当遍历器到达容器末尾时,nextObject返回nil。下面是最普通的使用遍历器的语法,使用的C语言风格的简写:
NSArray*array=[NSArrayarrayWithObjects:object1,object2,object3,nil];
NSEnumerator*enumerator=[arrayobjectEnumerator];
NSString*aString=@"foo";
idanObject=nil;
while((anObject=[enumeratornextObject])){
[anObjectdoSomethingWithString:aString];
}
//双括号能够防止gcc发出警告
快速枚举
Objective-C2.0提供了一个使用遍历器的新语法,隐式使用NSEnumerator(其实和一般的NSEnumerator没有什么区别)。它的具体形式是:
NSArray*someContainer=...;
for(idobjectinsomeContainer){//每一个对象都是用id类型
...
}
for(NSString*objectinsomeContainer){//每一个对象都是NSString
...//开发人员需要处理不是NSString*的情况
}
函数对象
使用选择器Objective-C的选择器很强大,因而大大减少了函数对象的使用。事实上,弱类型允许用户无需关心实际类型就可以发送消息。例如,下面的代码同前面使用遍历器的是等价的:
NSArray*array=[NSArrayarrayWithObjects:object1,object2,object3,nil];
NSString*aString=@"foo";
[arraymakeObjectsPerformSelector:@selector(doSomethingWithString:)
withObject:aString];
在这段代码中,每个对象不一定非得是NSString类型,并且对象也不需要必须实现了doSomethingWithString:方法(这会引发一个异常:selectornotrecognized)。
IMP缓存
我们在这里不会详细解释这个问题,但是的确可以获得C函数的内存地址。通过仅查找一次函数地址,可以优化同一个选择器的多次调用。这被称为IMP缓存,因为Objective-C用于方法实现的数据类型就是IMP。
调用class_getMethodImplementation()就可以获得这么一个指针。但是请注意,这是指向实现方法的真实的指针,因此不能有虚调用。它的使用一般在需要很好的时间优化的场合,并且必须非常小心。
算法
STL中那一大堆通用算法在Objective-C中都没有对等的实现。相反,你应该仔细查找下各个容器中有没有你需要的算法。从C++到Objective-C(20):隐式代码
本章中心是两个能够让代码更简洁的特性。它们的目的截然不同:键值对编码可以通过选择第一个符合条件的实现而解决间接方法调用;属性则可以让编译器帮我们生成部分代码。键值对编码实际上是Cocoa引入的,而属性则是Objective-C2.0语言新增加的。键值对编码(KVC)原则
键值对编码意思是,能够通过数据成员的名字来访问到它的值。这种语法很类似于关联数组(在Cocoa中就是NSDictionary),数据成员的名字就是这里的键。NSObject有一个valueForKey:和setValue:forKey:方法。如果数据成员就是对象自己,寻值过程就会向下深入下去,此时,这个键应该是一个路径,使用点号.分割,对应的方法是valueForKeyPath:和setValue:forKeyPath:。
@interfaceA{
NSString*foo;
}
...//其它代码
@end
@interfaceB{
NSString*bar;
A*myA;
}
...//其它代码
@end
@implementationB
...
//假设A类型的对象a,B类型的对象b
A*a=...;
B*b=...;
NSString*s1=[avalueForKey:@"foo"];//正确
NSString*s2=[bvalueForKey:@"bar"];//正确
NSString*s3=[bvalueForKey:@"myA"];//正确
NSString*s4=[bvalueForKeyPath:@"myA.foo"];//正确
NSString*s5=[bvalueForKey:@"myA.foo"];//错误
NSString*s6=[bvalueForKeyPath:@"bar"];//正确
...
@end
这种语法能够让我们对不同的类使用相同的代码来处理同名数据。注意,这里的数据成员的名字都是使用的字符串的形式。这种使用方法的最好的用处在于将数据(名字)绑定到一些触发器(尤其是方法调用)上,例如键值对观察(Key-ValueObserving,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:。当然,我们也可以指定自定义的名字,但是不同于前面所说的键值对编码,这个名字必须在编译期确定,因为属性的使用被设计成要和方法的直接调用一样的性能。因此,如果类型是不相容的,是不会有装箱机制的。
以下是带有注释的例子,先来有一个大体的了解。
@interfaceclassCar:NSObject
{
NSString*registration;
Person*driver;
}
//registration是只读的,使用copy设置
@propertyNSString*(readonly,copy)registration;
//driver使用弱引用(没有retain),可以被修改
@propertyPerson*(assign)driver;
@end
...
@implementation
//开发者没有提供,由编译期生成registration的代码
@synthesizeregistration;
//开发者提供了driver的getter/setter实现
@dynamicdriver;
//该方法将作为@dynamicdriver的getter
-(Person*)driver{
...
}
//该方法将作为@dynamicdriver的setter
-(void)setDriver:(Person*)value{
...
}
@end
属性的参数
属性的声明使用一下模板:
@propertytypename;
或者
@property(attributes)typename;
如果没有给出属性的参数,那么将使用默认值;否则将使用给出的参数值。这些参数值可以是:
·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=[valueretain];//赋值,同时引用计数器加1
·self->foo=[valuecopy];//对象拷贝(必须满足协议NSCopying)
在有垃圾收集器的环境下,retain同assign没有区别,但是可以加上__weak或者__strong。
@property(copy,getter=getS,setter=setF:)__weakNSString*s;//复杂声明
注意不要忘记setter的冒号:。
从C++到Objective-C(22):隐式代码(续二)
属性的自定义实现上一章中我们提到的代码中有两个关键字@synthesize和@dynamic。@dynamic意思是由开发人员提供相应的代码:对于只读属性需要提供setter,对于读写属性需要提供setter和getter。@synthesize意思是,除非开发人员已经做了,否则由编译器生成相应的代码,以满足属性声明。对于上次的例子,如果开发人员提供了-(NSString*)registration,编译器就会选择这个实现,不会用新的覆盖。因此,我们可以让编译器帮我们生成代码,以简化我们自己的代码输入量。最后,如果编译期没有找到访问器,而且没有使用@synthesize声明,那么它就会在运行时添加进来。这同样可以实现属性的访问,但是即使这样,访问器的名字也需要在编译期决定。如果运行期没有找到访问器,就会触发一个异常,但程序不会停止,正如同方法的缺失。当我们使用@synthesize时,编译器会被要求绑定某一特定的数据成员,并不一定是一样的名字。
@interfaceA:NSObject{
int_foo;
}
@propertyintfoo;
@end
@implementationA
@synthesizefoo=_foo;//绑定"_foo"而不是"foo"
@end
访问属性的语法
为获取或设置属性,我们使用点号:这同简单的C结构是一致的,也是在keypath中使用的语法,其性能与普通方法调用没有区别。
@interfaceA:NSObject{
inti;
}
@propertyinti;
@end
@interfaceB:NSObject{
A*myA;
}
@property(retain)A*a;
@end
...
A*a=...
B*b=...;
a.i=1;//等价于[asetI:1];
b.myA.i=1;//等价于[[bmyA]setI:1];
请注意上面例子中A类的使用。self->i和self.i是有很大区别的:self->i直接访问数据成员,而self.i则是使用属性机制,是一个方法调用。
高级细节
64位编译器上,Objective-C运行时环境与32位有一些不同。关联到@property的实例数据可能被忽略掉,例如被视为隐式的。更多细节请阅读Apple的文档。
从C++到Objective-C(23):动态
RTTI(Run-TimeTypeInformation)
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。
BOOLtest=[selfisKindOfClass:[Fooclass]];
if(test)
printf("IamaninstanceoftheFooclass\n");
注意,你可以使用superclass方法获取其父类。
conformsToProtocol
该方法用于确定一个对象是否和某一协议兼容。我们前面曾经介绍过这个方法。它并不是动态的。编译器仅仅检查每一个显式声明,而不会检查每一个方法。如果一个对象实现了给定协议的所有方法,但并没有显式声明说它实现了该协议,程序运行是正常的,但是conformsToProtocol:会返回NO。
respondsToSelector,instancesRespondToSelector
respondsToSelector:是一个实例方法,继承自NSObject。该方法用于检查一个对象是否实现了给定的方法。这里如要使用@selector。例如:
if([selfrespondsToSelector:@selector(work)])
{
printf("Iamnotlazy.\n");
[selfwork];
}
如果要检查一个对象是否实现了给定的方法,而不检查继承的方法,可以使用类方法instancesRespondToSelector:。例如:
if([[selfclass]instancesRespondToSelector:@selector(findWork)])
{
printf("Icanfindajobwithoutthehelpofmymother\n");
}
注意,respondsToSelector:不能用于仅仅使用了前向声明的类。
强类型和弱类型id
C++使用的是强类型:对象必须符合其类型,否则不能通过编译。在Objective-C中,这个限制就灵活得多了。如果一个对象与消息的目标对象不相容,编译器仅仅发出一个警告,而程序则继续运行。这个消息会被丢弃(引发一个异常),除非前面已经转发。如果这就是开发人员期望的,这个警告就是冗余的;在这种情形下,使用弱类型的id来替代其真实类型就可以消除警告。事实上,任何对象都是id类型的,并且可以处理任何消息。这种弱类型在使用代理的时候是必要的:代理对象不需要知道自己被使用了。例如:
-(void)setAssistant:(id)anObject
{
[assistantautorelease];
assistant=[anObjectretain];
}
-(void)manageDocument:(Document*)document
{
if([assistantrespondToSelector:@(manageDocument:)])
[assistantmanageDocument:document];
else
printf("Didyoufilltheblueform?\n");
}
在Cocoa中,这种代理被大量用于图形用户界面的设计中。它可以很方便地把控制权由用户对象移交给工作对象。
运行时操作Objective-C类
通过添加头文件<objc/objc-runtime.h>,我们可以调用很多工具函数,用于在运行时获取类信息、添加方法或实例变量。Objective-C2.0又引入了一些新函数,比Objective-C1.0更加灵活(例如使用class_addMethod(…)替代class_addMethods(…)),同时废弃了许多1.0的函数。这让我们可以很方便的在运行时修改类。
从C++到Objective-C(24):结语
《从C++到Objective-C》系列已经结束。再次重申一下,本系列不是一个完整的Objective-C的教学文档,只是方便熟悉C++或者类C++的开发人员(例如广大的Java程序员)能够很快的使用Objective-C进行简单的开发。当然,目前Objective-C的最广泛应用在于Apple系列的开发,MacOSX、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.
19.
20.
21.
22.
23.
24.
相关文章推荐
- Objective-C/C++混编编译器设置
- Objective-C与Objective-C++的混用代码示例
- 《从C++到Objective-C》看Objective-C
- 从C/C++到Objective-C(二)--- 面向对象
- 不要把objective-c当做c/c++的超集
- 关于在xcode里面c++代码与objective-c代码混编的问题
- Objective-C 与 C++ 的区别
- C++、Objective-C 混合编程
- 从 C++ 到 Objective-C(6):类和对象(续三)
- Objective-C 与 C++ 的异同
- 从C++到objective-c
- Objective-C和C++的区别
- iPhone开发入门(7)--- 从C/C++语言到Objective-C语言
- iphone开发之C++和Objective-C混编 如何在xcode中用C++的STL
- 小结 C++与Objective-c的编写代码的一些区别
- IOS-Swift、Objective-C、C++混合编程
- objective-c 和c++ 混合编程
- 混合使用Objective-C,C++和Objective-C++
- 从 C++ 到Objective-C
- C++和Objective-C混编(官方文档翻译)