对象分配空间与初始化
使用Objective-C语言创建一个对象有两个步骤,你必须:
-
为新对象动态分配内存空间
-
初始化新分配的内存,并赋初值
不经过如上两步,一个对象就没有完全功能化。每个步骤都可以分步完成,不过一般的都是在用写在同一行的代码实现:
- id anObject = [[Rectangle alloc] init];
把分配空间和初始化分离,你就可以分开的操作这两步,那么对其的修改也是隔离的。下文将首先关注分配内存空间,而后是初始化,接着讨论它们是如何控制和修改的。
在Objective-C中, 新对象的内存申请的类方法是定义在NSObject 类中的,NSObject 定义了两个主要方法:alloc和allocWithZone .
这两个方法会分配足够的内存以容纳全部的实体变量,不需要在子类中重写.
alloc
和allocWithZone:
方法初始化新分配的对象的isa
实体变量,让它可以指向对象的类(类对象).其他的实体变量都会被设置为0.通常,一个对象需要在使用前做更针对的初始化.
初始化是不同的类的实体方法的责任, 为了方便,一般都缩写为"init".如果方法不需要参数,那么初始化方法名就用这四个字母足矣,如果需要参数,就写成以"init"为前缀的参数标签。比如,NSView
对象可以用initWithFrame:
方法初始化.
每个声明了实体变量的类必须提供init...的方法初始化这些实体变量.NSObject类声明了
isa变量,并定义了
init方法.然而,因为
isa是当对象的内存分配后就已经初始化完成的
,所有的NSObject
的init
方法仅仅是返回self
.NSObject声明这个方法主要是为了建立之前所描述的命名习惯.
返回的对象
init...方法通常用于init方法的承接着初始化实体变量,并返回该承接者。返回对象供无错的使用正是其责任。
不过,在某些情况,这个责任可能意味着返回和承接者不同的对象。比如一个类保持了一些有名字的对象,它就可能提供一个叫做initWithName:
的方法去初始化新对象.如果不是每个对象都有各自的名字的话,那么initWithName:
可能会拒绝将同一个名字付给两个对象。当我们想要对一个新对象赋名字时,它发现这个对象的名字已经有对象使用过了,那么它可能会将这个新对象释放,并返回已经使用这个名字的老对象,这样可以确保我们想要构建的对象在同一个名字的前提下将是同一个对象。
在另一些的情况,可能无法让init...
方法做到它本来应该完成的任务。比如,一个叫initFromFile:
的方法设计上是想让其获得参数的文件的数据,但如果参数里的文件并不实际存在,这必然无法做到初始化。这种情况下,init...
方法将会 释放承接者并返回nil
, 表明被请求的对象无法被创建。
综上 init...
方法并不一定返回承接者即刚刚分配空间的对象甚至可能返回nil
, 所以初始化方法的返回值是相当重要的,它未必返回的就是alloc
或 allocWithZone:
创造的对象.下面的实例代码是非常危险的,因为忽略了init
的返回值。
- id anObject = [SomeClass alloc];
- [anObject init];
- [anObject someOtherMessage];
取而代之,为了安全的初始化对象,你应该始终将发送分配空间和初始化消息写在一行代码中
- id anObject = [[SomeClass alloc] init];
- [anObject someOtherMessage];
如果init...
方法有返回nil的可能
(见 “Handling Initialization Failure” ),你应该在继续处理之前校验返回值:
- id anObject = [[SomeClass alloc] init];
- if ( anObject )
- [anObject someOtherMessage];
- else
- ...
实现一个初始化方法
当一个新对象被创建后,它所占用的内存的每一bit(除了isa
外)都被置为0,因此所有实体变量的初值也是0. 在某些情况,这样就可以满足你对该对象的初始化的要求,但别的情况中,你要为实体变量提供别的默认初值,或者你可以给初始化方法传参并利用参数初始化,那么你就需要写一个自定义的初始化方法。在Objective-C中,自定义初始化方法要遵守比其他方法更多的限制与惯例。
限制与惯例
这里时一些仅适用于初始化方法的限制与惯例:
-
惯例上,初始化方法的名字由
init
开始。比如Foundation framework里就包括initWithFormat:
initWithObjects:
和initWithObjectsAndKeys:
-
初始化方法的返回类型应该是
id
.返回类型规定为
id
是因为id
类型表明该类是故意不写明,从而不类型绑定并易于修改,具体类型将依赖于调用时的上下文。比如NSString
提供了initWithFormat:
的方法,当参数是一个NSMutableString
(一个NSString的子类
)时, 方法将返回一个NSMutableString
, 而不是NSString
(不好意思,我没试验出来这种情况). (也可以看这里的单例示例“Combining Allocation and Initialization.” ) -
在自定义初始化方法的实现中,你必须调用预设的初始化方法(designated initializer ).
预设的初始化方法在 “The Designated Initializer” 里有描述 ; 而关于这个问题的完全解释在 “Coordinating Classes.”
简而言之,如果你正在实现一个新的预设初始化方法,它必须要调用父类的预设初始化方法. 如果你要实现别的初始化方法,它就必须调用本类的预设初始化方法,或者再别的初始化方法间接调用到了预设初始化方法。默认的预设初始化方法(如
NSObject
), 就是init
. -
你应该将
self
用初始化方法的返回值赋值,因为初始化方法可能返回的是别的对象而非原先的self. -
如果你要在初始化方法里对实体变量赋值,应该采用直接赋值而非访存方法。
直接赋值避免了访存方法可能触发的副效应.
-
在初始化方法的接触,你必须返回
self
除非初始化失败,那时你可以返回nil
.初始化方法失败在 “Handling Initialization Failure.” 有更详细的讨论
下面的示例描述如何实现一个继承自NSObject
的类的自定义初始化方法,该类含有一个实体变量creationDate
, 用于展示对象是如何创建的:
- - (id)init {
- // Assign self to value returned by super's designated initializer
- // Designated initializer for NSObject is init
- self = [super init];
- if (self) {
- creationDate = [[NSDate alloc] init];
- }
- return self;
- }
(关于使用 if (self)
的模式在“Handling Initialization Failure.” 有讨论)
初始化方法并不需要为每个实体变量提供参数. 比如一个类需要它的实例一个名字和一个数据源,它可能会提供一个形如initWithName:fromURL:
的方法,但非必须的实体变量可能仅需要一个任意值或默认的空值. 那么设置这些实体变量依赖于类似 setEnabled:
, setFriend:
,和 setDimensions:
这样的方法在初始化完成后修改默认值.
下面的例子展示了使用单个参数的初始化方法. 在本例中,类继承自NSView
. 例子显示了你在调用父类的预设初始化函数前可以做的事情.
- - (id)initWithImage:(NSImage *)anImage {
- // Find the size for the new instance from the image
- NSSize size = anImage.size;
- NSRect frame = NSMakeRect(0.0, 0.0, size.width, size.height);
- // Assign self to value returned by super's designated initializer
- // Designated initializer for NSView is initWithFrame:
- self = [super initWithFrame:frame];
- if (self) {
- image = [anImage retain];
- }
- return self;
- }
该例子并不是展示如何应对初始化时发生的问题,如何处理这种问题将在下一段讨论.
处理初始化失败
一般来说,如果初始化方法里产生了问题,你应该对self
调用 release
并返回 nil
.
下面时两大理由:
-
任何对象 (无论时你的类或子类或外部调用者) 可以在初始化方法里接受到
nil
并处理. 在不太可能的情况下,调用者会对对象在调用前建立很多外部关联,你必须要取消这些关联. -
你必须确保
dealloc
方法在被部分初始化的对象上的安全调用.
注意: 你应该仅在失败时对self
调用release
. 如果你在调用父类的初始化函数就返回了nil
就不应该调用release
. 你也应该仅清理已经建立的关联,而不是在dealloc
里处理并返回nil
. 这些步骤一般来说都是处理在检测父类初始化方法的返回值之后的一大块代码区域中的,这也是实践中的常规模式— 一如之前的例子:
- - (id)init {
- self = [super init];
- if (self) {
- creationDate = [[NSDate alloc] init];
- }
- return self;
- }
而下例是出自 “限制与管理” ,展示如何处理参数为不合适的值:
- - (id)initWithImage:(NSImage *)anImage {
- if (anImage == nil) {
- [self release];
- return nil;
- }
- // Find the size for the new instance from the image
- NSSize size = anImage.size;
- NSRect frame = NSMakeRect(0.0, 0.0, size.width, size.height);
- // Assign self to value returned by super's designated initializer
- // Designated initializer for NSView is initWithFrame:
- self = [super initWithFrame:frame];
- if (self) {
- image = [anImage retain];
- }
- return self;
- }
再下例展示了最好做法,当有问题的时候,还会返回错误信息:
- - (id)initWithURL:(NSURL *)aURL error:(NSError **)errorPtr {
- self = [super init];
- if (self) {
- NSData *data = [[NSData alloc] initWithContentsOfURL:aURL
- options:NSUncachedRead error:errorPtr];
- if (data == nil) {
- // In this case the error object is created in the NSData initializer
- [self release];
- return nil;
- }
- // implementation continues...
不要使用异常去反馈此类错误,更多信息可查阅 Error Handling Programming Guide .
协助类
一个类的init...
方法一般值用于初始化本类中声明的实体变量. 通过继承获得的变量则是向super
发送初始化消息:
- - (id)initWithName:(NSString *)string {
- self = [super init];
- if (self) {
- name = [string copy];
- }
- return self;
- }
给super
发送的初始化消息将会让继承层次上所有父类连锁初始化. 因为这是最先调用的,所以可以确保父类的实体变量将在子类的实体变量之前初始化。比如一个Rectangle
对象必然依次初始化为一个NSObject
对象,一个Graphic
对象,一个Shape
对象.
initWithName:
方法与继承的init
方法关联如下图所示 .
Figure 结合继承的初始化方法
一个类必须保证所有继承的初始化方法都可用。比如类A定义了init
方法,而它的子类B定义了initWithName:
方法, 就如上图所示。那么B必须确保init
消息仍然可以成功的初始化一个B的实体
. 最简单的方式就是覆盖继承而来的init
方法然后调用initWithName:
:
- - init {
- return [self initWithName:"default"];
- }
如此,initWithName:
方法将会依次调用到继承的方法,如之前所述。下图则是描述了B类的init调用顺序
.
在你定义的类中,覆盖继承而来的初始化方法,将使得你的代码更加容易移植到别的应用中去。如果你遗漏了一个继承的方法没有覆盖,别人可能会错误的初始化一个你的类的实例.
预设(默认)的初始化方法
在协作类里的例子里initWithName:
应该做为该类的预设(默认)初始化方法。预设初始化方法就是每个类中确保继承来的变量都可以被初始化的方法(通过向父类发信息调用继承方法). 它也是本类别的初始化方法需要在内部调用的方法. 按照Cocoa的惯例,预设初始化方法永远都是最自主的决定新实例的所有特性的方法(一般来说就是参数最多的方法,但不一定).
定义子类时,了解预设初始化方法是很重要的。比如类C,它是类B的子类,实现了一个initWithName:fromFile:
的方法,但除此之外,你还必须确保继承而来的init
和initWithName:
方法对C类仍然可用
, 当然你可以简单的直接在initWithName:
方法中调用initWithName:fromFile:
.
- - initWithName:(char *)string {
- return [self initWithName:string fromFile:NULL];
- }
对于C类的实体,继承而来的init
方法不需要覆盖就自然是调用initWithName:
的新版本,即在内部调用initWithName:fromFile:
.方法调用的关系如下图
上图其实忽略了一个重要的细节,即initWithName:fromFile:
方法,也就是C类的预设初始化方法, 需要向父类发送消息调用继承而来的初始化方法,但究竟调用哪个方法,是init
还是initWithName:
? 结论是不能调用init
, 有两个理由:
-
会引发循环调用(
init调用
C类的initWithName:
, 然后initWithName:又会
调用initWithName:fromFile:
, 而该方法又会再次调用init,如此循环
). -
这样就不能复用B类的
initWithName:方法了
.
因此, initWithName:fromFile:必须调用
initWithName:
:
- - initWithName:(char *)string fromFile:(char *)pathname {
- self = [super initWithName:string];
- if (self) {
- ...
- }
一般原则: 预设初始化方法在其内部调用父类的预设初始化方法。
预设初始化方法会连锁的向各自的父类的预设初始化方法发送消息 , 而其他的初始化方法则向本类的预设初始化方法发消息.
下图展示了 A
, B
, 及C类的初始化方法的关联
. 发向self的消息画在左侧,发向父类的画在右侧
.
注意在B类的
init是向
self发消息调用
initWithName:方法的
. 因此当实际类型是B的时候,init方法就是调用B类的initWithName:
方法, 而当实际类型是C类时,则调用C类的版本.
结合空间分配和初始化
在Cocoa中,一些类定义了将分配空间和初始化这两步结合在一起的创建方法,返回新的初始化完毕对象。这些方法经常被称坐便捷构造方法,并拥有 +
className... 的形式, className 就是该类的名字. 比如, NSString
就有这些方法(当然不是全部):
- + (id)stringWithCString:(const char *)cString encoding:(NSStringEncoding)enc;
- + (id)stringWithFormat:(NSString *)format, ...;
类似的, NSArray也定义了如下便捷方法
:
- + (id)array;
- + (id)arrayWithObject:(id)anObject;
- + (id)arrayWithObjects:(id)firstObj, ...;
重要: 如果没有垃圾回收机制时,在使用这些方法的时候必须理解其内存管理机制(见 “Memory Management” ). 你必须阅读Memory Management Programming Guide 去理解这些快捷构造方法的策略。
快捷构造方法的返回类型都是id
,原因见“Constraints and Conventions.” 中的讨论。
如果初始化方法必须要通知某些信息给空间分配,那么将空间分配和初始化结合在一起就显得相当有用. 比如,假设初始化方法需要的数据来自一个文件,且该文件含有足够的数据去初始化不止一个对象,那么不打开该文件,是不可能知道到底分配了多少对象空间。此种情况下,你可能会实现一个形如listFromFile:
的方法,方法的参数是文件名. 该方法可能去打开文件,看看到底有多少对象被分配了空间,再创建一个足够大的列表对象,其中包含了所有的新对象。过程就是从文件中读取数据,分配空间并初始化对象集合,将对象集合放入列表,在最后返回列表。
把分配空间和初始化放入单个函数里,对想避免分配不使用的对象也很有用. 正如在 “The Returned Object,” 提到的一样init...
方法某些时候可能会把原对象用别的对象所取代.比如, 当initWithName:
方法传递的name已经使用过了,它可能会释放这个方法的消息接受者对象,并返回之前用这个名字分配好的对象. 这意味着,一个对象可能被分配空间后,不经过使用就立刻被释放.
如果决定消息接受者是否需要初始化的代码写在分配空间的代码里,而不是在init...中
, 你就可以避免了对不会使用的实体分配空间的一步.
在下面的例子里,soloist
方法确保了不会有超过一个Soloist实例会被创建
. 它分配和初始化了一个共享的单例:
- + (Soloist *)soloist {
- static Soloist *instance = nil;
- if ( instance == nil ) {
- instance = [[self alloc] init];
- }
- return instance;
- }
注意在此种情况下返回的类型是Soloist *
. 因为这个方法返回的是共享的单例实体,强类型是很合适的,这个方法本身就就不应该被重写.