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

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

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

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

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

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