用Objective-C進行面向?qū)ο蟮木幊?/h1>
從Cocoa事件驅(qū)動架構(gòu)的機制和采用的范式可以看出它廣泛地使用了面向?qū)ο蟮姆椒ābjective-C是Cocoa的主要開發(fā)語言,也是完全面向?qū)ο蟮恼Z言,盡管它的基礎(chǔ)是ANSI C。它為消息的分發(fā)提供運行環(huán)境支持,也為定義新類指定了語法規(guī)則。Objective-C支持絕大多數(shù)其它面向?qū)ο缶幊陶Z言(比如C++和Java)具有的抽象和機制,包括繼承、封裝、重用性、和多態(tài)。
但是,Objective-C在一些重要的方面又和其它面向?qū)ο蟮恼Z言不同。舉例來說,Objective-C和C++不同,不支持操作符重載、模板、或多重繼承。Objective-C也不象Java那樣,具有“垃圾收集”機制,可以自動釋放不再需要的對象(雖然它有機制和規(guī)則可以完成同樣的任務(wù))。
雖然Objective-C沒有這些特性,但是它作為一種面向?qū)ο缶幊陶Z言的能力可以進行補償和超越。本文接下來的部分將探討Objective-C的特殊能力,同時概要介紹Java版本的Cocoa。
進一步閱讀:本部分的很多內(nèi)容是Objective-C權(quán)威指南—Objective-C編程語言—一書上的概括。有關(guān)Objective-C的詳細描述,請查閱該文檔。
Objective-C的優(yōu)點
如果您是一個面向過程的編程人員,對面向?qū)ο蟮母拍畈皇煜?,則可以首先將對象的本質(zhì)考慮為一個結(jié)構(gòu)體加上關(guān)聯(lián)的函數(shù),這可能有助于理解本文的內(nèi)容。這個概念和現(xiàn)實相差不太遠,特別是在運行環(huán)境的實現(xiàn)方面。
每個Objective-C對象都隱藏著一個數(shù)據(jù)結(jié)構(gòu),它的***個成員變量—或者說是實例變量—是“isa指針”(大多數(shù)剩下的成員變量由對象的類或超類來定義)。顧名思義,isa指針指向的是對象的類,這個類也是一個對象,有自己的權(quán)限(參見圖2-1),是根據(jù)類的定義編譯而來的。類對象負責(zé)維護一個方法調(diào)度表,該表本質(zhì)上是由指向類方法的指針組成的;類對象中還保留一個超類的指針,該指針又有自己的方法調(diào)度表和超類(還有所有通過繼承得到的公共和保護的實例變量)。isa指針對消息分發(fā)機制和Cocoa對象的動態(tài)能力很關(guān)鍵。
下圖為對象的isa指針

