如果您未曾開發過 iOS 或 Mac OS X 平臺的程序,那就需要開始瞭解它們的首要程序設計語言 Objective-C。Objective-C 並不是一種很難的語言,如果能花一點時間學習,相信您會漸漸領會到它的優雅之處。Objective-C 程序設計語言使您能進行復雜的、面向對象的編程。通過提供用於定義類和方法的語法,它擴展了標準的 ANSI C 程序設計語言。它還促進類和接口(任何類可採用)的動態擴展。
如果您熟悉 ANSI C,那麼下述信息應該能幫助您學習 Objective-C 的基本語法。如果您使用其他面向對象程序設計語言進行過編程,您會發現許多傳統的面向對象概念,例如封裝、繼承、多態,都出現在 Objective-C 中。如果您不熟悉 ANSI C,在嘗試閱讀此文章時,最好先閱讀一下 C 語言的概述。
Objective-C 語言在《The Objective-C Programming Language》(Objective-C 程序設計語言)中有完整說明。
Objective-C 是 C 語言的超集
Objective-C 程序設計語言採用特定的語法,來定義類和方法、調用對象的方法、動態地擴展類,以及創建編程接口,來解決具體問題。Objective-C 作爲 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 中某個類的規格需要兩個不同的部分:接口和實現。接口部分包含類聲明,並定義該類的公共接口。如同 C 代碼那樣,您定義頭文件和源代碼文件,將公共聲明與代碼的實現細節分開。(如果其他聲明是編程接口的一部分,並且打算專有,您可以將它們放在實現文件中。)這些文件的文件擴展名,列在下表中。
擴展名 |
源類型 |
---|---|
|
頭文件。頭文件包含類、類型、函數和常量聲明。 |
|
實現文件。具有此擴展名的文件可以同時包含 Objective-C 代碼和 C 代碼。有時也稱爲源文件。 |
|
實現文件。具有此擴展名的實現文件,除了包含 Objective-C 代碼和 C 代碼以外,還可以包含 C++ 代碼。僅當您實際引用您的 Objective-C 代碼中的 C++ 類或功能時,才使用此擴展名。 |
當您想要在源代碼中包括頭文件時,請在頭文件或源文件的前幾行之中,指定一個導入 (#import
) 指令,#import
指令類似於
C 的#include
指令,不過前者確保同一文件只被包括一次。如果您要導入框架的大部分或所有頭文件,請導入框架的包羅頭文件 (umbrella header file),它具有與框架相同的名稱。導入(假設的)Gizmo 框架的頭文件的語法是:
#import <Gizmo/Gizmo.h> |
下列框圖中的語法聲明名爲 MyClass
的類,它是從基礎類(或根類)NSObject
繼承而來的。(根類是供其他類直接或間接繼承的類。)類聲明以編譯器指令 @interface
開始,以 @end
指令結束。類名稱後面(以冒號分隔),是父類的名稱。在
Objective-C 中,一個類只能有一個父類。
在 @interface
指令和 @end
指令之間,編寫屬性和方法的聲明。這些聲明組成了類的公共接口。(已聲明的屬性在“已聲明的屬性和存取方法”中有介紹。)分號標記每個屬性和方法聲明的結尾。如果類具有與其公共接口相關的自定函數、常量或數據類型,請將它們的聲明放在 @interface
...@end
塊之外。
類實現的語法與類接口文件類似。它以 @implementation
編譯器指令開始(接着是該類的名稱),以 @end
指令結束。中間是方法實現。(函數實現應在 @implementation
...@end
塊之外。)一個實現應該總是將導入它的接口文件作爲代碼的第一行。
#import "MyClass.h" |
|
@implementation MyClass |
- (id)initWithString:(NSString *)aName |
{ |
// code goes here |
} |
|
+ (MyClass *)myClassWithString:(NSString *)aName |
{ |
// code goes here |
} |
@end |
對於包含對象的變量,Objective-C 既支持動態類型化,也支持靜態類型化。靜態類型化的變量,要在變量類型聲明中包括類名稱。動態類型化的變量,則要給對象使用類型 id
。您會發現在某些情況下,會需要使用動態類型化的變量。例如,集
(collection) 對象,如數組,在它包含對象的類型未知的情況下,可能會使用動態類型化的變量。此類變量提供了極大的靈活性,也讓 Objective-C 程序擁有了更強大的活力。
下面的例子,展示了靜態類型化和動態類型化的變量聲明:
MyClass *myObject1; // Static typing |
id myObject2; // Dynamic typing |
NSString *userName; // From Your First iOS App (static typing) |
請注意第一個聲明中的星號 (*
)。在 Objective-C 中,執行對象引用的只能是指針。如果您還不能完全理解這個要求,不用擔心。並非一定要成爲指針專家才能開始 Objective-C 編程。只需要記住,在靜態類型化的對象的聲明中,變量的名稱前面應放置一個星號。id
類型意味着一個指針。
方法和發消息
如果您不熟悉面向對象編程,則將方法想像成一個規範特定對象的函數,可能會有所幫助。通過將一則消息發送到——或發消息給——一個對象,您可調用該對象的一個方法。Objective-C 中有兩種類型的方法:實例方法和類方法。
-
實例方法,由類的實例來執行。換言之,在調用實例方法之前,必須先創建該類的實例。實例方法是最常見的方法類型。
-
類方法,可由它所在的類直接執行。它不需要對象的實例作爲消息的接收者。
方法聲明包含方法類型標識符、返回類型、一個或多個簽名關鍵詞,以及參數類型和名稱信息。以下是 insertObject:atIndex:
實例方法的聲明。
對於實例方法,聲明前面是減號 (-
);對於類方法,對應指示器是加號 (+
)。下面的“類方法”將詳細介紹類方法。
一個方法的實際名稱 (insertObject:atIndex:
) 是包括冒號字符在內的所有簽名關鍵詞的串聯。冒號字符表明有參數存在。在上述示例中,該方法採用兩個參數。如果方法沒有參數,則省略第一個(也是僅有的一個)簽名關鍵詞後面的冒號。
當您想要調用一個方法時,通過給實施該方法的對象發送一則消息來實現。(雖然“發送消息”常可與“調用方法”互換,但實際上,Objective-C 在運行時纔會執行實際地發送。)消息包含方法名稱,以及方法所需的參數信息(類型要匹配)。您發送到一個對象的所有消息,都被動態地分派,這樣使 Objective-C 類的多態行爲更加容易。(多態性是指不同類型的對象響應同一消息的能力。)有時被調用的方法,是由接收消息對象的類之超類來實現。
要分派消息,運行時需要一個消息表達式。消息表達式使用方括號([
和 ]
)將消息本身(以及任何所需參數)括起來,同時將接收消息的對象放在最左邊方括號右側。例如,要將 insertObject:atIndex:
消息發送給 myArray
變量保存的對象,您會使用以下語法:
[myArray insertObject:anObject atIndex:0]; |
爲避免聲明大量局部變量來儲存臨時結果,Objective-C 讓您嵌套消息表達式。每個嵌套表達式的返回值,都用作另一個消息的一個參數或接收對象。例如,可以將上個示例中使用的任何變量替換爲取回值的消息。因此,如果您具有另一個名爲 myAppObject
的對象,並且此對象具有訪問數組對象的方法以及要插入數組的對象,則可以將上個示例編寫爲如下形式:
[[myAppObject theArray] insertObject:[myAppObject objectToInsert] atIndex:0]; |
Objective-C 還提供用於調用存取方法的點記法語法。存取方法獲取並設定對象的狀態,因此對於封裝很重要,是所有對象的重要功能。對象隱藏或封裝其狀態,並顯示接口,該接口是訪問該狀態的所有實例都通用的。使用點記法語法,您可以將上個示例重新編寫爲如下形式:
[myAppObject.theArray insertObject:myAppObject.objectToInsert atIndex:0]; |
您還可以使用點記法語法進行賦值:
myAppObject.theArray = aNewArray; |
此語法只是編寫 [myAppObject setTheArray:aNewArray];
的另一種方式。在點記法表達式中,您不能使用對動態類型化的對象(類型爲 id
的對象)的引用。
在“您的首個 iOS 應用程序”中,您已經使用點語法來進行變量賦值:
self.userName = self.textField.text; |
類方法
儘管前幾個示例將消息發送到了類的實例,但您也可以將消息發送到類本身。(類是運行時創建的、類型爲 Class
的對象。)向類發送消息時,您指定的方法必須定義爲類方法,而非實例方法。類方法是一種功能,類似於 C++
中的靜態類方法。
您通常這樣使用類方法:要麼將類方法用作工廠方法創建類的新實例,要麼訪問與該類關聯的一些共享信息。類方法聲明的語法,與實例方法聲明的語法相同,只是方法類型標識符使用加號 (+
),而非減號。
以下示例說明如何將類方法用作類的工廠方法。在這種情況下,array
方法是 NSArray
類上的類方法——被 NSMutableArray
繼承——它分配並初始化類的新實例,並將其返回給您的代碼。
NSMutableArray *myArray = nil; // nil is essentially the same as NULL |
|
// Create a new array and assign it to the myArray variable. |
myArray = [NSMutableArray array]; |
已聲明的屬性和存取方法
屬性通常是指某些由對象封裝或儲存的數據。它可以是標誌(如名稱或顏色),也可以是與一個或多個其他對象的關係。一個對象的類定義一個接口,該接口使其對象的用戶能獲取並設定所封裝屬性的值。執行這些操作的方法,稱爲存取方法。
存取方法有兩種類型,每個方法都必須符合命名約定。“getter”存取方法返回屬性的值,且名稱與屬性相同。“setter”存取方法設定屬性的新值,且形式爲 set
PropertyName:
,其中屬性名稱的第一個字母大寫。正確命名的存取方法是
Cocoa 和 Cocoa Touch 框架的多種技術的關鍵元素,如鍵-值編碼 (KVC),它的機制是,通過對象的名稱間接訪問對象的屬性。
Objective-C 提供已聲明的屬性作爲一種方便的寫法,用於存取方法的聲明和實現。在“您的首個 iOS 應用程序”中,您聲明瞭userName
屬性:
@property (nonatomic, copy) NSString *userName; |
使用已聲明的屬性後,就不必爲該類中用到的每個屬性實現 getter 和 setter 方法。而是使用屬性聲明,指定您想要的行爲。編譯器接着可以根據該聲明,創建或合成實際的 getter 和 setter 方法。已聲明的屬性減少了您必須編寫的樣板文件代碼量,因此使代碼更簡潔、更少機會出錯。使用已聲明的屬性或存取方法,來獲取和設定各項對象狀態。
您在類接口中包括方法聲明和屬性聲明。您在類的頭文件中聲明公共屬性;而在源文件的類擴展中聲明專有屬性。(有關類擴展的簡短說明及其示例,請參閱“協議和類別”。)控制器對象(如委託和視圖控制器)的屬性通常應該爲專有的。
屬性的基本聲明使用 @property
編譯器指令,後面緊跟屬性的類型信息和名稱。您還可以使用自定選項來配置屬性,以定義存取方法如何表現、屬性是否爲弱引用,以及是否爲只讀。選項位於圓括號中,前面是 @property
指令。
以下代碼行說明了更多的屬性聲明:
@property (copy) MyModelObject *theObject; // Copy the object during assignment. |
@property (readonly) NSView *rootView; // Declare only a getter method. |
@property (weak) id delegate; // Declare delegate as a weak reference |
編譯器自動合成所聲明的屬性。在合成屬性時,它創建自己的存取方法,以及“支持”該屬性的專有實例變量。實例變量的名稱與屬性的名稱相同,但具有下劃線前綴 (_
)。只有在對象初始化和取消分配的方法中,您的應用程序應該直接訪問實例變量(而不是其屬性)。
如果您想要讓實例變量採用不同名稱,可以繞過自動合成,並明確地合成屬性。在類實現中使用 @synthesize
編譯器指令,讓編譯器產生存取方法,以及進行特殊命名的實例變量。例如:
@synthesize enabled = _isEnabled; |
同時,在聲明屬性時,您可以指定存取方法的自定名稱,通常是使 Boolean 屬性的 getter 方法遵循約定形式,如下所示:
@property (assign, getter=isEnabled) BOOL enabled; // Assign new value, change name of getter method |
塊
塊是封裝工作單元的對象,即可在任何時間執行的代碼段。它們在本質上是可移植的匿名函數,可作爲方法和函數的參數傳入,或可從方法和函數中返回。塊本身具有一個已類型化的參數列表,且可能具有推斷或聲明的返回類型。您還可以將塊賦值給變量,然後像調用函數一樣調用它。
插入符號 (^) 是用作塊的語法標記。塊的參數、返回值和正文(即執行的代碼)存在其他類似的語法約定。下圖解釋了該語法,尤其是在將塊賦值給變量時。
您接着可以調用塊變量,就像它是一個函數一樣:
int result = myBlock(4); // result is 28 |
塊共享局部詞法作用範圍內的數據。塊的這項特徵非常有用,因爲如果您實現一個方法,並且該方法定義一個塊,則該塊可以訪問該方法的局部變量和參數(包括堆棧變量),以及函數和全局變量(包括實例變量)。這種訪問是隻讀的,但如果使用 __block
修飾符聲明變量,則可在塊內更改其值。即使包含有塊的方法或函數已返回,並且其局部作用範圍已銷燬,但是隻要存在對該塊的引用,局部變量仍作爲塊對象的一部分繼續存在。
作爲方法或函數參數時,塊可用作回調。被調用時,方法或函數執行部分工作,並在適當時刻,通過塊回調正在調用的代碼,以從中請求附加信息,或獲取程序特定行爲。塊使調用方在調用時能夠提供回調代碼。塊從相同的詞法作用範圍內採集數據(就像宿主方法或函數所做的那樣),而非將所需數據打包在“關聯”結構中。由於塊代碼無需在單獨的方法或函數中實現,您的實施代碼會更簡單且更容易理解。
Objective-C 框架具有許多含塊參數的方法。例如,Foundation 框架的 NSNotificationCenter
類聲明以下方法,該方法具有一個塊參數:
- (id)addObserverForName:(NSString *)name object:(id)obj queue:(NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block |
此方法將一個觀察者添加到通知中心(通知在“採用設計模式使您的應用程序合理化”中有介紹)。指定名稱的一則通知發佈時,塊被調用以處理該通知。
opQ = [[NSOperationQueue alloc] init]; |
[[NSNotificationCenter defaultCenter] addObserverForName:@"CustomOperationCompleted" |
object:nil queue:opQ |
usingBlock:^(NSNotification *notif) { |
// handle the notification |
}]; |
協議和類別
協議可聲明由任何類實施的方法,即使實施該協議的那些類沒有共同的超類。協議方法定義了獨立於任何特定類的行爲。協議簡單定義了一個由其他類負責實現的接口。當您的類實施了一個協議的方法時,就是說您的類符合該協議。
從實踐角度而言,一個協議定義了一列方法,這些方法在對象之間建立合約,而無需那些對象成爲任何特定類的實例。此合約使那些對象能夠相互通信。一個對象想要告知另一個對象它遇到的事件,或者它可能想要尋求有關那些事件的建議。
UIApplication
類實現一個應用程序必需的行爲。UIApplication
類不是強制您對 UIApplication
進行子類化,來接收有關應用程序當前狀態的簡單通知,而是通過調用指定給它的委託對象的特定方法,爲您傳送那些通知。實施 UIApplicationDelegate
協議的對象,可以接收那些通知,並提供適當的響應。
在接口塊中,您指定您的類符合或採用一個協議,方法是將該協議的名稱,放在尖括號 (<...>
) 中,並且放在它繼承的類的名稱後面。在“您的首個
iOS 應用程序”中,您指明瞭採用 UITextFieldDelegate
協議,代碼行如下:
@interface HelloWorldViewController :UIViewController <UITextFieldDelegate> { |
您無需聲明所實現的協議方法。
協議的聲明,看起來類似於類接口的聲明,只是協議沒有父類,並且不定義實例變量(儘管它們可以聲明屬性)。以下示例展示使用一個方法進行一個簡單的協議聲明:
@protocol MyProtocol |
- (void)myProtocolMethod; |
@end |
對於許多委託協議來說,採用一個協議僅僅是實現該協議定義的方法。有些協議要求您明確地聲明支持該協議,而協議可以指定必需方法和可選方法。
當您開始探索 Objective-C 框架的頭文件時,將很快遇到如下的代碼行:
@interface NSDate (NSDateCreation) |
這行代碼通過使用圓括號將類別名稱括起來的語法約定,聲明瞭該類別。類別是 Objective-C 語言的一項功能,可讓您擴展類的接口,而無需對類進行子類化。類別中的方法成爲類類型的一部分(在程序的作用範圍內),而這些方法由類的所有子類繼承。您可以將消息發送給類(或其子類)的任何實例,以調用在類別中定義的方法。
您可以將類別用作一種手段,來對頭文件內的相關方法聲明進行分組。您甚至還可以將不同的類別聲明放在不同的頭文件中。框架在其所有頭文件中使用這些技巧,來達到清晰明確。
您還可以使用稱爲類擴展的匿名類別,在實現 (.m
) 文件中聲明專有屬性和專有方法。類擴展看起來類似於類別,只是圓括號之間沒有文本。例如,以下是一個典型的類擴展:
@interface MyAppDelegate () |
@property (strong) MyDataObject *data; |
@end |
已定義的類型和編碼策略
Objective-C 有幾個不能用作變量名稱的術語,保留用於特殊用途。其中部分術語是以 @
符號爲前綴的編譯器指令,如 @interface
和@end
。其他保留的術語,包括已定義的類型以及與這些類型相配的字面常量。Objective-C
使用很多已定義的類型和字面常量,這些卻不會出現在 ANSI C 中。在某些情況下,這些類型和字面常量替換 ANSI C 相應的類型和字面常量。下表介紹幾種重要類型,包括每種類型的允許字面常量。
類型 |
描述和字面常量 |
---|---|
|
動態對象類型。動態類型化的對象和靜態類型化的對象的否定字面常量,都是 |
|
動態類類型。其否定字面常量是 |
|
選擇器的數據類型 ( |
|
Boolean 類型。字面常量值是 |
在錯誤檢查和控制流代碼中,通常使用已定義類型和字面常量。在程序的控制流語句中,您可以測試合適的字面常量,來確定如何繼續。例如:
NSDate *dateOfHire = [employee dateOfHire]; |
if (dateOfHire != nil) { |
// handle this case |
} |
解釋一下此代碼,如果表示僱用日期的對象不是 nil
(換言之,如果它是一個有效對象),則邏輯在某個方向繼續。以下是執行相同分支的簡寫形式:
NSDate *dateOfHire = [employee dateOfHire]; |
if (dateOfHire) { |
// handle this case |
} |
您甚至可以進一步減少代碼行數(假設無需引用 dateOfHire
對象):
if ([employee dateOfHire]) { |
// handle this case |
} |
您可採用大體相同方式,來處理 Boolean 值。在下面的示例中,isEqual:
方法返回一個 Boolean 值。
BOOL equal = [objectA isEqual:objectB]; |
if (equal == YES) { |
// handle this case |
} |
您可以採用與測試是否存在 nil
的代碼相同的方式,來縮短此代碼。
在 Objective-C 中,您可以將消息發送到 nil
,而沒有副作用。事實上,完全沒有影響,只是如果方法應該返回一個對象,運行時就會返回 nil
。只要返回的內容類型化爲一個對象,即可保證發送給 nil
的消息的返回值正常運行。
Objective-C 中其他兩個重要的保留術語,是 self
和 super
。第一個術語 self
是可在消息實現內使用的局部變量,用於引用當前對象;它等同於
C++ 中的 this
。您可以用保留字 super
替換 self
,但在消息表達式中,只能作爲接收者。如果您將消息發送到self
,運行時先在當前對象的類中查找方法實現;如果在那裏找不到方法,則在其超類中查找(依此類推)。如果您將消息發送到super
,運行時先在超類中查找方法實現。
self
和 super
的主要用途,都是發送消息。當要調用的方法是由 self
的類實現時,您將消息發送到 self
。例如:
[self doSomeWork]; |
self
還用於點記法,用於調用由已聲明屬性合成的存取方法。例如:
NSString *theName = self.name; |
在繼承自超類的方法的覆蓋(即重新實現)中,通常將消息發送到 super
。在這種情況下,被調用方法與被覆蓋方法的簽名,都是相同的。
© 2013 Apple Inc. 保留一切權利。(上次更新:2013 年 4 月 23 日)