分配并初始化對象
分配并初始化對象
在Objective-C中創(chuàng)建一個對象需要兩步:
● 為新的對象分配空間
● 對分配的空間進行初始化
在沒有完成上述兩個步驟之前,對象是不能用的。上述的兩個步驟是分別通過不同的方法來完成的,但是通常是在一行代碼中進行的:
id anObject = [[Rectangle alloc] init];
把分配和初始化分開進行使得我們可以對每個步驟進行單獨的控制。下面的內容我們將先討論分配對象然后是初始化對象。
在Objective-C中,我們使用NSObject類中定義的方法來為對象分配空間(或者說是分配對象)。NSObject類中定義了兩個用來完成這項工作的方法:alloc以及allocWithZone:。
這兩個方法都能分配足夠大的空間以便容納屬于該消息接收者類的所有實例變量。在派生類中我們不需要對這兩個方法進行重寫。
alloc和allocWithZone:這兩個方法會對新對象的isa實例變量進行初始化,使得他指向該類的類對象。其他所有的變量會被初始化為0.一般情況下,在使用對象之前都需要對其進行更加明確的初始化。
這種初始化工作是由和類相關的實例方法來完成的。按照慣例,就是那些以init開始的方法。如果不需要參數(shù),那么方法名稱就是簡單的init四個字母。如果初始化時需要參數(shù),就應該在init后增加相應的標簽。如NSView類的對象就可以使用initWithFrame:方法來進行初始化。
init方法的返回值
一般情況下,init...是對消息接收者的實例變量進行初始化,并返回該對象。返回一個可用的對象是init...返回的職責。
然而,在一些情況下這種返回可用對象的職責要求init...方法返回可能不是消息接收者對象,而是別的對象。例如,如果一個類中維護的是命名對象的列表,在initWitdName:方法中就會拒絕把相同的名稱賦給兩個不同的對象。當把一個已經(jīng)使用的名稱賦給一個新對象的時候, initWithName:方法可能會先釋放掉新的對象,而返回之前已經(jīng)存在的那個對象,這樣以確保對象名稱的唯一性,實現(xiàn)根據(jù)名稱獲取對應對象的功能。
在少數(shù)情況下,還需要在init...方法中做一些其他的事情。例如,方法initFromFile:從傳入的文件中獲取數(shù)據(jù)來對對象進行初始化。如果傳入的文件名稱不是真實的存在的文件,那么該初始化方法就不能完成正常的初始化工作。在這種情況下,init...就可能釋放掉消息接收者這個對象,然后返回nil,以便表明不能創(chuàng)建該對象。
正是由于init...方法的返回值可以不是剛剛創(chuàng)建的對象————消息的接收者,甚至可能返回nil,因此在程序中正確使用init...方法的返回值就顯得很重要了,而不僅僅只是需要正確使用alloc 或者allocWithZone:方法的返回值。下面代碼中的做法是非常危險的,因為它沒有考慮到返回值為nil的情況:
id anObject = [SomeClass alloc];
[anObject init];
[anObject someOtherMessage];
相反,為了能夠正確地對對象進行初始化,我們應該把分配和初始化對象放在一行代碼中:
id anObject = [[SomeClass alloc] init];
[anObject someOtherMessage];
如果init..方法的返回值確實可能返回nil, 那么我們在進行后面的處理之前應該先做檢查:
id anObject = [[SomeClass alloc] init];
if ( anObject)
[anObject someOtherMessage];
else
...
初始化方法的實現(xiàn)
在Objective-C中,一旦創(chuàng)建了對象,對象空間中除了isa之外的所有比特也就是全部的實例變量的值都被置為0。在某些情況下,這也許正是我們期望的。在很多別的情況下,我們希望把對象的實例變量初始化為別的缺省的值,或者是根據(jù)傳入的參數(shù)來對其進行初始化。此時,我們就需要自己編寫初始化方法。在Objectic-C中,初始化方法和普通的方法相比是有一些限制以及習慣做法的。
初始化方法相關的限制和慣例
關于初始化方法有如下的一些限制和慣例:
● 按照慣例,初始化方法名稱都是以init開始的。
基礎框架中提供的initWithFormat:,initWithObject:以及initWithObjectAndKey:等方法的命名就是遵循這個慣例的。
● 初始化方法的返回值應該id類型的。
返回值是id類型是出于這樣的考慮:初始化方法返回值不是具體的類類型,是可變的,這取決于上下文。例如:NSString類提供了一個initWithFormat:的方法。然而,當向NSMutableString(NSString類的派生類)的對象發(fā)送initWithFormat:消息的時候,其返回的是NSMutableString類型的對象,而不是NSString類型的。
● 在實現(xiàn)自定義初始化方法的時候,最終都是要調用一個“指定的”初始化方法。
更多信息請參閱“指定的”初始化方法以及“初始化時類之間的協(xié)調”兩小節(jié)。
簡而言之,如果我們需要編寫的就是“指定的”那個初始化方法,那么我們必須在其實現(xiàn)中調用超類的“指定的”初始化方法。如果我們實現(xiàn)的別的初始化方法那么我們必須在實現(xiàn)時調用該類自身的“指定的”初始化方法,或者是調用那些最終調用了“指定的”初始化方法的初始化方法。(譯者注:好繞呀!)
● 在初始化方法中我們必須給self賦值為方法返回的那個對象。這是因為初始化方法的返回值很有可能是和原來不同的對象。
● 在初始化方法中如果需要設置實例變量的值,通常都是直接給實例變量賦值,而不是使用其訪問方法。
直接訪問實例變量可以避免使用訪問方法有可能帶來的負面影響。
● 在初始化方法的最后必須返回self。除非初始化失敗了可以返回nil。
關于初始化失敗更多在“初始化失敗的處理”。
下面的示例代碼實現(xiàn)了一個NSObject類的派生類的自定初始化方法。該類有一個實例變量creationDate,用于表示對象的創(chuàng)建時間:
-(id) init
{
//給self賦值為超類的“指定的”初始化方法的返回值
//也就是NSObject類的init方法
self = [super init];
if (self)
{
createDate = [[NSDate alloc] init];
}
return self;
}
之所以使用if(self)的原因將在“初始化失敗的處理”章節(jié)中進行討論。
在初始化方法中沒有必要為所有的參數(shù)頭提供相應的參數(shù)。例如,如果某個類中需要一個名稱和數(shù)據(jù)源,那么他可以提供初始化方法:initWithName:FromURL:。該類中的其他實例變量可以是任意值或者缺省地設置為nil。可以在初始化完成后,依靠諸如:setEnable:,setFriend:,setDemensions:等方法來修改這些初始化中設置的缺省值。
在下面的示例程序中初始化方法需要一個參數(shù)。其中的類是從NSView類繼承而來。程序為我們展示了在調用超類的“指定的”初始化方法之前是可以做一些其他工作的:
-(id)initWithImage:(NSImage *)anImage
{
//根據(jù)圖像的大小來設置新實例的大小
NSSize size = anImage.size;
NSRect frame = NSMakeRect(0.0, 0.0, size.width, size.height);
//給self賦值為超類的“指定的”初始化方法的返回值
//也就是NSView類中的initWithFrame:方法
self = [super initWithFrame:frame];
if (self)
{
image = [anImage retain];
}
return self;
}
這個示例程序中沒有演示如果初始化失敗應該怎么做。這一點我們將在下節(jié)中進行討論。
初始化失敗的處理
通常情況,如果初始化失敗,我們應該調用self的release方法,并返回nil。
鑒于這種情況,我們必須注意以下兩點:
● 任何接收到初始化返回nil的對象都應該能夠處理這種情況。如果在調用某個對象的初始化方法之前已經(jīng)建立和該對象的外部引用(這種情況不太可能發(fā)生),那么就必須在處理初始化失敗時解除這種引用。
● 我們必須保證dealloc方法能夠處理這種初始化沒有完全完成的情況,也就是實例的部分數(shù)據(jù)被初始化了,部分數(shù)據(jù)沒有被成功初始化的情況。
注意:只有在靠近初始化失敗的地方才應該調用self的release方法。如果是調用超類的初始化方法返回nil,那么此時不應該調用self的release方法。此時需要做的只是解除那些在dealloc方法中沒有解除的對該對象的引用,然后返回nil。
下面的實例程序是基于“初始化方法相關的限制和慣例”小節(jié)中的程序,展示了在初始化方法中如何處理參數(shù)不正確的情況:
-(id) initWithImage:(NSImage *)anImage
{
if ( anImage? == nil )
{
[self release];
return nil;
}
//根據(jù)圖像的大小來設置新實例的大小
NSSize size = anImage.size;
NSRect frame = NSMakeRect(0.0, 0.0, size.width, size.height);
//給self賦值為超類的“指定的”初始化方法的返回值
//也就是NSView類中的initWithFrame:方法
self = [super initWithFrame:frame];
if (self)
{
image = [anImage retain];
}
return self;
}
下面程序演示了一種更好的做法:如果初始化失敗,則通過一個NSError對象返回更有意義的錯誤信息。
-(id) initWithURL:(NSURL *)aURL error:(NSError **)errorPtr
{
self = [super init];
if ( self )
{
NSData * data = [[NSData] alloc] initWithContentsOfURL:aURL
options:NSUncahedRead
error:errorPtr];
if ( data == nil )
{
//此時,表示錯誤信息的對象是在NSData的初始化方法中創(chuàng)建的。
[self release];
return nil;
}
}
//其他實現(xiàn)代碼
...
}
我們不應該使用異常來處理這種錯誤。更多信息請參閱《錯誤處理編程指南》一書。
初始化時類之間的協(xié)調
一個類的init...方法中通常只對在該類中定義的變量進行初始化。對繼承而來的實例變量的初始化是通過向super發(fā)送消息,調用在繼承關系圖中更高層的類的初始化方法來完成的:
-(id)initWithName:(NSString *)string
{
self = [super init];
if (self)
{
name =[string copy];
}
return self;
}
通過向super發(fā)送消息可以把繼承關系圖中的所有相關類關聯(lián)起來。由于是最先向super發(fā)送消息的,所以這種方式確保了超類的實例變量是在派生類的實例變量之前被初始化的。例如,Rectangle類的對象最先應該是作為NSObject類的對象來進行初始化,然后是Graphic類的對象,再次是Shape類的對象,最后才是Rectangle類的對象。
之前程序段中的initWithName:和繼承而來的init方法的之間的關系所示:
如上圖中,由于A中定義了init方法,作為A類的派生類B繼承了該init方法。但是由于A中的init方法只是對定義在A中的實例變量進行了初始化,因此繼承而來的init方法不能對B類中的實例變量進行初始化,因此B中必須重寫init方法,以便B類的init方法即能完成對繼承而來實例變量的初始化也能完成對自身中定義的實例變量的初始化。:
- init
{
return [self initWithName:"default"];
}
而在initWithName:方法中調用了繼承而來的init方法,A類和B類初始化的關系變成所示:
這樣做會使得自己的程序更具有兼容性。如果沒有對這樣的繼承而來的方法進行重寫,可能會導致別人使用了沒有被正確初始化的對象。
下圖為今年部分iOS開發(fā)的視頻教程,因為不定時更新中故不做多的截圖,如果有iOS開發(fā)上的問題不懂或者需要視頻教程可以看我的個人簡介。
不定時更新中。