對隱藏在對象表面下的工作機制的驚鴻一瞥只是使您簡單了解Objective-C運行環(huán)境如何支持消息分發(fā)、繼承、和一般對象行為的其它方面。但是它對于理解Objective-C的主要能力—動態(tài)能力是很必要的。
動態(tài)能力
Objective-C是一種非常動態(tài)的語言。這種動態(tài)能力使程序可以突破編譯和連接時的約束,將更多符號辨識的工作轉(zhuǎn)移到處于受控狀態(tài)的運行環(huán)境上。Objective-C比其它編程語言具有更強的動態(tài)能力,這種能力來源于如下三個方面:
動態(tài)類—在運行時確定對象的類
動態(tài)綁定—在運行時確定要調(diào)用的方法
動態(tài)裝載—在運行時為程序增加新的模塊
Objective-C為動態(tài)類型引入了一個稱為id的數(shù)據(jù)類型,用于表示任意的Cocoa對象。列表2-2中的代碼實例顯示了這種基本對象類型的典型用法:
id word; |
while (word = [enm nextObject]) { |
// etc.... |
id數(shù)據(jù)類型使我們有可能在運行時進行任意的對象替換。您因此可以在代碼中用運行時的因子指定希望使用的對象類型。動態(tài)類型使對象中的關(guān)聯(lián)可以在運行時確定,而不需要在靜態(tài)設(shè)計時強制指定對象類型。編譯時的類型檢查可以確保更加嚴格的數(shù)據(jù)完整性,但是作為交換,動態(tài)類型則給您的程序更大的靈活性。而且,通過對象的內(nèi)省(比如詢問動態(tài)類型轉(zhuǎn)換后的匿名對象所屬的類),您仍然可以在運行時確認對象的類型,并驗證它是否可以進行特定的操作(當(dāng)然,您總是可以在需要的時候進行靜態(tài)類型檢查)。
動態(tài)類型為Objective-C的第二種動態(tài)能力—動態(tài)綁定—提供了物質(zhì)基礎(chǔ)。正如動態(tài)類型將對象類的確定推遲到運行時一樣,動態(tài)綁定將調(diào)用方法的確定也推遲到運行時。在編譯時,方法的調(diào)用并不和代碼綁定在一起,只有在消息確實發(fā)送出來之后,才確定被調(diào)用的代碼。通過動態(tài)類型和動態(tài)綁定技術(shù),您的代碼每次執(zhí)行都可以得到不同的結(jié)果。運行時因子負責(zé)確定消息的接收者和被調(diào)用的方法。
運行時的消息分發(fā)機制為動態(tài)綁定提供支持。當(dāng)您向一個動態(tài)類型確定了的對象發(fā)送消息時,運行環(huán)境系統(tǒng)會通過接收者的isa指針定位對象的類,并以此為起點確定被調(diào)用的方法,方法和消息是動態(tài)綁定的。而且,您不必在Objective-C代碼中做任何工作,就可以自動獲取動態(tài)綁定的好處。您在每次發(fā)送消息時,特別是當(dāng)消息的接收者是動態(tài)類型已經(jīng)確定的對象時,動態(tài)綁定就會例行而透明地發(fā)生。
動態(tài)裝載是***一種動態(tài)能力。它是Cocoa的一個特性,依賴于Objective-C的運行環(huán)境支持。通過動態(tài)裝載,Cocoa程序可以在需要的時候才裝載執(zhí)行代碼和資源,而不是在啟動的時候裝載所有的程序組件??蓤?zhí)行代碼(在裝載之前就連接好了)通常包含一些新的、會被集成到應(yīng)用程序運行時映像的類。代碼和本地化資源(包括nib文件)被包裝在程序包中,可以通過Foundation框架中的NSBundle類中定義的方法來顯式裝載。
這種程序代碼和資源的“遲緩裝載(lazy-loading)”機制降低了對系統(tǒng)內(nèi)存的要求,從而提升了程序的整體性能。更重要的是,動態(tài)裝載使應(yīng)用程序變得可擴展。您可以考慮在應(yīng)用程序中采用插件架構(gòu),使自己和其它開發(fā)者可以通過附加的模塊進行定制,應(yīng)用程序可以在發(fā)布數(shù)月或數(shù)年后動態(tài)裝載附加的模塊。如果設(shè)計是正確的,這些類就不會和已經(jīng)存在的類發(fā)生沖突,因為每個類都封裝了自己的實現(xiàn)并擁有自己的名字空間。
語言擴展
Objective-C在基本語言上做了兩個擴展:范疇(categories)和協(xié)議(protocols),它們是強大的軟件開發(fā)工具。這兩個擴展引入了聲明方法并將它們關(guān)聯(lián)到某個類的技術(shù)。
范疇
范疇提供一種為某個類添加方法而又不必制作子類的途徑。范疇中的方法會變成類的一部分(在您的應(yīng)用程序的作用域內(nèi)),并為該類的所有子類所繼承。在運行時,原始方法和通過范疇添加的方法之間沒有差別,您可以向類(或者它的子類)實例發(fā)送消息,以調(diào)用范疇中定義的方法。
范疇不僅是一種為類添加行為的便利方法,還可以對方法進行分組,將相關(guān)的方法放在不同的范疇中。范疇對于組織規(guī)模大的類特別方便,例如當(dāng)幾個開發(fā)者同時在一個類上工作時,您甚至可以將不同的范疇放在不同的源文件中。
范疇的聲明和實現(xiàn)很象子類。在語法上,唯一的區(qū)別是范疇的名稱需要跟在@interface或@implementation導(dǎo)向符之后,且放在園括號中。舉例來說,假定您希望為NSArray類增加一個方法,以便用更加結(jié)構(gòu)化的方式打印集合的描述。那么您可以在范疇的頭文件中書寫如下的聲明代碼:
#import <Foundation/NSArray.h> // if Foundation not already imported |
|
@interface NSArray (PrettyPrintElements) |
- (NSString *)prettyPrintDescription; |
@end |
然后在實現(xiàn)文件中書寫如下代碼:
#import "PrettyPrintCategory.h" |
|
@implementation NSArray (PrettyPrintElements) |
- (NSString *)prettyPrintDescription { |
// implementation code here... |
} |
@end |
范疇有一些限制。您不能通過范疇為類添加新的實例變量。雖然范疇方法可以覆蓋現(xiàn)有的方法,但這并不是推薦的做法,特別是當(dāng)您希望對現(xiàn)有行為進行增強的時候。一個原因是范疇方法是類接口的一部分,因此無法通過向super發(fā)送消息來獲取類中已經(jīng)定義的行為。如果您需要改變一個類的現(xiàn)有方法的行為,更好的方法是生成一個該類的子類。
您可以通過范疇來為根類—NSObject—添加方法。通過這種方式添加的方法可以用于與該代碼相連接的所有實例和類對象。非正式的協(xié)議—Cocoa委托機制的基礎(chǔ)—在NSObject類中聲明為范疇。然而,這種在使用上的廣泛適用也有它的風(fēng)險。您通過范疇向NSObject添加的行為可能會有意料不到的結(jié)果,可能導(dǎo)致崩潰,數(shù)據(jù)損壞,甚至更壞的結(jié)果。
協(xié)議
Objective-C的另一個擴展稱為協(xié)議,它非常象Java中的接口。兩者都是通過一個簡單的方法聲明列表發(fā)布一個接口,任何類都可以選擇實現(xiàn)。協(xié)議中的方法通過其它類實例發(fā)送的消息來進行調(diào)用。
協(xié)議的主要價值和范疇一樣,在于它可以作為子類化的又一個選擇。它們帶來了C++多重繼承的一些優(yōu)點,使接口(如果不是實現(xiàn)的話)可以得到共享。協(xié)議是一個類在聲明接口的同時隱藏自身的一種方式。接口可以暴露一個類提供的所有(通常是這種情況)或部分服務(wù)。類層次中的其它類都可以通過實現(xiàn)協(xié)議中的方法來訪問協(xié)議發(fā)布的服務(wù),不一定和協(xié)議類有繼承關(guān)系(甚至不一定具有相同的根類)。通過協(xié)議,一個類即使對另一個類的身份(也就是類的類型)一無所知,也可以和它進行由協(xié)議定義的特定目的的交流。
有兩種類型的協(xié)議:正式和非正式協(xié)議。非正式協(xié)議在"范疇"部分中已經(jīng)簡單介紹了,它們是NSObject類中定義的范疇。因此每個以NSObject為根類的對象(和類對象)都隱式采納了范疇中發(fā)布的接口。和正式協(xié)議不同,一個類不必實現(xiàn)非正式協(xié)議中的每個方法,而是只實現(xiàn)它感興趣的方法就可以了。為了使非正式協(xié)議正確工作,聲明非正式協(xié)議的類在向某個目標對象發(fā)送協(xié)議消息之前,必須首先向它發(fā)送respondsToSelector: 消息并得到肯定的回答(如果目標對象沒有實現(xiàn)相應(yīng)的方法,則產(chǎn)生一個運行時例外)。
Cocoa中的“協(xié)議”通常指的是正式協(xié)議。它使一個類可以正式地聲明一個方法列表,作為向外提供服務(wù)的接口。Objective-C語言和運行系統(tǒng)支持正式協(xié)議;編譯器可以根據(jù)協(xié)議進行類型檢查,對象可以在運行時進行內(nèi)省,以確認是否遵循某個協(xié)議。正式協(xié)議有自己的專用術(shù)語和語法。術(shù)語方面,提供者和客戶的意義有所不同:
提供者(通常是一個類)聲明正式的協(xié)議。
客戶類采納正式協(xié)議,表示客戶類同意實現(xiàn)協(xié)議中所有的方法。
如果一個類采納某協(xié)議或者是從采納該協(xié)議的類派生出來的(協(xié)議可以被子類繼承),則可以說該類遵循該協(xié)議。
在Objective-C中,聲明和采納協(xié)議都有自己的語法。協(xié)議的聲明必須使用編譯導(dǎo)向符@protocol。下面的例子顯示了NSCoding協(xié)議(在Foundation框架的NSObject.h頭文件中)的聲明方式:
@protocol NSCoding |
- (void)encodeWithCoder:(NSCoder *)aCoder; |
- (id)initWithCoder:(NSCoder *)aDecoder; |
@end |
協(xié)議的聲明類不需要實現(xiàn)這些方法,但應(yīng)該對遵循該協(xié)議的對象方法進行調(diào)用。
如果一個類要采納某個協(xié)議,需要在在@interface導(dǎo)向符后、緊接著超類的位置上指定協(xié)議的名稱,并包含在尖括號中。一個類可以采納多個協(xié)議,不同的協(xié)議之間用逗號分隔。下面是Foundation框架中的NSData類采納三個協(xié)議的方式:
@interface NSData : NSObject <NSCopying, NSMutableCopying, NSCoding> |
通過采納這些協(xié)議,NSData許諾自己要實現(xiàn)協(xié)議中聲明的所有方法。范疇也可以采納協(xié)議,對協(xié)議的采納將成為類定義的一部分。
Objective-C通過類遵循的協(xié)議和類繼承的超類來定義類的類型。您可以通過發(fā)送conformsToProtocol:消息來檢查一個類是否遵循特定的協(xié)議:
if ([anObject conformsToProtocol:@protocol(NSCoding)]) { |
// do something appropriate |
} |
在類型聲明—方法、實例變量、或函數(shù)中,您可以將遵循的協(xié)議作為類型的一部分來指定。這樣您就可以通過編譯器來得到另一個級別的類型檢查,這種檢查比較抽象,因為它不和特定的實現(xiàn)相關(guān)聯(lián)。您可以使用與協(xié)議采納相同的語法規(guī)則,即把協(xié)議的名稱放在尖括號中,通過這種語法可以在類型中指定遵循的協(xié)議。您常常會看到在這些聲明中使用了動態(tài)對象類型id,例如:
- (void)draggingEnded:(id <NSDraggingInfo>)sender; |
這里,參數(shù)中引用的對象可以是任意類型的類,但是必須遵循NSDraggingInfo協(xié)議。
除了目前為止已經(jīng)提到的協(xié)議之外,Cocoa還提供了幾個協(xié)議的例子。一個有趣的例子就是NSObject協(xié)議。可以想象得到的是,NSObject類采納了這個協(xié)議,還有一個根類—NSProxy—也采納了這個協(xié)議。通過這個協(xié)議,NSProxy類可以和Objective-C運行環(huán)境的一部分進行交互,包括引用計數(shù)、內(nèi)省、和對象行為的其它基礎(chǔ)部分。
正式協(xié)議有其自己的限制。如果協(xié)議聲明的方法列表隨著時間而增長,協(xié)議的采納者就會不再遵循該協(xié)議。因此,Cocoa中的正式協(xié)議被用于穩(wěn)定的方法集合,比如NSCopying和NSCoding。如果您預(yù)期協(xié)議方法會增多,則可以聲明為非正式協(xié)議,而不是正式協(xié)議。#p#
使用Objective-C
在面向?qū)ο蟮某绦蛑?,完成工作的方式是通過消息,即一個對象向另一個對象發(fā)送消息。通過消息,發(fā)送對象可以向接收對象(接收者)發(fā)出請求,請求接收者執(zhí)行某些動作,返回某些對象或值,或者同時執(zhí)行兩者。
Objective-C在消息傳遞方法采用了獨特的語法形式。列表2-2的語句來自SimpleCocoaTool工程的代碼:
NSEnumerator *enm = [sorted_args objectEnumerator]; |
消息表達式位于賦值符號的右邊,包含在方括號中。消息表達式中最左邊的部分是接收者。它是一個變量,代表送出消息的對象。在這個例子中,接收者是sorted_args,即NSArray類的一個實例。緊接著接收者的是消息體,在這個例子中就是objectEnumerator(這里我們要專注的是消息語法,而不是深入探討這個SimpleCocoaTool中的消息或其它消息實際上做些什么)。objectEnumerator消息調(diào)用sorted_args對象中名為objectEnumerator的方法,該方法會返回一個對象的引用,并由賦值符號左邊的enm變量來保存。enm變量的類型被靜態(tài)地定義為NSEnumerator類的一個實例。您可以將這個語句圖解為:

