在一個 Objective-C 應(yīng)用中大部分的工作是由于消息跨越對象的生態(tài)系統(tǒng)被送回和送達而產(chǎn)生的。這些對象中的一部分是由 Cocoa 或者 Cocoa Touch 所提供的類的實例,一部分是你自己的類的實例。
前一章描述了來為一個類定義接口和實現(xiàn)的語法,其中也包括了實現(xiàn)包含為響應(yīng)消息而需要被執(zhí)行的代碼的方法的語法。這一章解釋了如何給一個對象發(fā)送這樣一條消息,并涵蓋了 Objective-C 的一些動態(tài)特征,包括動態(tài)類型和決定哪個方法應(yīng)當(dāng)在運行時被調(diào)用的能力。
在一個對象能夠被使用前,它必須結(jié)合為它的屬性所做的內(nèi)存分配和內(nèi)部值的初始化以被正確地創(chuàng)建。這一章描述了如何嵌套方法調(diào)用來分配和初始化一個對象來保證它是被正確配置的。
盡管在 Objective-C 的對象間中有多種不同的方法來發(fā)送消息,到目前為止最普遍的方法是使用方括號的基礎(chǔ)語法,像這樣:
[someObject doSomething];
左邊的引用,在這個案例中的 someObject
,是消息的接收者。右邊的消息,doSomething
,是訪問接收者的方法的名稱。換句話說,當(dāng)上面這行代碼被執(zhí)行的時候,someObject
將被發(fā)送消息 doSomething
。
前一章描述了如何為一個類創(chuàng)建一個借口,像這樣:
@implementation XYZPerson : NSObject
- (void)sayHello;
@end
和怎樣為創(chuàng)建那個類的實現(xiàn),像這樣:
@implementation XYZPerson
- (void)sayHello {
NSLog(@"Hello, world!");
}
@end
注解:這個例子使用了一個 Objective-C 的字符串字面量,@“Hello, world!”
。字符串是在 Objective-C 中幾個允許為它們的創(chuàng)建而使用的速記文字語法的其中之一。規(guī)定@“Hello, world!”
概念上等同于“An Objective-C string object that represents the string Hello, world!.”
字面量和對象創(chuàng)建將在這一章之后的對象是被動態(tài)創(chuàng)建的中被進一步地解釋。
假設(shè)你已經(jīng)得到了一個 XYZPerson
的對象,你可以像這樣給它發(fā)送 sayHello
的消息:
[somePerson sayHello];
發(fā)送一條 Objective-C 的消息概念上非常像調(diào)用一個函數(shù)。圖2-1為消息“sayHello”
展示了實際的程序流程。
圖 2-1 基本的消息程序流程
為了指定一條消息的接收者,理解指針在 Objective-C 中怎樣被用來指向?qū)ο笫欠浅V匾摹?/p>
C 和 Objective-C 使用變量來跟蹤數(shù)值,就像其他編程語言一樣。
在標(biāo)準(zhǔn)C語言中有許多基本的標(biāo)量變量被定義,包括整數(shù)型,浮點數(shù)型和字符型,它們像這樣被聲明和賦值:
int someInteger = 42;
float someFloatingPointNumber = 3.14f;
局部變量,是在一個方法或函數(shù)中被聲明的變量,像這樣:
- (void)myMethod {
int someInteger = 42;
}
被限定在方法中被定義的作用域中。
在這個例子中,someInteger
在 myMethod
中被聲明為一個局部變量。一旦執(zhí)行到方法的閉合括號,someInteger
將不再可存取。當(dāng)一個局部的標(biāo)量變量(像一個 int 或者一個 float)消失,數(shù)值也將消失。
Objective-C對象,相比之下,則分配得稍許不同。對象通常比方法調(diào)用的簡單作用域生命更長。特別的,一個對象經(jīng)常需要比那些被創(chuàng)建來跟蹤它的原始變量要存活更久的時間。因此,一個對象的存儲空間是動態(tài)地被分配和解除分配的。
注解:如果你習(xí)慣于使用棧和堆,則當(dāng)對象在堆上被分配時,一個局部變量是在棧上被分配的。
這需要你使用 C語言中的指針(指向存儲空間的地址)來追蹤它們在存儲空間中的位置,像這樣:
- (void)myMethod {
NSString *myString = // get a string from somewhere...
[...]
}
盡管指針變量 myString
(星號指示它的指針)的作用域受 myMethod
作用域的限制,它在存儲空間中所指的實際的字符串對象在作用域外可能會有更長的生存時間。舉例來說,它可能已經(jīng)存在,或者你可能需要在其他的方法調(diào)用中傳遞對象。
如果你在發(fā)送一條消息時需要傳遞一個對象,你為方法參數(shù)中的一個提供一個對象指針。前一章描述了聲明只有一個參數(shù)的方法的語法:
- (void)someMethodWithValue:(SomeType)value;
因此聲明一個帶有字符串對象的方法的語法,看起來是像這樣的:
- (void)saySomething:(NSString *)greeting;
你可以像這樣實現(xiàn) saySomething:
的方法:
- (void)saySomething:(NSString *)greeting {
NSLog(@"%@", greeting);
}
指針 greeting
表現(xiàn)得像一個局部變量并且被限制在 saySomething:
的作用域中。盡管它所指的真實的字符串對象先于方法調(diào)用存在,并且將在方法完成后繼續(xù)存在。
注解:NSLog()
使用說明符來指示替代單元。就像C標(biāo)準(zhǔn)函數(shù)庫中的 printf()
函數(shù)。記錄到控制臺的字符串是通過插入提供的值(剩余的參數(shù))來修改格式字符串(第一個參數(shù))的結(jié)果。
在 Objective-C 中有一個額外的可替代的單元,%@
,用來指示一個對象。在運行時,這個說明符將隨著調(diào)用 descriptionWithLocale:
方法(如果它存在)或者提供的對象上的 description
方法中的一個而被替換。description
方法由 NSObject
實現(xiàn)用來傳回類和對象的存儲空間,但是許多 Cocoa 和 Cocoa Touch 的類將它重寫來提供更多有用的消息。在 NSString
的例子中,description
方法簡單地返回了它所代表的字符串字符。
想要獲取更多有關(guān) NSLog()
和 NSString
類的說明符使用的消息,可參見 String Format Specifiers。
就像通過方法參數(shù)傳遞值,方法直接返回值是有可能的。在這章中到現(xiàn)在每一個展示出的方法都有一個返回值類型 void
。這個C語言的關(guān)鍵詞 void
意味著一個方法不返回任何東西。
規(guī)定返回值類型為 int
意味著方法返回一個標(biāo)量整形數(shù)值:
- (int)magicNumber;
方法的實現(xiàn)使用C語言中的 return
聲明來指示在方法在結(jié)束執(zhí)行之后被傳回的值,像這樣:
- (int)magicNumber {
return 42;
}
忽略一個方法會返回值的事實是完全可以接受的。在這個例子中 magicNumber
方法除了返回一個值不做其他任何有用的事,但是像這樣調(diào)用方法沒有任何錯誤:
int interestingNumber = [someObject magicNumber];
你可以用相同的方法從方法中返回對象。舉個例子,NSString
類,提供了一個 uppercaseString
方法:
- (NSString *)uppercaseString;
當(dāng)一個方法返回一個純數(shù)值時做法相同,盡管你需要使用一個指針來追蹤結(jié)果:
NSString *testString = @"Hello, world!";
NSString *revisedString = [testString uppercaseString];
當(dāng)這個方法調(diào)用返回時,revisedString
將會指向一個代表 Hello,world!
的 NSString
對象。
記住當(dāng)實現(xiàn)一個方法來返回一個對象時,像這樣:
- (NSString *)magicString {
NSString *stringToReturn = // create an interesting string...
return stringToReturn;
}
盡管指針 stringToReturn
出了作用域,字符串對象繼續(xù)存在當(dāng)它被作為一個返回值被傳遞時。
在這種狀況下有很多存儲空間管理的考慮:一個返回了的對象(在堆中被創(chuàng)建)需要存在足夠長的時間使它被方法的原有調(diào)用者使用,但不是永久存在,這是因為那樣會導(dǎo)致內(nèi)存泄露。在很大程度上,具有自動引用計數(shù)(ARC)特征的 Objectiive-C 編譯程序會為你照顧到這些需要考慮的地方的。
無論何時你正在寫一個方法的實現(xiàn),你可以獲得一個重要的隱藏值, self
。概念上,self
是一個指向"已經(jīng)接收到這個消息的對象"的方法。它是一個指針,就像上文的值 greeting
,可以被用來在現(xiàn)在接收對象上調(diào)用方法。
你也許決定通過修改 sayHello
方法來使用上文的 saySomething:
方法來重構(gòu) XYZPerson
的實現(xiàn),因此將 NSLog()
的調(diào)用移動到不同的方法中。這將意味著你能進一步增加方法,像 sayGoodbye
,那將每次聯(lián)系 saySomething:
方法來解決真實的問候過程。如果你稍后想將每一個問候在用戶接口中的每個文本框中展示出來,你只需要修改 saySomething:
方法而不是仔細檢查并單獨地調(diào)整每一個問候方法。
新的在當(dāng)前對象上使用 self
來調(diào)用消息的實現(xiàn)將會是這樣的:
@implementation XYZPerson
- (void)sayHello {
[self saySomething:@"Hello, world!"];
}
- (void)saySomething:(NSString *)greeting {
NSLog(@"%@", greeting);
}
@end
如果你用這種更新過的實現(xiàn)把消息 sayHello
發(fā)送給對象 XYZPerson
,實際的程序流程將會在圖2-2中顯示。
圖 2-2 和自己通信時的程序流程
在 Objective-C 中對你來說有另外一個重要的關(guān)鍵詞,叫作 super
。發(fā)送一條消息給 super
是調(diào)用一個被進一步完善繼承鏈的超類所定義的方法的途徑。super
最普遍的用法是重寫一個方法的時候。
比方說你想要創(chuàng)建一個新類型的 person 類,“shouting person”類,它每一句問候都用大寫字母展示出來。你可以復(fù)制整個 XYZPerson
類并修改每個方法中的每個字符串使它們是大寫的。但是最簡單的方法是創(chuàng)建一個新的繼承自 XYZPerson
的類,只要重寫 saySomething:
方法這樣它就會以大寫的形式展現(xiàn)出來,像這樣:
@interface XYZShoutingPerson : XYZPerson
@end
@implementation XYZShoutingPerson
- (void)saySomething:(NSString *)greeting {
NSString *uppercaseGreeting = [greeting uppercaseString];
NSLog(@"%@", uppercaseGreeting);
}
@end
這個例子聲明了一個額外的字符串指針,uppercaseGreeting
,并且將發(fā)給初始對象 greeting
的消息 uppercaseString message
返回的值賦給它。正如你早一些所見到的,這將成為一個將原始字符串中的每個字符轉(zhuǎn)換為大寫新的字符串對象。
因為 sayHello
由 XYZPerson
實現(xiàn),而 XYZShoutingPerson
是用來繼承 XYZPerson
的,你也可以在 XYZShoutingPerson
對象上調(diào)用 sayHello
對象。當(dāng)你在 XYZShoutingPerson
對象上調(diào)用 sayHello
對象時,[self saySomething:...]
的調(diào)用將使用重寫過的實現(xiàn)并且將問候顯示為大寫,實際的程序流程圖在圖2-3中顯示。
圖 2-3 對于一個覆寫方法的程序流程
新的實現(xiàn)并不是理想的,是因為如果你確實稍后決定修改 saySomething
的 XYZPerson
實現(xiàn),用用戶接口元素來展示問候而不是通過 NSLog()
,你也將需要修改 XYZShoutingPerson
的實現(xiàn)。
一個更好的想法將會是改變 saySomething
的 XYZShoutingPerson
版本來調(diào)用超類(XYZPerson)實現(xiàn)來處理實際的問候:
@implementation XYZShoutingPerson
- (void)saySomething:(NSString *)greeting {
NSString *uppercaseGreeting = [greeting uppercaseString];
[super saySomething:uppercaseGreeting];
}
@end
由于給對象 XYZShoutingPerson
發(fā)送消息 sayHello
而來的實際的程序流程如圖2-4所示。
圖 2-4 和超類通信時的程序流程
正如在這章早些時候描述的,給 Objective-C 對象的存儲空間的分配是動態(tài)的。創(chuàng)建一個對象的第一步是確認(rèn)有足夠的存儲空間,不僅是對被一個對象的類所定義的屬性來說,也要滿足在它的繼承鏈中在每一個超類上所定義的屬性。
根類 NSObject
提供了一個類方法,alloc
,為你處理這一過程:
+ (id)alloc;
注意到這個方法的返回值類型為 id
。這在 Objective-C 中是一個特殊的關(guān)鍵詞,表示“一些類型的對象”。它是一個對象的指針,就像(NSObject *),但是又是特別的因為它不使用星號。它在這章的稍后,Objective-C 是一種動態(tài)語言中會被更仔細地描述。
alloc
方法有另外一個重要的任務(wù),就是通過將存儲空間設(shè)為零來清空為對象的屬性而分配的存儲空間。這避免了一個尋常的問題,即存儲空間含有之前曾存儲的垃圾。但是這不足以完全初始化一個對象。
你需要將對 alloc
的調(diào)用和對另一個 NSObject
的方法 init
的調(diào)用結(jié)合起來:
- (id)init;
init
方法被類使用以來確認(rèn)它的屬性在創(chuàng)建時有合適的初始值,它將在下一章被詳細介紹。
注意到 init
也返回 id
。
如果一個方法返回一個對象指針,將那個方法的調(diào)用作為接收者嵌套進另一個方法的調(diào)用時有可能的,由此在一個聲明中結(jié)合了多個消息的調(diào)用。正確分配和初始化一個對象的方法是在對 init
的調(diào)用中嵌套對 init
的調(diào)用,像這樣:
NSObject *newObject = [[NSObject alloc] init];
這個例子設(shè)置了 newObject
對象來指向一個新被創(chuàng)建的 NSObject
實例。
最內(nèi)部的調(diào)用第一個被實現(xiàn),所以 NSObject
類被送到返回一個新被分配的 NSObject
實例的 alloc
方法。這個返回的對象之后被作為 init
消息的接收者被使用,它自己返回對象并賦給 newObject
指針,正如在圖2-5中顯示的那樣。
圖 2-5 嵌套 alloc
和 init
消息
注解:init
返回一個由 alloc
創(chuàng)建的不同的對象是有可能的,所以正如展示的那樣,嵌套調(diào)用是最好的嘗試。
永遠不要在沒有將任何指針賦給對象的情況下初始化一個對象。作為一個例子,不要這樣做:
NSObject *someObject = [NSObject alloc];
[someObject init];
如果對 init
的調(diào)用返回了一些其他的對象,你將留下一個初始被分配過但從沒有初始化的對象的指針。
一些對象需要用需要的值來初始化。一個 NSNumber
對象,舉例來說,必須用它需要代表的數(shù)值來創(chuàng)建。
NSNumber
類定義了幾種初始化方法,包括:
- (id)initWithBool:(BOOL)value;
- (id)initWithFloat:(float)value;
- (id)initWithInt:(int)value;
- (id)initWithLong:(long)value;
帶有參數(shù)的初始化方法的調(diào)用和普通的 init
方法是一樣的———一個 NSNumber
對象像這樣被分配和初始化:
NSNumber *magicNumber = [[NSNumber alloc] initWithInt:42];
正如在前邊的章節(jié)中所提到的,一個類也可以定義工廠方法。工廠方法提供了傳統(tǒng)的 alloc]
,init]
過程的不許嵌套兩個方法的選擇。
NSNumber
類定義了幾個類工廠方法來匹配它的初始化方法,包括:
+ (NSNumber *)numberWithBool:(BOOL)value;
+ (NSNumber *)numberWithFloat:(float)value;
+ (NSNumber *)numberWithInt:(int)value;
+ (NSNumber *)numberWithLong:(long)value;
一個工廠方法像這樣被使用:
NSNumber *magicNumber = [NSNumber numberWithInt:42];
這實際上和之前使用 alloc]
initWithInt:]
的例子相同。類工廠方法通常指直接調(diào)用 alloc
和相關(guān)的 init
方法,它為方便使用而被提供。
創(chuàng)建一個類的實例時使用類方法 new
是有可能的。這個方法由 NSObject
提供并且在你自己的超類中不需要被覆寫。
這實際上和調(diào)用沒有參數(shù)的 alloc
和 init
是一樣的:
XYZObject *object = [XYZObject new];
// is effectively the same as:
XYZObject *object = [[XYZObject alloc] init];
一些類允許你使用簡潔的,文字的語法來創(chuàng)建實例。
你可以創(chuàng)建一個 NSString
實例,舉例來說,使用一個特殊的文字記號,像這樣:
NSString *someString = @"Hello, World!";
這實際上和分配,初始化一個 NSString
或者使用它的類工廠方法中的一個相同:
NSString *someString = [NSString stringWithCString:"Hello, World!"
encoding:NSUTF8StringEncoding];
NSNumber
類也允許各種各樣的文字:
NSNumber *myBOOL = @YES;
NSNumber *myFloat = @3.14f;
NSNumber *myInt = @42;
NSNumber *myLong = @42L;
此外,這些例子中的每一個實際上和使用相關(guān)的初始化方法或者一個類工廠方法相同。
你也可以使用一個框表達式來創(chuàng)建一個 NSNumber
,像這樣:
NSNumber *myInt = @(84 / 2);
在這種情況中,表達式被用數(shù)值表示,而且一個 NSNumber
實例伴隨著結(jié)果被創(chuàng)建。
Objective-C 也支持文字來創(chuàng)建不可變的 NSArray
和 NSDictionary
對象;這些將在 Values and Collections
中進一步討論。
正如之前所提到的,你需要使用一個指針來追蹤存儲空間中的一個對象。因為 Objective-C 的動態(tài)特征,你為那個指針使用什么特定的類型都沒有關(guān)系——當(dāng)你給它發(fā)送消息時,正確的方法將總是在相關(guān)的對象上被調(diào)用。
id
型定義了一個通用的對象指針。當(dāng)聲明一個變量時使用 id
是有可能的,但你會失去關(guān)于對象編譯時的信息。
考慮以下的代碼:
id someObject = @"Hello, World!";
[someObject removeAllObjects];
在這種情況下,someObject
將指向一個 NSString
實例,但是編譯器不知道任何事,而事實上它是對象的某一類型。消息 removeAllObjects
由一些 Cocoa 或者 Cocoa Touch 對象(例如 NsMutableArray)定義所以編譯器不會抱怨,盡管這個代碼因為一個 NSString
對象不能響應(yīng) removeAllObjects
而會在運行時會生成一個異常。
使用一個靜態(tài)類型來重寫編寫代碼:
NSString *someObject = @"Hello, World!";
[someObject removeAllObjects];
意味著編譯器將會由于 removeAllObjects
沒有在任何它所知道的公共的 NSString
接口中被聲明而生成一個錯誤。
因為對象的類是在運行時被決定的,當(dāng)創(chuàng)建或者使用一個實例時你無論給變量指定什么類型都沒有差別。要使用這章早些時候描述的 XYZPerson
和 XYZShoutingPerson
類,你可能要使用下面的代碼:
XYZPerson *firstPerson = [[XYZPerson alloc] init];
XYZPerson *secondPerson = [[XYZShoutingPerson alloc] init];
[firstPerson sayHello];
[secondPerson sayHello];
盡管 firstPerson
和 secondPerson
作為 XYZPerson
的對象都是靜態(tài)類型的,secondPerson
在運行時將會指向一個 XYZShoutingPerson
對象。當(dāng) sayHello
方法在每一個對象上被調(diào)用時,正確的實現(xiàn)將會被使用;對于 secondPerson
,這意味著 XYZShoutingPerson
的版本。
如果你需要確定一個對象是否和另一個對象相同,記住你在使用指針是重要的。
標(biāo)準(zhǔn)的C語言等號運算符 == 被用來檢測兩個變量的值是否相同,像這樣:
if (someInteger == 42) {
// someInteger has the value 42
}
當(dāng)處理對象的時候,運算符 == 被用來檢測兩個單獨地指針是否指向一個相同的對象:
if (firstPerson == secondPerson) {
// firstPerson is the same object as secondPerson
}
如果你需要檢測兩個對象是否代表相同的數(shù)據(jù),你需要調(diào)用一個像 isEqual:
的方法,從 NSObject
可以獲得:
if ([firstPerson isEqual:secondPerson]) {
// firstPerson is identical to secondPerson
}
如果你需要比較一個對象是否比另一個對象代表了更大或更小的值,你不能使用標(biāo)準(zhǔn)C語言的比較運算符 > 和 <。相反,像 NSNumber
,NSString
和 NSDate
這樣的基本類型,提供了一個方法 compare:
:
if ([someDate compare:anotherDate] == NSOrderedAscending) {
// someDate is earlier than anotherDate
}
在你聲明它們的時候初始化純量變量總是一個好主意,否則它們的初始值將包含前一個堆棧的垃圾內(nèi)容:
BOOL success = NO;
int magicNumber = 42;
這對對象指針來說不是必要的,因為編譯器將自動把變量設(shè)為 nil
如果你不規(guī)定任何其他的初始值:
XYZPerson *somePerson;
// somePerson is automatically set to nil
nil
的值對初始化一個對象指針來說是最安全的,如果你沒有另一個值來使用的話。因為在 Objective-C 中發(fā)送消息給 nil
是完全可以接受的。如果你確實給 nil
發(fā)送了消息,顯然什么都不會發(fā)生。
注解:如果你期待從發(fā)送給 nil
的消息中獲得一個返回值,返回值將會是對象返回類型的 nil
型,數(shù)字類型的 0, BOOL
類型的 NO
。返回的結(jié)構(gòu)擁有所有初始化為 0 的成員。
如果你需要檢查確認(rèn)一個對象不是 nil
(一個變量指向存儲空間中的一個對象),你可以使用標(biāo)準(zhǔn)C語言中的不等運算符:
if (somePerson != nil) {
// somePerson points to an object
}
或者簡單地提供變量:
if (somePerson) {
// somePerson points to an object
}
如果 somePerson
變量是 nil
,它的邏輯值是 0
(false)。如果它有地址,它就不是零,所以被認(rèn)為是 true。
相同的,如果你需要檢查一個 nil
變量,你可以使用相等運算符:
if (somePerson == nil) {
// somePerson does not point to an object
}
或只是使用 C 語言邏輯非操作符:
if (!somePerson) {
// somePerson does not point to an object
}
1.在你的項目里從上一章最后的練習(xí)中打開 main.m
文件并且找到 main()
函數(shù)。作為對于任何用 C 語言寫的可執(zhí)行的程序,這個函數(shù)代表了你應(yīng)用的起點。
使用 alloc
和 init
創(chuàng)建一個新的 XYZPerson
實例,然后調(diào)用 sayHello
方法。
注解:如果編譯器沒有自動提示你,你將需要在 main.m
的頂部導(dǎo)入頭文件(包含 XYZPerson
接口)
2.實現(xiàn)在這一章早些時候展示的方法 saySomething:
,并且重新編寫方法 sayHello
來使用它。添加各種其他的問候并且在你之前創(chuàng)建的每一個實例上調(diào)用它們。
3.為 XYZShoutingPerson
類創(chuàng)建新的類文件,設(shè)置成繼承自 XYZPerson
。
覆寫 saySomething:
方法來展示大寫的問候,并且測試在 XYZShoutingPerson
實例上的行為。
4.實現(xiàn)在前一章你聲明的 XYZPerson
類 person
工廠方法,來返回一個被正確分配和初始化的 XYZPerson
類的實例,然后在 main()
中使用方法來代替嵌套的 alloc
和 init
。
提示:嘗試使用 [[self alloc] init] 而不是使用 [[XYZPerson alloc] init]。
在一個類工廠方法中使用 self
意味著你在指向類本身。
這意味著你不必在 XYZShoutingPerson
實現(xiàn)中覆寫 person
方法來創(chuàng)建正確的實例。通過檢查以下代碼來測試這個:
XYZShoutingPerson *shoutingPerson = [XYZShoutingPerson person];
創(chuàng)建正確類型的對象。
5.創(chuàng)建一個新的局部的 XYZPerson
指針,但是不包括任何其他賦值。
使用一個轉(zhuǎn)移指令(if
聲明)來檢查變量是否自動被賦為 nil
。
更多建議: