對象分配空間與初始化
使用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
方法關聯如下圖所示 .
結合繼承的初始化方法
一個類必須保證所有繼承的初始化方法都可用。比如類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 *
. 因爲這個方法返回的是共享的單例實體,強類型是很合適的,這個方法本身就就不應該被重寫.