消息通常有參變量,或者稱為參數(shù)。僅帶一個參數(shù)的消息在消息名稱后面附加一個冒號,并將參數(shù)直接放在冒號后:

和函數(shù)的參變量一樣,參數(shù)的類型必須和方法聲明中指定的類型相匹配。作為例子,請看如下SimpleCocoaTool工程中的表達式:
NSCountedSet *cset = [[NSCountedSet alloc] initWithArray:args]; |
這里args也是NSArray類的一個實例,它是initWithArray:消息的參數(shù)。
如果消息有多個參數(shù),則消息名稱就有多個部分,每個部分都以冒號結(jié)束,冒號后面是新的參數(shù):

上面引用的initWithArray:例子很有意思,它說明了嵌套的使用。在Objective-C中,您可以將一個消息嵌套到另一個消息內(nèi)部,將一個消息表達式返回的對象用作將它包圍在內(nèi)的另一個消息表達式的接收者。因此,為了解釋嵌套的消息表達式,可以從最里面的表達式開始,然后向外延伸。下面的語句可以解釋為:
將alloc消息發(fā)送給NSCountedSet類,以創(chuàng)建(通過為其分配內(nèi)存)一個未初始化的類實例。
請注意:Objective-C類自身也是對象,因此您也可以象它們的實例一樣,向它們發(fā)送消息。在消息表達式中,類消息的接收者總是一個類對象。
將initWithArray:消息發(fā)送給未初始化的類實例,以根據(jù)args數(shù)組對對象本身進行初始化,并返回一個自身的引用。
接下來考慮SimpleCocoaTool工程中main例程中的如下語句:
NSArray *sorted_args = [[cset allObjects] sortedArrayUsingSelector:@selector(compare:)]; |
這個消息表達式中值得注意的是sortedArrayUsingSelector:消息的參數(shù)。該參數(shù)要求使用編譯器導(dǎo)向符@selector來創(chuàng)建一個選擇器。選擇器是一個名稱,在Objective-C運行環(huán)境中用于唯一標識一個接收者的方法,這個名稱包含消息名的所有部分,包括冒號,但是不包括其它部分,比如返回類型或參數(shù)類型。
讓我們暫停一下,回顧一下消息和方法的專用術(shù)語。方法本質(zhì)上就是類定義和實現(xiàn)的函數(shù),消息接收者是該類的實例。消息是一個與參數(shù)結(jié)合在一起的選擇器,消息發(fā)送給接收者后導(dǎo)致對方法的調(diào)用。消息表達式同時包含接收者和消息。圖2-2對這些關(guān)系進行描述:
下圖為消息的專用術(shù)語

