如果你剛開始接觸 iOS 或 Mac OS X 編程,首先要學習一點編程語言 Objective-C 入門知識。Objective-C 並不是一門復雜的語言,經過一段時間的接觸,你就能體會到它的優雅。Objective-C 語言實現了嚴格的面向對象編程。它擴展了標准 ANSI C 語言,增加了定義類和方法的語法。它還推行類和接口的動態擴展性,使任何類都能適配和采用。
如果你已經掌握了 ANSI C 語言,下面的內容能夠幫助你掌握 Objective-C 的基本語法。如果你有其他面向對象編程語言的基礎,你會發現 Objective-C 中含有大量傳統的面向對象概念,例如封裝、繼承、多態性等。反過來,如果你對 ANSI C 尚不熟悉,我們誠摯建議您在閱讀本文之前,先至少閱讀一篇關於 C 語言的介紹。
在《The Objective-C Programming Language》中完整講解了 Objective-C 語言。
關於 Objective-C
Objective-C 語言規定了一系列用來定義類和方法的語法,以及用來推行類和可適應接口的動態擴展的結構。
C 語言的超集
既然是 C 語言的超集,Objective-C 支持所有 C 語言的基本語法。你可以繼續按你的習慣書寫代碼,例如原始類型(int、float 等)、結構、函數、指針,還有過程控制語句例如 if…else 和 for 語句。你可以使用標准 C 語言庫,比如 stdlib.h 和 stdio.h 中聲明的內容。
Objective-C 在 ANSI C 的基礎上增加了如下內容:
- 定義新類的語法規約
- 類和實例方法的規約
- 調用方法的語法(稱為消息機制)
- 聲明屬性並從中合成存取方法的語法
- 靜態和動態類型的規約
- 塊對象(Block)- 封裝起來的代碼片段,可以在任何時候被執行
- 對基本語言的擴展,例如協議、范疇類等
如果現在還不明白這些 Objective-C 概念也不必擔心。當你繼續閱讀后面的內容時就會學習這些概念。如果你是初次接觸面向對象概念的開發者,可以先把對象想象成一個含有函數的結構。這個比喻不算太離譜,尤其對運行時的實現而言。
Objective-C 的優勢
Objective-C 不僅提供了其他面向對象編程語言中的抽象概念和運行機制,而且還是一種非常靈活的語言,這種動態性就是 Objective-C 的最大優勢所在。這種動態性可以讓應用在運行中(即“運行時”)判斷其該有的行為,而不是在編譯構建時就固定下來。因此,Objective-C 把應用程序從編譯時、連接時的限制中解放出來,並在用戶掌握控制權時,更多依賴於運行時的符號解析。Objective-C 的動態性來自三個方面:
- 動態類型 可以讓你的代碼在運行時判斷對象的類型。id 數據類型可以在運行時用任何數據類型來替換。所以,你可以讓運行時因素來決定代碼中用到的對象是什么類型。動態類型讓你的應用更加靈活,這是靜態類型做不到的,不過這會讓數據的嚴格統一性降低。
注意:靜態類性中的類都是固定種類的(比如 NSString *var),它有自身的優勢,實際上用處比動態類型更廣泛。打個比方,使用靜態類型,編譯器就可以完全分析你的代碼。這讓代碼性能和可預知性更高。在其他面向對象編程語言中,動態類型有時被成為弱類型,靜態類型被成為強類型。
- 動態綁定 讓你的代碼在運行時判斷需要調用什么方法,而不是編譯時。就像動態類型把對象的類型放到運行時再去判斷一樣,動態綁定把選擇調用哪種方法的任務放到了運行時去完成。和其他面向對象語言一樣,方法調用和代碼並沒有在編譯時就聯結在一起,只有在消息發出時,它們才真正聯結。 動態類型和動態綁定的存在使得選擇哪個接收者以及調用哪個方法都可以在運行時來決定。用一個畫圖程序來打比方,它能夠定義從父類繼承而來的圖形類應該怎樣歸類;你可以直接在某個對象上調用 draw 方法,無需知曉該對象的類以及它繪制自己的具體途徑。
- 動態載入 可以讓你的程序在運行時添加代碼模塊以及其他資源。有了動態載入特性,應用可以根據需要加載一系列可執行代碼以及資源,而不是在啟動時就加載所有組件。這能夠大大提高性能。可執行代碼中可以含有和程序運行時整合的新類。
類和對象
和其他大部分面向對象編程語言一樣,Objective-C 中的類也支持封裝數據,以及定義可以在該數據上執行的動作。對象是運行時類的一個實例。在類里聲明了的實例變量和方法,它的每個實例都在內存中擁有同樣的實例變量,以及指向那些方法的指針。創建一個對象時,你需要經過兩個步驟:內存分配(allocation)和初始化(initialization)。
Objective-C 中的類有自己的規范要求,必須包括兩個不同的部分:接口(interface)和實現(implementation)。接口部分含有類的聲明、實例變量和相關方法的聲明。既然是作為 C 語言,通過分別定義頭文件和源文件,你就可以將公有聲明和具體的實現代碼給分離開來。(你可以在實現文件里放一些聲明代碼,因為有些情況下,它們共同構成一個公用程序里的接口部分。)下表列出了這些文件的后綴以及區別:
后綴、源文件類型:
- .h – 頭文件。頭文件含有類、類型、函數和常量的聲明。
- .m – 源文件。這個后綴的源文件可以同時包含 Objective-C 和 C 語言的代碼。
- .mm – 源文件。這個后綴的源文件可以同時包含 C++、Objective-C 以及 C 語言的代碼。只有在你的 Objective-C 代碼中用到了 C++ 的類或者特性時才需要使用這個后綴。
如果需要在源代碼中包含頭文件,你需要使用 #import 命令,和 C 語言中的 #include 命令類似。兩者的區別在於,#import 能夠保證頭文件只被包含一次。
圖 1 中是一段的類聲明的語法展示,聲明了一個叫做 MyClass 的類,它繼承於基本類(或根類):NSObject。(根類可以被所有的其他類直接或間接繼承。)類聲明開頭是一條編譯器指令 @interface,結尾是一條 @end 指令。在類名稱后邊(中間用冒號分隔),是父類的名稱。在 Objective-C 中,每個類只能有一個父類。類中包含的實例變量(有時被稱為 ivar,在其他編程語言中有時被稱為成員變量)的聲明被一個花括號({ 和 })包裹起來。實例變量是可選的。在實例變量語句塊下邊是屬性(圖中沒有寫出來)和類的方法聲明。每個實例變量和方法聲明的語句結尾都要有一個分號。
圖 1 一段類聲明
類的實現的語法也是類似的。開頭是編譯器指令 @implementation(后面有類的名稱),結尾是 @end 指令。方法的實現代碼就在這兩個指令的中間。實現代碼中必須導入它的接口文件,寫在代碼的第一行。
#import “MyClass.h”
@implementation MyClass
- (id)initWithString:(NSString *)aName
{
// 在這里書寫代碼
}+ (MyClass *)myClassWithString:(NSString *)aName
{
// 在這里書寫代碼
}
@end
我們之前講過,Objective-C 支持包含對象的動態類型變量,它同時也支持靜態類性。靜態類型變量的聲明前邊要有所述類的名稱。而動態類型變量聲明以 id 來代表對象。在某些情形下,你會用到動態類型變量。比如,一個數組這樣的對象集合(里面包含的對象類型可能是無法預知的)就會用到動態類型變量。這樣的變量能夠提供無比靈活的功能,使得 Objective-C 程序能夠擁有更大的動態性。
下面的例子展示了靜態類型和動態類型變量的聲明方式:
MyClass *myObject1; // 靜態類性
id myObject2; // 動態類型
NSString *userName; // 曾出現在“你的第一個 iOS 應用”中(靜態類型)
請注意第一個聲明里的 * 星號。在 Objective-C 語言中,對象永遠是通過指針來引用的。如果現在你還不能明白這句話的意思也不必擔心,在學會Objective-C 基礎之后再研究指針也不遲。現在你需要記住的,是在靜態類型變量聲明時,變量名稱前面一定要有 * 星號。而 id 類型則暗示它是一個指針。
方法和消息
如果你剛剛開始接觸面向對象編程,不妨先把“方法”想象成每個對象特有的一個函數。通過向一個對象發送消息,你便調用了對象的某個方法。Objective-C 中有兩種方法:實例方法以及類方法。
- 實例方法顧名思義,它的作用域僅在某個類的一個實例當中。換句話說,在調用某個實例方法之前,你必須先創建一個實例才行。實例方法是最常見的方法。
- 類方法則是指其作用域包括該方法所在的整個類。它不要求某個對象的實例作為消息的接收者。
方法的聲明由以下幾個部分構成:方法類型標識符,返回類型,一個或多個方法簽名關鍵字,以及參數類型和名稱。下面的圖中是實例方法 insertObject:atIndex: 的聲明語句。
在實例方法中,聲明的開頭是一個 – 減號;而聲明類方法時前面要使用 + 加號。下文的“類方法”章節將詳細講述類方法的概念。
方法的名稱(insertObject:atIndex:)是一系列方法簽名關鍵詞聯結而成,包括冒號。冒號表示將會出現參數。在上面的例子中,這個方法含有兩個參數。如果某個方法沒有參數,則需要將第一個(也是唯一一個)方法簽名關鍵詞后面的冒號省略掉。
當你需要調用一個方法時,就是要向實現了該方法的對象發送一個消息,簡而言之,就是給對象發送消息。(雖然“發送消息”常常用作“調用方法”的近義詞,但是在 Objective-C 的運行時中,實際情況是發送消息。)一個消息就是一個方法的名字帶上該方法所需的參數信息(要和數據類型正確對應)。你向對象發送的所有消息都是動態調度的,以此來實現 Objective-C 語言的多態性。(多態性是指:不同類型的對象都能對同一種消息做出回應。)有時,所調用的方法是由接收消息的對象的類的父類實現的。
要調度一個消息,運行時要求正確的消息表達方式。消息表達式由一對方括號([ 和 ])把消息(以及所需的參數)包裹起來,接收消息的對象寫在左邊括號后邊。比如,要向 myArray 變量所含的對象發送一個 insertObject:atIndex: 消息,你要按下面的語法進行書寫:
[myArray insertObject:anObject atIndex:0];
為了避免聲明大量局部變量來存儲臨時結果,Objective-C 允許嵌套消息表達式。每個嵌套的表達式返回的值都會作為一個參數,或者接收消息的對象,甚至是另一個消息。比如,你可以將上一個例子中的任意一個變量替換成用消息接收數值。這樣一來,如果你還有一個叫做 myAppObject 的對象,它含有訪問數組對象以及將對象插入數組的方法,你可以將那個例子改造成下面這樣:
[[myAppObject theArray] insertObject:[myAppObject objectToInsert] atIndex:0];
Objective-C 還提供了點語法特性,用來訪問存取方法。存取方法是對象的 get 和 set 語句,這里是封裝的關鍵,封裝是所有對象的重要特性。對象把自己的狀態封裝(或隱藏)起來,並提供一個能讓所有實例訪問這個狀態的通用接口。利用點語法,之前的例子又可以被改寫成:
[myAppObject.theArray insertObject:myAppObject.objectToInsert atIndex:0];
點語法還可以用來賦值:
myAppObject.theArray = aNewArray;
這個語法其實是 [myAppObject setTheArray:aNewArray]; 這個語句的另一種表述方式。
而且,請回想一下,在“你的第一個 iOS 應用”教程里,你已經用過點語法來對變量進行賦值了:
self.userName = self.textField.text;
下文中的“已聲明的屬性和存取方法”章節將向你詳細介紹存取方法。
類方法
雖然之前的范例都是向類的實例發送消息,但你也可以向類自身發送消息。(這里的類,可以被理解為由運行時生成的 類 的對象。)向一個類發送消息時,該方法必須在之前被聲明為類方法,而不是實例方法。類方法和 C++ 中的靜態方法很相似。
你通常會將類方法用作工廠方法,借以創建該類的新的實例或者訪問與該類相關的某些共有信息。類方法的聲明語法和實例方法的十分相像,不同之處是方法類型標識符是一個 + 加號,而不是 – 減號。
下面的例子展示了如何把一個類方法當作該類的工廠方法進行調用。在本例中,array 方法是 NSArray 類的一個類方法,並且被 NSMutableArray 類繼承。它會給這個類的新實例分配內存並將這個實例初始化,最后把它返回給你的代碼。
NSMutableArray *myArray = nil; // nil 本質上等同於 NULL
// 創建一個新數組,並將其賦值給 myArray 變量。
myArray = [NSMutableArray array];
已聲明的屬性和存取方法
一個屬性,按通常的理解就是對象封裝的狀態里的一項。它要么是一個特性,比如名字或者顏色;要么是與另一個或多個其他對象的關聯。對象的類定義了一個接口,使用該對象的用戶可以獲取(get)和設置(set)封裝屬性中的數值。而執行這個功能的方法就叫做存取方法。
存取方法共有兩種,且都要符合一套命名規約。“Getter(取值器)”存取方法能夠返回某個屬性的值,它的方法名和該屬性同名。“Setter(賦值器)”存取方法能夠給某個屬性賦予新的值,它的命名規約是 set屬性名稱: 這樣的形式,屬性名稱的首字母大寫。在 Objective-C 框架中,只有嚴格按照規約對存取方法進行命名才能實現多種技術。
Objective-C 提供了已聲明的屬性,可以作為聲明的便利途徑,有時還能作為存取方法的實現。在“你的第一個 iOS 應用”教程中,你曾經聲明了 userName 屬性:
@property (nonatomic, copy) NSString *userName;
已聲明的屬性使得 getter 和 setter 方法無需在類里顯式聲明。相反,你在聲明屬性時可以決定其具體行為方式,然后要求編譯器基於屬性聲明,生成(或說創建)實際可用的 getter 和 setter 方法。已聲明的屬性減少了大量不必要的代碼,節省開發者的時間,並且讓你的代碼更加清爽、減少出錯的可能。
在類接口文件中,要包含屬性聲明和方法聲明。基本的聲明要使用 @property 編譯器指令,后面是屬性的類型和名稱。你還可以為屬性設定不同的選項,也就是說可以調整存取方法的具體行為方式,比如屬性是否為弱引用,或者是否為只讀屬性。這些選項寫在 @property 指令后邊的圓括號中。
下邊的幾行代碼展示了更多屬性聲明的例子:
@property BOOL flag; // 默認是只聲明類型和名稱
@property (copy) NSString *nameObject; // 在賦值過程中拷貝對象
@property (readonly) UIView *rootView; // 只聲明 getter 方法
在類的實現代碼中,你要使用 @synthesize 編譯器指令來要求編譯器根據聲明的情況,生成存取方法:
@synthesize flag;
@synthesize nameObject;
@synthesize rootView;
你也可以把 @synthesize 語句放到一行里面:
@synthesize flag, nameObject, rootView;
在 @synthesize 指令中,你還可以命令編譯器添加相應的實例變量到類定義中去。要增加一個實例變量,在屬性的名稱后面寫一個等號,然后寫你想要的實例變量名稱:
@synthesize nameObject=_nameObject;
塊對象
塊對象是封裝了一系列功能的一個對象,或者更通俗地講,它是一個代碼片段,能夠在任何時刻被執行。它們本質上就是可移植的匿名的函數,可以作為其他方法的參數傳入,也可以作為其他方法或函數的返回值。塊對象本身有含類型的參數表,可能帶有不確定的或已聲明的返回值類型。你可以把一個塊對象賦值給某個變量,然后在你需要的時候像調用函數一樣調用它就行了。
脫字符(^)是塊的語法標記。還有按照我們熟悉的參數語法規約所定義的返回值以及塊對象的主體(也就是可以執行的代碼)。下圖是如何把塊對象賦值給一個變量的語法講解:
接下來,按照調用函數的方式調用塊對象變量就可以了:
int result = myBlock(4); // 結果是 28
塊對象可以在局部作用域之內共享數據。塊對象的這個特性非常有用,假設你實現了一個方法,該方法定義了一個塊對象,那么這個塊對象就可以訪問該方法內的局部變量和參數(包括堆棧變量),也可以訪問函數和全局變量,甚至包括實例變量。這種訪問是只讀性質的,但如果變量聲明使用了 __block 修飾符,則它的值可以在塊對象中被改變。即使在方法或函數封裝的塊對象返回一個值並銷毀其作用域之后,只要對該塊對象的引用不消失,局部變量作為塊對象的一部分將一直存在。
和方法、函數的參數類似,塊對象可以被當作一個回調函數。當方法或函數被調用時,它們會執行某些功能,並在合適的時機回調之前調用它們的代碼(通過塊對象),來請求附加信息,或者從中獲取程序特定的行為。塊對象讓調用者能夠在調用的時候提供回調代碼。塊對象不會將請求數據打包到一個“上下文”結構中,而是從方法或函數的相同作用域中捕獲數據。由於塊對象代碼無需在單獨的方法或函數中另外實現,所以你的實現代碼能夠變得更加簡潔,可讀性更強。
Objective-C 框架有許多帶有塊對象參數的方法。比如這段,在 UIKit 框架里聲明了如下類方法,它有兩個塊對象參數:
+ (void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion;
這個方法可以讓你生成一個動畫效果。第一個塊對象參數用來選擇動畫效果;第二個塊對象參數指定動畫完成后要執行的任務。下面的例子中,第一個塊對象將視圖動畫的結尾 alpha 值設為 0(讓它變成透明的)。第二個塊對象則把整個視圖移除。
[UIView animateWithDuration:0.2 animations:^{
view.alpha = 0.0;} completion: ^(BOOL finished) {
if (finished == YES)
[view removeFromSuperview];
}];
協議和范疇類
協議可以用來聲明能夠在任何類中實現的方法,甚至那些實現該方法的類繼承自別的類。協議方法定義的行為是獨立於任何一個類的。協議可以定義一個要求其他類必須承諾實現的接口。也就是說,如果你的類實現了協議中的方法,那么這個類就承諾要完成該協議。
從實用的角度說來,協議定義了一系列方法,並建立起對象之間的“合約”。而這些對象不必是任何一個確定的類的實例。這個合約使得對象之間可以進行交流。某個對象想要告訴另一個對象,馬上將要面臨的事件,或者想要詢問關於那些事件的建議。
UIApplication 類實現了所需的應用行為。你不必為了接收簡單的應用當前狀態的消息而創建一個 UIApplication 的子類。UIApplication 類會調用指定的委托對象中的特定方法來傳遞那些消息。實現了 UIApplicationDelegate 協議方法的對象就能夠接收到那些消息了,並且能夠提供合適的反饋。
在承諾實現、或采用某個協議的接口代碼中,協議的名稱要寫在父類名稱后邊的一對尖括號里(<…>)。在“你的第一個 iOS 應用”教程里,你采用了 UITextFieldDelegate 協議:
@interface HelloWorldViewController : UIViewController <UITextFieldDelegate> {
}
@end
在實現中,你無需聲明協議方法。
協議的聲明看起來和類接口很相似,不過不同的是協議沒有父類,並且不含任何實例變量(但它們能夠聲明屬性)。下面的例子展示如何聲明只有一個方法的簡單協議:
@protocol MyProtocol
- (void)myProtocolMethod;
@end
對於許多委托協議而言,采用一個協議就等於是實現該協議中定義的方法。有些協議要求你明確聲明你會支持該協議,而有些協議則是既包含必須實現的方法,也包含可選方法。
當你在 Objective-C 框架中瀏覽頭文件時,你很快就會看到類似這樣的語句:
@interface NSDate (NSDateCreation)
這行語句聲明了一個范疇類(category),其語法是將范疇類的名稱包裹在一對圓括號中。范疇類是 Objective-C 語言的一個特性,讓你能夠擴展某個類的接口,而無需創建它的子類。范疇類中的方法將成為此類的一部分(在你的程序作用域范圍內),並會被此類的所有子類繼承。你可以向此類(或它的子類)的任意一個實例發送消息來調用范疇類中聲明的方法。
你可以利用范疇類在一個頭文件里組織互相關聯的方法聲明。你甚至可以在不同的頭文件中放入不同的范疇類聲明。Cocoa Touch 框架和 Cocoa 框架在幾乎所有頭文件中都利用了這個技術,代碼才如此明晰。你還能使用匿名范疇類(也就是在圓括號中不寫任何字符),這樣可以把實例變量隱藏在私有的實現文件里。
預定義類型和編碼策略
在 Objective-C 語言中有一些特定的詞匯,你要避免在聲明變量時使用這些詞匯,因為它們都有專門的用途。其中有一些是編譯器指令,以 @ 符號開頭(例如 @interface 和 @end)。有些保留詞匯是預定義類型,以及和這些類型有關的文字。Objective-C 使用一系列不屬於 ANSI C 的預定義類型和詞匯。在某些情況下,這些類型和詞匯會代替它們在 ANSI C 中的對應者。下表列出了幾個非常重要的類型,包括每個類型所允許的詞匯。
類型、描述和詞匯:
id – 動態對象類型。動態類型和靜態類型對象的否定詞匯為 nil。
Class – 動態類的類型。它的否定詞匯為 Nil。
SEL – 選擇器的數據類型(typedef);這種數據類型代表運行時的一種簽名方法。它的否定詞匯為 NULL。
BOOL – 布爾型。代表它的值的詞匯為 YES 和 NO。
你通常會在代碼排錯以及流程控制中使用這些預定義的類型和詞匯。在程序的流程控制語句中,你可以通過檢測特定詞匯來判斷如何采取下一步動作。例如:
NSDate *dateOfHire = [employee dateOfHire];
if (dateOfHire != nil) {
// 處理此種情況
}
把代碼解釋一下:如果對象代表的聘用日期不為 nil,也就是說是一個合法的對象,那么邏輯就朝一個特定的方向發展。下邊是這段代碼的簡化版,效果是相同的:
NSDate *dateOfHire = [employee dateOfHire];
if (dateOfHire) {
// 處理此種情況
}
你甚至可以把代碼簡化成這個樣子(前提是你不需要引用 dateOfHire 對象):
if ([employee dateOfHire]) {
// 處理此種情況
}
處理布爾值的方法是一樣的。在這個例子中,isEqual: 方法會返回一個布爾型的值:
BOOL equal = [objectA isEqual:objectB];
if (equal == YES) {
// 處理此種情況
}
你同樣可以按照前邊省略 nil 的方式來簡化這段代碼。
在 Objective-C 語言里,你可以對 nil 發送消息,不必擔心任何副作用。其實,是根本不會有任何作用,只不過運行時會返回 nil,如果方法本來要返回一個對象的話。發送給 nil 的消息返回的值只要是一個對象,代碼就能繼續正常工作。
另外兩個重要的預留詞匯是 self 和 super。前者 self 是個局部變量,你可以在消息實現中把它看做當前對象進行引用;它和 C++ 語言中的 this 是一樣的。你可以用預留詞匯 super 替換 self,但只能作為消息表達式的接收者。如果你對 self 發送了消息,那么運行時首先就會在這個對象的類中尋找相應的方法實現;如果這里找不到,那么它會轉而到其父類中去查找(如此往復)。如果你對 super 發送消息,運行時首先就是去父類中尋找方法的實現。
利用 self 和 super 主要是為了發送消息。當被調用的方法在 self 的類中被實現之后,你就可以向 self 發送消息,例如:
[self doSomeWork];
self 還可以用在點語法中,用來調用由已聲明的屬性生成的存取方法,例如:
NSString *theName = self.name;
你常常會在重載(既重新實現已有的方法)從父類繼承而來的方法時向 super 發送消息。在這種情況下,被調用的方法擁有和重載后方法的簽名相同。