Objective-C使用了很多在ANSI C中找不到的類型和常量(literal)。在某些情況下,這些類型和常量會代替ANSI C的對應(yīng)部分。表2-1描述一些重要的類型,包括每個類型允許使用的常量。
下表Objective-C定義的重要類型和常量
類型 | 描述和文字 |
id | 動態(tài)對象類型,否定常量為nil。 |
Class | 動態(tài)類的類型,否定常量為Nil。 |
SEL | 選擇器的數(shù)據(jù)類型(typedef)。和ANSI C一樣,這種類型的否定常量為NULL。 |
BOOL | 布爾類型。允許的值為YES和NO。 |
在程序的控制流程語句中,您可以通過測試正確的否定常量來確定處理流程。舉例來說,下面的while語句來自SimpleCocoaTool工程的代碼,它隱式測試了word對象,以判斷返回對象是否存在(或者從另一個角度看,測試是否不等于nil):
while (word = [enm nextObject]) { |
printf("%s\n", [word UTF8String]); |
} |
在Objective-C中,您可能經(jīng)常向nil發(fā)送消息而沒有副作用。運行環(huán)境保證發(fā)給nil的消息的返回值和其它類型的返回值對象一樣是可以工作的。
SimpleCocoaTool代碼中***需要注意的是一些Objective-C的初學(xué)者不容易注意到的東西。請對比下面的語句:
NSEnumerator *enm = [sorted_args objectEnumerator]; |
和:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; |
表面上它們是做同樣的事情,兩者都返回一個對象的引用。然而,在返回對象的所有權(quán)以及由此引出的誰負責(zé)釋放該對象的問題上有一個重要的語義差別。在***個語句中,SimpleCocoaTool程序并不擁有返回對象;而在第二個語句中,程序創(chuàng)建了對象,并因此擁有了該對象,程序***需要做的是向已創(chuàng)建的對象發(fā)送release信息,從而釋放該對象。其它只有一個顯式創(chuàng)建的對象(NSCountedSet實例)也在程序的結(jié)束部分顯式釋放了。有關(guān)對象所有權(quán)和對象清理的策略,以及使用什么方法執(zhí)行這種策略的信息,請參見"Cocoa對象的生命周期"部分。