Objective-C——關於Objective-C

蘋果官方文檔翻譯 《Objective-C語言編程》(Programming with Objective-C)

https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Introduction/Introduction.html#//apple_ref/doc/uid/TP40011210

Objective-C
是你編寫OS X以及iOS軟件所使用的主要語言。它是對C語言的極大擴展,爲其添加了面向對象編程的能力以及一套動態運行時的機制。Objective-C
繼承了C語言的語法,基礎數據類型以及流程控制語句,並且在這些基礎之上添加了定義類和類方法的語法。Objective-C還在語言層面上支持對象結構管理和對象字面解釋,使得用戶能夠動態輸入和綁定代碼,使得諸多功能在運行時生效。

概覽

本文檔對Objective-C語言進行了介紹,並針對其功用給出了詳盡的示例。你將學習如何通過描述對象去自定義類,同時還將瞭解如何利用Cocoa和Cocoa Touch框架提供的類。儘管框架內定義的類並沒有內嵌在Objective-C中,但想要使用Objective-C編程,離不開這些類。而且,諸多語言層面的功能都需要依賴這些由框架提供的類去執行。

一個應用就是一組對象的有機結合

當你爲OS X或iOS編寫應用時,大部分時間都將用了同各種對象打交道。這些對象都是Objective-C中類的實例對象,一部分屬於框架內定義的類,剩下的則是你自己定義的類。

如果你要創造自定義類,應該從編寫描述該類的接口文件開始,其應該顯示使用該類須知的相關信息。這個接口文件應由若干用來封裝數據的屬性,以及一個方法列表組成。方法的聲明表明了對象所能接收的消息,以及發送消息時應該傳入的屬性的信息。同時需要編寫的還有實現文件,其包含所有被聲明的方法的具體實現代碼。

相關章節:類的定義,使用對象,數據封裝

範疇是對現有類的拓展

如果想爲現有類添加一些自定義的功能,不必再去創建一個新類,只需爲其定義一個範疇,就可以做到這一點。利用範疇,你可以爲任意一個現有的類添加自定義方法,即使你不知道這些類原本的實現代碼,例如由框架提供的NSString類。

如果你知道一個類的源代碼,你大可以利用類擴展來添加新的屬性以及修改現有屬性的特徵。類擴展一般用來隱藏私有屬性或方法,可以是在一個源代碼文件中,也可以在一個自定義框架中。

相關章節:修改現有類

協議定義了消息發送的規則

一個基於Objective-C編寫的應用中的主要工作機制是各種對象之間互相發送消息。通常情況下,對象可以接收的消息都通過在接口文件中聲明的方法來表示。但有時,你可以定義一組相關的方法,不綁定任何特定的類,這種方式有其特殊的作用。

Objective-C利用協議來定義一組相關的方法,例如對象可以藉由這些方法來調用其代理,這些方法可以是必須實現的,也可以是可選的。任何類都可以明確聲明其遵守相關協議,這意味着其必須對協議中的所有必需實現的提供實現代碼。

相關章節:使用協議

數據值和集合通常利用Objective-C對象來表示

在Objective-C中,通常用Cocoa或Cocoa Touch框架內所提供的類來表示數據值。NSString類用了表示字符串,NSNumber類用來表示不同類型的數字,例如整數或浮點數,NSValue類用來表示其他類型的數值,如C結構體。你也可以使用C語言提供的各種基本數據類型,如int,float,char。

集合一般通過集合類的實例對象來表示,諸如NSArray,NSSet,或NSDictionary,都可以用來容納其他Objective-C對象。

相關章節:數據值和集合

代碼塊可以簡化編碼任務

代碼塊是C,Objective-C以及C++中所引入的語言特性。它可以用來表示執行特定任務的一組代碼。其是對一組代碼的封裝,這同其他語言中的closure很像。代碼塊通常被用來簡化編碼任務,例如數據枚舉,歸類和測試。代碼塊也使併發和異步執行變的簡單,方便的使用例如CGD等技術。

相關章節:使用代碼塊

錯誤對象用來表示運行時故障

雖然Objective-C包含了處理異常的語法,但Cocoa 和 Cocoa Touch只在編程錯誤發生時使用異常(例如超過了數據檢索的範圍),這些異常需要在應用交貨前解決。

其他類型的錯誤,包括運行時故障,例如磁盤空間不夠,無法連接網絡等——會通過NSError對象來表示。你的應用應該對這些錯誤有所準備,並提供相應的解決方案,以便在這些錯誤出現時提供更好的用戶體驗。

相關章節:錯誤更正

Objective-C編碼有據可循

當編寫Objective-C代碼時,你應遵循一定的規則。以方法名爲例,首字母小寫,包含多個單詞時應使用駝峯規則編寫,例如doSomething以及doSomethingElse。不只要注意大小寫,可讀性也非常重要:你的代碼應當儘量具備良好的可讀性,即方法名應當恰如其分的表達其含義,但又不能太羅嗦。

除此之外,還有一些規則是你在使用框架是應遵守的。以屬性的存取方法爲例,爲了使用KVC或KVO技術,你必需嚴格遵循屬性的命名規則。

相關章節:編碼規則

閱讀前提

如果你剛剛接觸OS X或iOS開發,在閱讀文檔之前,你應該通讀Start Developing iOS Apps Today或Start Developing Mac Apps Today,以便對應用開發有一個初步的瞭解。此外,你對xcode
也應該具備一定的瞭解,因爲本文檔大部分章節的末尾都附有相關練習,需要使用xcode完成。xcode是用來創建iOS和OS X應用的開發環境。你講使用它編寫代碼,設計應用的交互界面,測試應用,以及排除bug。

雖然讀者最好具備一些C語言或基於C語言的其他編程語言的基礎,如Java或C#,本文檔還是包含了C語言基礎特性的例子,如流程控制語句。如果讀者擁有其他高級語言的知識,如Ruby或Python,那麼就可以順利閱讀。

本文檔將涉及到面向對象編程的相關概念,特別是適用於Objective-C語言的。本文檔假定讀者具備基本的面向對象編程的概念,如果你對這些概念還不夠熟悉,請閱讀相關章節:Concepts in Objective-C Programming。

相關拓展

本文檔的內容適用於xcode4.4以以上,並假定應用的目標系統是OS X10.7以上或iOS 5以上。跟多關於xcode的信息,參見Xcode Overview。跟多語言版本方面的信息,參加Objective-C Feature Availability Index.

Objective-C應用利用引用計數來管理對象的聲明週期。大多數情況下,編譯器的ARC特性將爲你解決這一問題。如果你無法使用ARC,或者需要轉換或維護舊的代碼,這些代碼需要使用手動引用計數,請參閱Advanced Memory Management Programming Guide。

除了編譯器,Objective-C利用運行時系統來使用它的動態和麪相對象的特性。雖然一般讀者無需擔心Objective-C是怎樣工作的,但還是有可能接觸到運行時系統,請參閱Objective-C Runtime Programming和Objective-C Runtime Reference。


類的定義(2015.6.23)

當你爲OS X / iOS 編寫程序時,大部分時間都將與對象打交道。Objective-C中的對象同其他面相對象的編程語言一樣:它們將自身所代表的數據同相關的行爲整合起來。

一個應用是由諸多互相通訊的對象結合起來的有機整體,其目的是完成特定的功能。如顯示人機交互界面,相應用戶的輸入,存儲信息等。對於OS X / iOS 開發來說,你無需從零創建各種對象;相反你已經有Cocoa / Cocoa Touch 框架所提供的包含相當數量的對象的庫,可供使用。

有些對象是利等可用的,例如字符串和數字等基本類型,又例如一些用戶交互界面的元素,像按鈕和表格視圖。還有一些對象需要你親自編寫代碼規定規定它們的行爲。應用開發的過程牽涉到如何最優使用底層框架提供的對象,如何編寫自定義對象,如何使二者協同工作,從而爲應用帶來其獨特的優點和功能。

在面相對象編程的術語中,一個對象是一個類的實例。本章解釋瞭如何在Objective-C中定義一個類,聲明它的接口(包含類和其實例的使用方法)。接口文件含有一個類對象可以接收的消息列表,所以你還需要爲類匹配相應的實現。實現包含對象接收到消息時所需執行的代碼。

類是對象的藍圖

一個類是對所有類對象共同行爲和屬性的描述。對於一個字符串對象來說(在Objective-C中,這是NSString類的一個實例對象),類提供各種檢驗和轉換對象內部字符的方式。類似的,用來描述代表數字的類(NSNumber)針對對象內部包含的數字提供了各種功能,例如將數字之轉換爲不同的類型。

正如根據同一個藍圖可以創建出許多外表不同的大樓,但其內部結構都是相同的。類的每個實例對象都共有某些同樣的屬性和行爲。每個NSString實例對象都有相同的行爲,不過它到底代表什麼樣的字符串數值。

任何一個對象自創建之初,其目的都是具有某個特定的功能。讀者也許知道一個字符串對象代表了某個字符串,但無需知道它用來儲存這些字符串的內部機制。讀者對於它用來處理字符串的內部機制一無所知,但需要知道怎樣使用這個對象去完成讀者想要達成的功能,比如要求其返回特定的字符,或着完成對字符串大小寫的轉換,並返回全新的字符串。

在Objective-C中,類的接口文件詳細描述了其他對象應如同類對象互動。換句話說,它是公開的,實例對象如何同外界聯繫的說明。

可變性決定了對象所代表的數值是否可以被更改

有些類規定其實例對象是不可變的。這意味着其內部數值必需在對象被創建之初就填入,隨後不可以被其他對象更改。在Objective-C中,所有的基本NSString和NSNumber數據類型都是不可變的。如有讀者需要一個對象來表示一個新的數值,那麼就需要再創建一個NSNumber對象。

有些不可變類還擁有一個可變的版本。如果讀者的確需要在運行時該表一個字符串的內容,例如在字符串後面添加從網絡接收到的新的字符,那麼就可以使用一個NSMutableString類的實例對象。這個類的實例對象同NSString類的對象具有相同的行爲,除此之外,它還具備改變其所代表的字符串的功能。

儘管NSString和NSMutableString是兩個不同的類,但它們有着許多相似之處。相比於分開編寫這兩個類,重複它們的相似之處,利用繼承來編寫它們是更好的選擇。

繼承自其他類的類

在自然世界中,生物分類學利用諸如種,屬,族之類的字眼來講不同種類的動物劃分到相應的羣組當中。這些羣組具有層級結構,例如某些種都隸屬於一個屬,某些屬都隸屬於一個族。例如,人類,大猩猩,猿有着許多相似之處。雖然他們分屬於不同的種,甚至不同的屬,但它們在生物分類學上都從屬於同一個族(稱爲“人科”),見表1-1.

在面相對象編程的世界裏面,對象同樣被劃分爲有層級之分的羣組。雖然沒有像動物分類學那樣利用專門的術語去表示,但對象根據其所屬的類被劃分爲不同羣組。正如人類從“人科”那裏繼承了某些特徵,一個類也可以從其父類那裏繼承某些功能。

當一個類繼承自另一個類,子類就繼承了父類所定義的所有屬性和行爲。另外它還可以自定義自身所特有的行爲或屬性,甚至重新定義父類的行爲。

以Objective-C的字符串類爲例,NSMutableString的父類是NSString,見表1-2.NSMutableString具有NSString的所有功能,例如查找特定的自負,返回新的改變大小寫的字符串等,但NSMutableString在此基礎之上額外添加了一些方法,例如能夠讓用戶在現有字符串末尾添加新的字符,替換或刪除字符串或字符等。

根類提供基本功能

正如所有的生物都都具備某些聲明特徵,在Objective-C中所有的對象都具有某些基本功能。但一個Objective-C對象需要同一個來自其他類的實例對象協同工作時,它要求這個對象能夠具備某些基本的特性和功能。爲此,Objective-C爲所有類指定了一個根類,被稱爲NSObject。當一個對象遇到另一個對象,它們就可以利用NSObject類所定義的基本行爲進行互動。

當讀者編寫自定義類時,至少應該繼承自NSObject。總的來說,自定義類應該繼承自Cocoa / Cocoa Touch 框架內的能夠提供最接近讀者需求的類。

如果你想爲一個iOS應用製作一個按鈕,而現有的UIButton類無法滿足你的需求,那麼就應該創建一個繼承自UIButton的自定義類,而非NSObject。如果繼承自NSObject,你首先就需要複製所有UIButton的功能,跟別說UIButton以外的功能了。另外,通過繼承自UIButton,你的自定義類講自動獲得未來針對UIButton的各種修復和改進。

UIButton類自身繼承自UIControl,其定義了所有iOS界面控件的基本行爲。UIControl又繼承自UIView,使得其可以具備顯示在屏幕上。UIView繼承自UIResponder,允許其響應用戶輸入,如點擊,手勢和設備搖晃。最後,在繼承樹的最底層,UIResponder繼承自NSObject,見表1-3.

繼承鏈意味着任何繼承自UIButton的自定義類不僅將繼承UIButton的功能,還將得到繼承鏈向上所有超類的功能。讀者最後將得到一個具備按鈕功能的對象,它可以將自己顯示在屏幕上,響應用戶輸入,同其它對象互動。

時刻注意你所使用的類在繼承鏈中的位置非常重要,這樣纔可以明確類的功能。通過幫助文檔中的類參考,讀者可以輕鬆找到繼承鏈中的所有超類,如果你在類的接口文件,或參考中找不到某個屬性或方法,那麼它很可能定義在超類中。

接口文件定義了類對象的互動方式

面相對象編程的好處之一就是前面所提到過的——要使用一個類,讀者只需知道如何與該類的類對象交互即可。更準確的說,一個對象應該將其內部實現細節隱藏起來。

舉例來說,如果你在一個iOS應用中使用標準的UIButton對象,你無需擔心當按鈕出現在屏幕上時對象如何處理與像素的關係。你只需要知道通過改變對象某些特性,例如按鈕的標題和顏色,按鈕就可以在屏幕上正確的現實出來即可。

當讀者定義自己的類時,要搞清楚哪些屬性和行爲應該公之於衆。哪些特性是可以由其他對象訪問的?這些特性可以被改變嗎?其他對象如何同自定義類的實例對象溝通?

這些信息都被包含在接口文件中——它定義了讀者想讓其他對象如何同自定義類的實例對象互動。公共接口和內部行爲,即實現文件的描述應該分開。在Objective-C中,接口代碼和實現代碼通常被放在不同的文件但中,需要公開的信息放在接口文件即可。

基本語法

Objective-C用來聲明類接口的語法如下所示:

@interface SimpleClass: NSObject

@end

以上例子聲明瞭一個名爲SimpleClass的類,繼承自NSObject。

公共屬性和行爲利用@interface聲明。在本例中,除了父類之外,什麼都沒有聲明,所以SimpleClass的實例對象的功能全部繼承自NSObject。

屬性是對象內部數據的入口

通常,對象都擁有屬性以共外接訪問其內部數據。如果讀者定義一個類來記錄聯繫人,那麼可能就需要相應的字符串代表一個人的名和姓。

這些屬性應該顯示在接口文件中,如:

@interface Person : NSObject
@property NSString *firstName;
@property NSString *lastName;
@end

在本例中,Person類聲明瞭兩個公共屬性,都是NSString類的實例對象。

這兩個屬性都是Objective-C對象,所以使用*表示它們是C指針。語句的寫法也同C語言一樣,末尾要有;作爲結尾。

也許還需要再添加一個屬性來作爲一個人的出生年份,這樣就可以根據年份統計而非姓名。添加一個代表數字的對象如下:

@property NSNumber *yearOfBirth;

可是如果只是簡單的存儲一個數值,對於NSNumber來說似乎有點大材小用。另一種方法是使用C語言提供的基本類型,保存一個整數:

@property int yearOfBirth;

屬性的特性表明了數據存取和儲存的限制

上面的例子中所有的屬性都完全對外界開放。這意味着其他對象可以讀取和改變屬性所代表的數值。

而有些情況下,你也許需要聲明一個不能被改變的屬性。在真實的世界當中,一個人想要改名,必需完成辦很多手續。如果讀者編寫的是一個官方的人事記錄軟件,就可能需要講名字設定爲只讀。任何想要改變名字的請求都必須通過另一箇中間對象來審覈,包括批准或者拒絕。

Objective-C的屬性聲明可以包括特性。特性可以用來表示,包括但不限於,一個屬性是否爲只讀。在官方的人事紀錄軟件彙總,Person類的接口類似於:

@interface Person : NSObject
@property (readonly) NSString *firstName;
@property (readonly) NSString *lastName;
@end

屬性特性在括號中聲明,緊跟@property關鍵字,詳見章節爲公開數據聲明公共屬性

方法聲明代表對象可以接收的消息

目前爲止針對類的聲明都是典型的模型對象,即用來封裝數據的對象。在Perosn類的例子當中,也許除了存取屬性聲明的變量以外,不需要其他任何額外的功能。但對於大多數類來說,它們都包含訪問自身屬性意外的功能。

鑑於Objective-C應用是許多對象結合成的有機整體,可以知道這戲對象可以通過發送消息進行交互。在Objective-C術語中,A對象向B對象發送消息被稱爲調用B對象的方法

Objective-C方法在概念上同C函數類似,而語法上卻很不一樣。一個C函數聲明類似於:

void SomeFunction();

而對應的Objective-C方法聲明則類似於:

- (void)someMethod;

在本例中,方法沒有形參。位於頭部的C語言括號內的void關鍵字表示當方法完成時其不返回任何值。

位於方法名開頭的-(減號)表示這事一個實例方法,可以針對類的任意實例對象使用。與之相對的是類方法,可以針對類本身使用,詳見章節Objective-C的類也是對象

同C函數的原型類似,接口內的方法聲明同C語言表達式一樣,需要一個;結尾。

方法可以接收形參

如果讀者需要聲明一個可以接收一個甚至多個形參的方法,語法同C語言大不一樣。

對於一個C函數來說,形參在括號內部聲明:

void SomeFunciton(SomeType value);

而Objective-C方法則把形參作爲其名稱的一部分聲明:

- (void)someMethodValue: (SomeType)value;

同返回類型一樣,形參的類型放在括號裏,正如一個標準的C語言類型轉換符一樣。

如果讀者需要提供多個形參,語法形式同C語言也不相同。C函數的多個形參都放在括號裏,由都好分隔;而在Objective-C中,接收兩個形參的方法的聲明類似於:

- (void)someMethodWithFirstValue: (SomeType)value1 secondValue: (AnotherType)value2;

在本例中,value1和value2的名稱在方法被調用時指代傳入的實參,就像使用變量那樣。

有些編程語言允許函數通過所謂的命名實參來定義;然而在Objective-C中卻不是這樣。方法中形參的順序必需同方法聲明一致。事實上secondValue部分屬於方法名的一部分,可以寫作:

someMethodWithFirstValue:secondValue:

這是可以大大提升Objective-C語言可讀性的衆多特性之一。其原理是通過方法調用傳入的值是方法的一部分,緊鄰相關的方法名,詳見章節你可以將對象傳給方法作爲形參

注:value1和value2變量名並非是方法聲明的一部分,這意味着在方法實現中不一定要使用完全一致的變量名。只需要記住方法的特徵相符即可,包括方法名,形參類型和返回類型必需相同(以及方法類型——by 作者)。

例如,以下方法同上面的方法特徵完全相符,是一個方法:

- (void)someMethodWithFirstValue: (SomeType)info1 secondValue: (AnotherType)info2;

以下這些方法同上面的方法特徵不符:

- (void)someMethodWithFirstValue:(SomeType)info1 anotherValue:(AnotherType)info2;
- (void)someMethodWithFirstValue:(SomeType)info1
secondValue:(YetAnotherType)info2;

類名必需唯一

必需注意在一個應用當中每個類的名稱都必須唯一,包括不能同框架所提供的現成類重名。如果在一個項目中讀者創建了一個同已有類通明的自定義類,編譯器將發出警告。

因此,對於讀者自定義類,最好加上相應的前綴,不少於三個字母或更多。前綴可能同你目前所編寫的應用相關,也可能是你以前寫出的可以重複利用的代碼,或者只是你名字的首字母縮寫。

本文檔給出的所有例子中類都帶有前綴:

@interface XYZPerson : NSObject
@property (readonly) NSString *firstName;
@property (readonly) NSString *lastName;
@end

注(歷史遺留):如果你還對爲什麼這麼多類都有NS作爲前綴,這是因爲Cocoa / Cocoa Touch 的一些歷史原因。Cocoa最開始作爲編寫NeXTStep操作系統下的應用程序而生。1996年蘋果公司併購了NeXTStep公司,並將NeXTStep融入OS X系統,其中就包括當時其所用的類名。Cocoa Touch 是Cocoa的iOS版本;有些類同時存在於Cocoa / Cocoa Touch 當中,同時也有很多類分別從屬於這兩個框架。兩個字母的前綴NS和UI被蘋果公司保留使用,讀者不應該再去使用。

至於方法名和屬性名,只需在其所被定義的類中唯一即可。

儘管應用中的每個C函數的名稱都必須爲一,而對於Objective-C類的方法來說,多個類擁有同一個名稱的方法是完全可以接受的(同時也是比較方便的)。在一個類中同一個方法你只能聲明一次,但是,如果你要覆蓋父類已經定義的方法,就需要再次寫出這個方法,重新聲明。

關於方法,對象的屬性以及實例變量(相關章節大多數屬性都同實例變量有關),這些在類中具有唯一的名稱即可。不過,如果讀者要使用全局變量,那麼其名稱在整個應用或者項目中必需唯一。

更多有關命名的慣例和建議,詳見慣例

類的實現定義了其內部行爲

當讀者定義了類的接口之後,包括各項公開的屬性和方法,接下來就需要編寫代碼實現類的行爲。

正如早前所說的,類的接口代碼通常被單獨放在一個指定的文件當中,通常稱爲頭文件,其擴展名是`.h`。讀者應該在擴展名爲.m的實現文件中編寫類的實現代碼。

當接口代碼被放在接口文件中時,讀者應該在編寫實現文件之前告訴編譯器讀取接口文件。Objective-C提供一個預編譯指令,#import來完成這一任務。這個指令同C語言的#include很類似,但不同之處在於其可以確保相同的文件在編譯器期間只被讀取一次。

請注意預編譯指令跟C語言指令不同,其末尾沒有;.

基本語法

類的實現的基本語法如下:

#import "XYZPerson.h"
@implementation XYZPerson
@end

如果你在接口中聲明瞭方法,那麼就必須在此文件中實現它們。

方法實現

對於簡單的,只有一個方法的類接口來說,實現如下:

@interface XYZPerson : NSObject
- (void)sayHello;
@end

其實現類似於:

#import "XYZPerson.h"
@implementation XYZPerson
- (void)sayHello {
    NSLog(@"Hello, World!");
}
@end

這個例子使用了NSLog()函數來向控制檯發送一條文字消息。這個函數同C語言庫中的pritf()函數很類似,可以接受任意數量的形參,其中第一個形參必需是一個Objective-C字符串對象(NSString)。

方法的實現代碼同C函數的定義很相似,都是用大括號將實現代碼括起來。其中,方法名,返回值和形參的類型必需和在接口文件中的聲明一致。

Objective-C同C語言一樣,區分大小寫,所以以下方法:

- (void)sayhello {
}

同上面的sayHello方法相比,將被編譯器當作兩個完全不同的方法來處理。

總的來說,方法名應該由小寫字母開頭。相較於C函數,Objective-C的命名慣例跟多的使用描述性的名稱了給方法取名。如果一個方法名包含多個單詞,則使用駝峯拼寫來保證可讀性。

請注意空行在Objective-C中的使用很多。可以將代碼塊利用tab或space進行適當的縮進,讀者將經常看到半個括號獨佔一行,例如:

- (void)sayHello
{
    NSLog(@"Hello, World!");
}

作爲OS X / iOS 的整合開發環境,Xcode會自動幫讀者縮進代碼。詳見Xcode Workspace Guide

Objective-C中的類也是對象

在Objective-C中,類本身就是一個模糊的對象類型,稱爲類對象。類對象不能像普通對象那樣聲明屬性,但是卻可以接受消息。

類對象的典型應用是一種類方法,叫做工廠方法。是除了通過內存分配和初始化來以外的創建類的實例對象以外的一種方法(詳見章節對象是動態創建的)。以NSString類爲例,其具有多個創建空字符串對象或帶初始化字符串對象的工廠方法,包括:

+ (id)string;
+ (id)stringWithString:(NSString *)aString;
+ (id)stringWithFormat:(NSString *)format, ...;
+ (id)stringWithContentsOfFile:(NSString *)path encoding:(NSStringEncoding)enc
  error:(NSError **)error;
+ (id)stringWithCString:(const char *)cString encoding:(NSStringEncoding)enc;

根據以上例子可知,類方法通過+(加號)表示,同實例方法的-有所區分。

類方法的原型可能會在接口中聲明,正如實例方法的原型一樣。類方法同實例方法一樣都需要在@implementation中實現。

使用對象

在一個Objective-C應用中,大部分工作都發生在諸多對象之間互相發送消息的基礎上。有些對象屬於Cocoa / Cocoa Touch 框架所提供的類,有些則由讀者的自定義類創建。

第一章討論了定義接口和實現的語法,包括爲了能夠迴應所接收的消息而實現相關方法的語法。本章將講解如何向對象發送消息,還會涉及到一些Objective-C的動態特徵,包括動態輸入以及運行時方法的調用機制。

在創建一個可用的對象之前,必需首先利用一套內存分配技術爲其屬性分配內存,必要時還需要爲其內部數值附上合適的初始值。本章將講解如何運用消息嵌套技術爲對象分配內存,進行初始化。

對象發送和接收消息

儘管在Objective-C中對象之間發送消息的方法不止一種,但目前來說,最常用的發送消息的基本語法是用方括號,如:

[someObject doSomething];

左邊的指代物,即someObject,是消息的接收者。右邊的消息,doSomething,是被調用的接收者的方法的方法名。換句話說,當以上代碼被執行時,someObject將會被髮送doSomething消息。

上一章曾講過如何爲類創建接口,如下:

@interface XYZPerson : NSObject
- (void)sayHello;
@end

另外還講過如何創建實現,如下:

@implementation XYZPerson
- (void)sayHello {
    NSLog(@"Hello, world!");
}
@end

注:本例使用了Objective-C的字符串@"Hello, world!"。字符串是Objective-C中允許通過字符串語法創建的類的實例對象之一。需要特別指出@"Hello, wordl!"即是一個代表Hello, world!字符串的Objective-C字符串對象。字符串對象的創建會在稍後在本章講解,詳見對象是動態創建的

假設想在有一個XYZPerson對象,你可以向其發送sayHello消息,如下:

[somePerson sayHello];

發送Objective-C消息從概念上來說同調用C函數很相似,表1-2表示對象在收到sayHello消息後的流程。

爲了弄清對象如何接受消息,就必須知道在Objective-C中指針是怎樣指代對象的。

利用指針追蹤對象

C和Objective-C都利用變量來追蹤數值,這根大多數編程語言一樣。

在標準C語言中有一組基礎標量變量,包括整數,浮點數,字符,它們的聲明和賦值如下所示:

int someInteger = 42;
float someFloatingPointNumber = 3.14f;

本地變量,即在一個方法或函數內聲明的變量,如下所示:

- (void)myMethod {
    int someInteger = 42;
}

它們的作用範圍被限制在聲明它們的方法/函數之內。

在本例中,someInteger是myMethod內的本地變量;一旦程序執行到了最後的大括號,someInteger就不再能夠被訪問了。當一個本地的標量變量(如int或float)消失,它所代表的值也隨之消失。

Objective-C對象,相反,在內存分配方面卻略有不同。對象的聲明週期通暢要比一個方法的作用範圍要長。特別需要指出的是,對象的聲明週期通常要比用來指代它的變量的聲明週期更長。所以,關於對象的內存的釋放和回收是動態的。

注:如果你知道,那麼可以說本地變量是分配在棧中的,而對象是分配在堆中的。

這就要求讀者利用C指針(其保存內存地址)來追中對象在內存中的地址,例如:

- (void)myMethod {
    int someInteger = 42;
}

雖然指針變量myString的作用域僅限於myMedthod,但它指向的字符串對象卻能夠在內存中有着更長的聲明週期。它可能早就被創建出來,又或者你需要調用其它方法來將它傳至另一個指針。

你可以將對象作爲形參牀給方法

如果發送消息時需要傳遞對象,那麼讀者應該在形參的位置填入指向該對象的指針。前面章節講解了聲明帶有一個參數的方法的語法:

- (void)someMethodWithValue:(SomeType)value;

因此聲明接受一個字符串對象的方法的語法應該類似於:

- (void)saySomething:(NSString *)greeting;

你可以爲saySomething方法做出以下實現:

- (void)saySomething:(NSString *)greeting {
    NSLog(@"%@", greeting);
}

指針greeting的作用範圍僅限於saySomething方法,即使它指向的字符串對象在方法被調用之前就已經存在,並且其在方法執行完畢之後還將繼續存在。

注:NSLog()函數利用格式說明符來指代轉換說明的順序,這點同C語言的printf()函數一樣。控制檯輸入的文字是通過在格式化字符串(第一個實參)中插入替換值(其它實參)實現的。在Objective-C中,還有一個額外的轉化說明%@,用來指代一個對象。在運行時,這個說明符將通過向其所指代的對象發送descriptionWithLocale:消息或者description消息來完成對其的替換。description方法由NSObject類實現,將返回對象所屬的類以及其在內存內的地址,但許多Cocoa / Cocoa Touch框架的類都覆蓋了這一方法,代之以提供給更有用的信息。對於NSString類來說,description方法將返回其代表的字符串數據。若需要了解 更多關於NSLog()函數和NSString的格式化說明符的內容,請參見String Format Specifiers

方法可以返回一個值

既然可以向方法的形參傳入數值,那麼方法也可以返回數值。目前爲止本章節的所有方法的返回值都是void。C語言void關鍵字意味着方法不會返回任何值。

返回值類型是int的方法表示其會返回一個int型的標量值:

- (int)magicNumber;

方法的實現代碼利用C語言的return語句表示其返回一個值。這個值在方法執行完畢後被傳回到調用方,例如:

- (int)magicNumber {
    return 42;
}

讀者可不不去理會方法的返回值。正如在本例中,magicNumber方法並沒有任何事情,只是返回了一個值,但待用方法本身不存在任何問題:

[someObject magicNumber];

如果讀者需要追中方法的返回值,那麼可以聲明一個變量,然後將方法調用的返回值付給它,例如:

int interestingNumber = [someObject magicNumber];

當然方法也可以返回一個對象。以NSString類爲例,它有一個uppercaseString方法:

- (NSString *)uppercaseString;

它的用法同返回標量值的方法一樣,唯一的區別是你需要喲過一個指針來追蹤其返回的結果(對象):

NSString *testString = @"Hello, world!";
NSString *revisedString = [testString uppercaseString]

當這一方法執行完畢並返回時,reviseString指針將指向一個NSString對象,其代表代表字符串HELLO WORLD!。

要記住當一個方法執行完畢,並返回一個對象是,例如:

- (NSString *)magicString {
    NSString *stringToReturn = // create an interesting string...
    return stringToReturn;
}

字符串對象將在方法執行完畢後繼續存在(已經被傳走),儘管指向它的指針stringToReturn已經消失。

在這種情況下,讀者需要考慮一些內存管理的問題:一個被方法返回的獨享(在堆上創建)的聲明週期需要足夠長,以便方法的調用者能夠使用它,但是,這並非意味着其永遠不會消失,因爲這樣會帶來內存泄露的問題。大多數情況下,Objective-C編譯器的ARC特性將替你處理這些問題。

對象可以向自身發送消息

當你編寫實現代碼時,你可以使用一個重要的隱藏值,self。理論上來說,self用來指代:接受此消息的對象。它是一個指針,正如上面用到的greeting一樣。另外它還可以用來向當前接受消息的對象發送消息。

你可能覺得應該重構XYZPerson類的實現代碼,在sayHello方法中調用saySomething方法,這樣的話就可以只將NSLog()函數放到一個獨立的方法中。這意味着你有可以在此基礎之上創造更多的方法。如sayGoodbye,這會調用saySometing方法,來處理真實的問候過程。如果隨後還想在用戶界面的文本框裏展示各種問候用語,你只需要改變saySomething方法即可,而不需要去單獨調整每個問候方法。

新的實現代碼利用self來向當前對象發送消息,例如:

@implementation XYZPerson
- (void)sayHello {
    [self saySomething:@"Hello, world!"];
}
- (void)saySomething:(NSString *)greeting {
    NSLog(@"%@", greeting);
} @end

如果讀者利用更新過後的實現代碼向一個XYZPerson對象發送sayHello消息,那麼程序流程圖應該如圖2-2所示。

對象可以調用父類實現的方法(2015.6.24)

在Objective-C中,還有一個讀者可以使用的重要的關鍵字,super。向super發送消息可以調用調用相應的由繼承鏈上一級的父類實現的方法。super最常見的用法是覆蓋父類的方法。

假設你需要創建一個全新的類,名稱爲shouting person,它的所有問候語都將用大寫字母顯示。你可以完全複製XYZPerson類,然後將每個方法內的字符串都改成大些字母,但最簡單的方法是創建一個繼承自XYZPerson類的子類,然後覆蓋saySomething方法,這樣就可以以大寫字母顯示所有問候,例如:

@interface XYZShoutingPerson : XYZPerson
@end
@implementation XYZShoutingPerson
- (void)saySomething:(NSString *)greeting {
    NSString *uppercaseGreeting = [greeting uppercaseString];
    NSLog(@"%@", uppercaseGreeting);
}
@end

上面的例子聲明瞭一個額外的字符串指針uppercaseGreeting,然後將向原來的greeting對象發送uppercaseString消息後返回的值賦給它。正如讀者前面看到的,通過這樣的方法可以獲得一個全新的字符串,其是在原字符串單基礎之上將所有字母變成大寫後得到的。

由於XYZPerson類實現了方法sayHello,而且XYZShoutingPerson是XYZPerson的子類,所以你也可以向XYZPerson類的實例對象發送sayHello消息。當你調用XYZShoutingPerson類的sayHello方法時,[self saySomething:...]消息用將會調用覆蓋後的方法,即將所有字母轉換成大寫,如流程圖2-3所示。

然後新的實現方法並不完美,因爲如果你稍後想要修改XYZPerson類中的saySomething方法的實現的話(在用戶界面顯示問候語,而非控制檯),你就需要同時在XYShoutingZPerson類中修改實現。

一個較爲理想的方法是修改XYZShoutingPerson類的saySomething方法實現,通過調用父類(XYZPerson)的實現去處理問候語:

@implementation XYZShoutingPerson
  - (void)saySomething:(NSString *)greeting {
     NSString *uppercaseGreeting = [greeting uppercaseString];
    [super saySomething:uppercaseGreeting];
  }
@end

現在如果想一個XYZShoutingPerson類對象發送sayHello消息,其流程圖如圖2-4所示。

對象是動態創建的

正如本章前面所介紹的,對於一個Objective-C對象來說,其內存分配是動態的。創建對象的第一步是分配內存,不僅要確保一個類中所定義的屬性得到了妥善的內存分配,還有確保其所有父類的屬性也得到了妥善的分配。

根類NSObject提供了一個方法,可以爲讀者完成這一步驟:

+ (id)alloc;

要注意這一方法的返回類型是id。在Objective-C中,這是一個特殊的關鍵詞,表示某種類型的對象。它是一個指向對象的指針類似於NSObject *,但它的使用不加*(星號)。在章節Objective-C是一門動態語言中有更詳細的描述。

alloc方法還有一個重要的任務,就是將分配給對象屬性的內存晴空,將值設爲0。這避免了由於內存曾經儲存的數據所引發的錯誤,但若要初始化對象,這是不夠的。

讀者需要將alloc方法的調用和init方法的調用結合起來,init是另一個NSObject類所定義的方法:

- (id)init;

init方法被用來在對象創建之初將一個類的所有屬性設置爲合適的初始值,下一張將會對它有更詳細的討論。

注意init方法的返回值類型是id

如果一個方法返回一個指向對象的指針,那麼就可以將調用這個方法的消息嵌入到另一個消息中,這個方法返回的對象將作爲外圍消息的接收者。正確的內存分配和初始化潛逃順序如下:

NSObject *newObject = [[NSObject alloc] init];

上面的例子將newObject指針指向一個新創建的NSObject類的實例對象。

最內部的調用被首先執行,所有NSObject類收到了alloc消息,然後返回一個內存分配好的新的NSObject對象實例。這個被返回的對象然後被用做init消息的接收者,這又將返回一個對象,指針newObject將指向它,如表2-5所示。

注:init方法有可能返回一個同alloc方法所創建的對象相比完全不同的對象,所以最好的方法是將兩個方法嵌套發送。任何被初始化的對象都必須分配指針。例如,以下代碼是錯誤的:

NSObject *someObject = [NSObject alloc];
[someObject init];

如果對init方法的調用返回了一個一個其它的對象,那麼讀者將得到一個指向內存分配過,但並沒有被初始化。

初始化方法可以接收實參

有些對象必須被初始化爲指定的值。以一個NSNumber對象爲例,其被許被初始化爲它所能代表的數字值。

NSNumber類包含若干初始化方法,包括:

- (id)initWithBool:(BOOL)value;
- (id)initWithFloat:(float)value;
- (id)initWithInt:(int)value;
- (id)initWithLong:(long)value;

帶實參的初始化方法同原本的init方法使用方法一樣——一個NSNumber對象的內存分配和初始化過程如下:

NSNumber *magicNumber = [[NSNumber alloc] initWithInt:42];

工廠方法是內存分配和初始化這一過程的補充

正如前面章節所述,一個類也可以定義工廠方法。工廠方法是內存分配和初始化這一過程的另一種選擇,這樣就不用再潛逃兩個方法。

NSNumber類定義了若干工廠方法來對應相應的初始值類型,包括:

+ (NSNumber *)numberWithBool:(BOOL)value;
+ (NSNumber *)numberWithFloat:(float)value;
+ (NSNumber *)numberWithInt:(int)value;
+ (NSNumber *)numberWithLong:(long)value;

工廠方法的使用方法如下:

NSNumber *magicNumber = [NSNumber numberWithInt:42];

這個工廠方法同前面的alloc] init]作用完全一樣。工廠方法會直接調用alloc和相應的init方法,從而使對象創建變得簡單。

使用new方法創建不需要初始化不需要實參的對象

另外,同樣可以使用'new'方法創建實例對象。這個方法有NSObject類提供,而且不用在子類的實現中覆蓋。

以下方法同不需要任何實參的alloc和init過程效果相同:

XYZObject *object = [XYZObject new];
// is effectively the same as:
XYZObject *object = [[XYZObject alloc] init];

使用簡便字面量語法創建對象

有些類允許讀者使用更簡單的字面量語法去創建實例對象。

例如,你可特殊的字面標記來創建一個NSString實例對象,例如:

NSString *someString = @"Hello, World!";

以上方法同使用alloc + init的組合,或者工廠方法的效果完全相同:

NSString *someString = [NSString stringWithCString:"Hello, World!"
                                          encoding:NSUTF8StringEncoding];

NSNumber類同樣可以使用許多字面量語法:

NSNumber *myBOOL = @YES;
NSNumber *myFloat = @3.14f;
NSNumber *myInt = @42;
NSNumber *myLong = @42L;

再次重申,這些例子中字面量語法的同使用初始化方法或工廠方法的效果完全相同。你還可以利用括號表達式來創建複雜的NSNumber對象:

NSNumber *myInt = @(84 / 2);

在上面的例子中,括號內的表達時先被計算,計算的結果將被用來創建一個NSNumber對象。

Objective-C也支持用字面量語法創建不可變的NSArray對象和NSDictionary對象;將在相關章節值和集合中做進一步討論。

Objective-C是一門動態的語言

如前所述,讀者需要一個指針來追蹤內存中的對象。由於Objective-C的動態本質,追蹤對象所使用的指針類型其實並不重要——當向一個指向特定類型對象的指針發送消息時,不管指針的類型如何聲明,程序總是會調用符合對象類型的正確的方法。

id類型定義了一個指向通用類型對象的指針。完全可以在聲明一個指針變量時使用id作爲其類型定義,但這樣的話編譯器就不能根據指針類型提供相應的信息。

請思考下面的代碼:

id someObject = @"Hello, World!";
[someObject removeAllObjects];

在上面的例子中,someObject將直線一個NSString實例對象,但編譯器出了知道這個實例對象術語某個類,其它的一無所知。removeAllObjects方法是由某個Cocoa / Cocoa Touch框架內的類定義的,因此編譯器不會發出警告,即使在程序運行時這段代碼會由於NSString對象無法相應removeAllObjects方法而產生異常錯誤。

使用靜態類型重寫以上代碼:

NSString *someObject = @"Hello, World!";
[someObject removeAllObjects];

現在編譯器將會及時發出警告,因爲它已經知道someObject對象的類型是NSString,不能相應removeAllObject方法。

由於一個對象的類型只能等到運行時才能確定,所以當讀者爲對象分配指針是,指針的類型並沒有什麼作用。若要使用XYZPerson或XYZShoutingPerson類,你可以編寫以下代碼:

XYZPerson *firstPerson = [[XYZPerson alloc] init];
XYZPerson *secondPerson = [[XYZShoutingPerson alloc] init];
[firstPerson sayHello];
[secondPerson sayHello];

儘管firstPerson和secondPerson都被聲明爲指向XYZPerson類實例對象的指針,但是,在運行時,secondPerson將會指向一個XYZShoutingPerson實例對象。當sayHello消息被分別發送給兩個對象時,程序會各自調用正確的實現方法;對於secondPerson來說,意味着XYZShoutingPerson類內所定義的實現。

判斷對象是否相等

如果讀者需要判斷一個對象是否同另一個對象相同,要注意所有的對象都是由指針追蹤的。

標準的C語言使用==(相等運算符)來判斷兩端的標量所代表的值是否相等,如:

if (someInteger == 42) {

// someInteger has the value 42
      }

在處理對象時,==用來判斷兩個不同的指針是否指向同一個對象:

if (firstPerson == secondPerson) {
    // firstPerson is the same object as secondPerson
}

如果讀者需要判斷兩個對象是否代表相同的數據值,就需要使用由NSObject類定義的方法isEqual:

if ([firstPerson isEqual:secondPerson]) {
    // firstPerson is identical to secondPerson
}

如果讀者需要比較一個對象所代表的數據大於或者小於另一個對象,不能簡單的使用C語言的比較操作符<>。而是應該使用一些foundation框架所提供的基本類型,如NSNumber,NSString和NSDate類,這些類提供一個compare方法:

if ([someDate compare:anotherDate] == NSOrderedAscending) {
    // someDate is earlier than anotherDate
}

使用nil

這聲明標量變量時就直接對其賦值,是比較好的做好。否則的話它們的初始值會受到以前處於棧內的內容的污染,從而產生錯誤:

BOOL success = NO;
int magicNumber = 42;

然而對於指向對象的指針來說就不需要,因爲如果讀者在聲明指針時沒有給它們賦初始值,編譯器會自動將它們標記爲nil

XYZPerson *somePerson;
// somePerson is automatically set to nil

如果你暫時還沒有對象可以作爲初始值賦給指針,那麼nil值是最安全的選擇。因爲在Objective-C中可以向nil發送消息。如果讀者向nil發送了消息,什麼都不會發生。

注:如果讀者想更確切的知道向nil發送消息的返回值,如下:對於返回值類型爲對象的消息來說,其返回值爲nil;數字類型的是0;布爾類型的是NO;結構體類型的全部結構成員都會被設置爲0;

如果你需要確保一個指針不是指向nil(即指向內存中的對象),你可以使用C語言的!=(不等運算符):

if (somePerson != nil) {
    // somePerson points to an object
}

或者利用if表達式的條件控制語句直接驗證:

if (somePerson) {
    // somePerson points to an object
}

如果somePerson變量指向nil,它的邏輯值是0(假 / false)。如果它指向一個地址,那麼它的邏輯值就是非0,邏輯判斷結果會是真。

類似的,如果你要判斷一個變量是否指向nil,你可以使用相等運算符:

if (somePerson == nil) {
    // somePerson does not point to an object
}

或者直接利用邏輯非運算符:

if (!somePerson) {

 // somePerson does not point to an object
      }

封裝數據

除了上一章介紹的首發消息,對象還會通過屬性封裝數據。

本章將講解Objective-C用語聲明對象屬性的語法,並將深入剖析這些屬性在默認情況下是怎樣合成存取方法的。如果一個屬性由一個實例變量構成,那麼在任何一個初始化方法中,這個變量的值必需的到正確的設置。

如果一個對象需要通過屬性保持同另一個對象的聯繫,那麼就要洞悉兩個對象之家的關係本質。儘管Objective-C的內存管理在大多數時候都有ARC完成,但明白如何避免諸如強引用循環(會導致內存泄漏)等內存管理問題還是很重要的。本章將講解一個對象的聲明週期,以及如何如果組織對象的關係圖。

屬性封裝着對象的值

爲了完成任務,大多數對象都必須記錄一些信息。有些對象在設計之初就會被要求儲存一個或更多的值,例如NSNumber類會保存一個數字值,一個自定義的XYZPerson類會模擬一個人,儲存其姓和名。有些對象在用法方面會比較抽象,例如處理用戶交互界面和界面所顯示信息之間的關係,但即使是這些對象也需要記錄交互界面和模型對象的相關信息。

爲可裸露數據聲明公共屬性

Objective-C屬性是類封裝相應的信息的一種方法。正如讀者在相關章節屬性控制在對象數據值的入口中所看的,屬性的在接口文件中聲明,如:

@interface XYZPerson : NSObject
@property NSString *firstName;
@property NSString *lastName;
@end

在這個例子中,XYZPerson類聲明瞭字符串屬性來保存一個人的姓和名。

鑑於面向對象編程中的基本原則之一就是對象應該在接口文件以外隱藏自身的內部工作機制,所以在訪問對象的屬性時,要使用對象公開的行爲,而非直接試圖獲取內部的數據值。

利用存取方法得到或設置屬性值

讀者可以通過存取方法設置一個對象的屬性:

NSString *firstName = [somePerson firstName];
[somePerson setFirstName:@"Johnny"];

默認情況下,這些存取方法是由編譯器自動合成的,所以在接口文件中利用@property關鍵字聲明屬性即可,其它不需要做。

合成的存取方法遵循以下命名慣例:

  • 獲取屬性值的方法(取方法getter method)同屬性的名稱相同。

  • 設置屬性值的方法(存方法setter method)以set開頭,其後加上首字母大寫的屬性名。屬性名爲firstName的屬性的存方法應爲setFirstName。

如果你不想不想讓屬性被通過存方法更改,你可以在聲明屬性時爲其添加一個只讀read-only特性:

@property (readonly) NSString *fullName;

特性不僅可以明確表示屬性同其他對象的互動關係,還規定了編譯器如何合成相應的存取方法。

在上面的例子中,編譯器將會合成一個名爲fullName的取方法,但沒有setFullName存方法。

注:與readonly相對的是readwrite。無需特別聲明讀寫特性,因爲它是默認值。

如果如果讀者相對存取方法取其他的名字,可以通過特性重新給存取方法命名。如果屬性類型是BOOL(YES或NO值),習慣上將取方法的第一個單詞改爲is。例如,有一個類型爲布爾值的名爲finished的屬性,它的取方法名爲isFinished。

再次申明,讀者可以爲屬性添加特性。

@property (getter=isFinished) BOOL finished;

如果需要聲明多個特性,可以利用逗號將特性隔開:

@property (readonly, getter=isFinished) BOOL finished;

在上面的例子中,編譯器智慧合成名稱爲isFinished的去方法,而不會合成setFinshed去方法。

注:一般來說,屬性的羣去放啊放應該遵循KVC,也就是它們會遵循特定的命名慣例。更多信息詳見,KVC編程指南

點語法是調用存取方法的便捷語法

除了明確使用存取方法,Objective-C還提供一種點語法以便訪問對象的屬性。

點語法允許讀者這樣訪問屬性:

NSString *firstName = somePerson.firstName;
somePerson.firstName = @"Johnny";

點語法純粹是一種存取方法的便捷寫法。當讀者使用點語法時,程序仍然調用存方法或取方法來讀取或改變對象的屬性:

  • 讀取屬性時使用somePerson.firstName就等同於使用[somePerson firstName]

  • 設置屬性時使用somePerson.firstName = @"Johnny"就等同於使用[somePerson setFirstName:@"Johnny""]

這意味着通過點語法存取屬性同樣受到屬性特性的影響。如果一個屬性被標記爲只讀,那麼當讀者視圖利用點語法改變它的值的時候,編譯器會發出錯誤警告。

大多數屬性都是實例變量

默認情況下,一個可讀寫的屬性都是一個實例變量,編譯器在合成屬性時會生成這個變量。

實例變量是一個能夠在對象聲明週期內存在,並保存數值的變量。系統在對象創建之初就會爲其實例變量分配內存(通過alloc方法),並在對象被收到dealloc方法時釋放這些內存。

除非讀者做出額外的聲明,被合成的實例變量的名稱同屬性名相同,但是實例變量名前有一個_下劃線前綴。對於一個叫做firstName的屬性來說,它的由編譯器自動合成的實例變量的名稱爲_firstName。

儘管,即使對於對象本身來說,訪問屬性的最佳方式也是通過存取方法,但在類方法的實現中,通過實例變量名稱直接訪問也是可以的。通過下劃線讀者可以非常直觀的表明這是在訪問一個實例變量,而非,例如,本地變量。

- (void)someMethod {
    NSString *myString = @"An interesting string";
    _someString = myString;
}

在上面的例子中,很明顯可以看出myString是一個本地變量,_someString是一個實例變量。

總的來說,你應該使用存取方法或點語法來訪問屬性,即使你是在對象的類的自身的實現中訪問的,這時應該搭配self使用:

- (void)someMethod {
    NSString *myString = @"An interesting string";
    self.someString = myString;
  // or
    [self setSomeString:myString];
}

這一規則的例外是出了在編寫初始化,取消對象內存分配或自定義存取方法時,本章稍後將講解。

可以自定義合成的實例變量的名稱

如前所述,對於一個可寫入的屬性來說其默認的實例變量名稱是_propertyName。

如果你希望讓一個實例變量擁有一個默認值意外的名稱,你需要在實現代碼中使用以下的語法告訴編譯器:

@implementation YourClass
@synthesize propertyName = instanceVariableName;
...
@end

例如:

@synthesize firstName = ivar_firstName;

在上面的例子中,屬性名仍然爲firstName,而且讓然可以通過firstName和setFirstName的存取方法以及點語法訪問,但其背後運行機制中的實例變量的名稱將爲ivar_firstName。

重要:如果讀者直接使用@synthesize + 屬性名,其後不加任何實例變量名,如:

@synthesize firstName;

那麼實例變量將會和屬性名同名,沒有下劃線。在上面的例子中,實例變量名是firstName,沒有下劃線。

可以定義不通過屬性定義實例變量

當需要追蹤一個值或其他對象的時候,最好是使用屬性。

如果讀者的確需要在不通過屬性的情況下定義自己的實例變量,那麼可以在類的接口文件實現文件中,在大括號內添加實例變量,如:

@interface SomeClass : NSObject {
    NSString *_myNonPropertyInstanceVariable;
  }
... @end
  @implementation SomeClass {
      NSString *_anotherCustomInstanceVariable;
} ... @end

注:讀者同樣可以在類擴展中添加實例變量,詳見相關章節類擴展擴展了內部實現

直接通過初始化方法訪問實例變量

存方法可能會帶來一些副作用。它們可能會出發KVC通知,或沒有按照讀者所定義的實現完成特定的任務。

在初始化方法中,讀者應該總是直接訪問實例變量,因爲在初始化的過程中,屬性準備完畢時,對象的其他部分可能還爲初始化完畢。即使在自己編寫的類中不自定義任何存取方法,或者能夠確保不會產生任何副總用,但是,這個類的子類也很可能覆蓋相應的行爲,從而產生某些錯誤。

一個典型的初始化方法看起來類似於:

- (id)init {
    self = [super init];
    if (self) {
        // initialize instance variables here
}
    return self;
}

在一個init方法中,在開始任何自身的初始化工作前,應該調用超類的初始化方法的並將其返回值賦給self。超類的初始化方法有可能未能成功初始化對象,並返回nil,所以讀者應該在進行自身的初始化工作之前,總是檢查self是否爲nil。

通過調用[super init]作爲方法的第一行 ,從這個對象的類的根類開始,程序會按順序依次調用這個類的每一個父類的初始化方法。初始化一個XYZPerson對象的過程如圖3-1所示。

正如讀者在前面章節所見到的,一個對象可以通過調用init方法,或某些帶有初始值的方法來初始化。

以XYZPerson爲例,最好能夠創建一個能夠設置人的姓和名的初始化方法:

- (id)initWithFirstName:(NSString *)aFirstName lastName:(NSString *)aLastName;

方法的實現應該類似於:

 - (id)initWithFirstName:(NSString *)aFirstName lastName:(NSString *)aLastName {
      self = [super init];
      if (self) {
          _firstName = aFirstName;
                  _lastName = aLastName;
      }
      return self;
  }
指定初始化方法是基礎的初始化方法

如果一個對象聲明瞭一個或多個初始方法,讀者應該決定哪個方法是指定初始化方法。這個方法通常應該擁有最多的可能性的初始化方法(例如擁有最多的參數),並且可以方便的被其他初始化方法調用。讀者還應該利用指定初始化方法代入合適的初始值覆蓋init方法。

如果XYZPerson類有一個作爲生日的屬性,那麼這個類的指定初始化方法應該是:

- (id)initWithFirstName:(NSString *)aFirstName lastName:(NSString *)aLastName
                                            dateOfBirth:(NSDate *)aDOB;

這個方法將設置相關的實例變量。如果讀者仍然希望僅僅爲姓和名專門提供一個便捷的初始化方法,那麼可以利用指定初始化方法來實現這個方法,如:

- (id)initWithFirstName:(NSString *)aFirstName lastName:(NSString *)aLastName { return [self initWithFirstName:aFirstName lastName:aLastName dateOfBirth:nil];
}

當然還可以實現一個標準的init方法,以提供合適的默認值:

- (id)init {
    return [self initWithFirstName:@"John" lastName:@"Doe" dateOfBirth:nil];
}

如果讀者要爲一個具有多個init方法的類編寫一個子類,那麼要麼需要覆蓋負累的指定初始化方法,替換爲你自己的初始化方法,要麼額外的初始化方法。不管用哪種方式,在進行自己的初始化工作之前,你都應該首先調用超類的初始化方法(代替[super init])。

實現自定義的存取方法

屬性並不一定總是要有實例變量作爲支持。

例如,XYZPerson類可能爲一個人的全名定義了一個只讀屬性:

@property (readonly) NSString *fullName;

相較於每次改變姓和名時都要更新fullName屬性,利用自定義的存取方法來設置字符串會是更好的選擇:

- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}

這個簡單的例子通過格式化字符串和轉換說明符(上一章的內容)來構建一個含有姓和名的字符串,中間用空格隔開。

注:儘管這只是一個簡單的例子,但需要注意姓名的格式是因地區而異的。

如果讀者需要在自定義的存取方法中使用一個實例變量,那麼在方法的實現內必須直接訪問實例變量。例如,直到屬性被請求時再對其進行初始化,這是一種很常見的做法,通常被稱爲lazy accessor,例如:

@property (readonly) NSString *fullName;

- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}

- (XYZObject *)someImportantObject {
    if (!_someImportantObject) {
        _someImportantObject = [[XYZObject alloc] init];
    }
    return _someImportantObject;
}

返回返回值之前,方法首先判斷_someImportantObject實例實例變量是否爲空;如果是,則爲其創建一個對象,並賦給它。

注:在至少需要合成一個存取方法的情況下,編譯器就會自動合成實例變量。如果對於一個可讀寫的屬性同時手動實現了存取方法,或對於一個只讀屬性實現了取方法,那麼編譯器就會認定讀者接管了屬性的實現,不會自動合成實例變量。

如果你仍然使用一個實例變量,就需要主動要求編譯器合成實例變量:

@synthesize property = _property;
屬性默認是多線程的

默認情況下,一個Objective-C屬性是多線程的:

@interface XYZObject : NSObject
@property NSObject *implicitAtomicObject;          // atomic by default
@property (atomic) NSObject *explicitAtomicObject; // explicitly marked atomic
@end

這意味着編譯器合成的存取方法可以被完全檢索和設置,即使存取方法被多線程同時調用。

由於多線程的內部實現和併發性是私有的,所以無法將合成的存取方法和讀者自定義的存取方法結合在一起。這樣的話會受到編譯器的警告。例如
爲一個多線程的,可讀寫的屬性自定義了一個存方法,與此同時又要求編譯器自動合成取方法。

讀者可以使用nonatomic單線程特性來明確編譯器自動合成的存取方法只單純設置或返回一個值,如果同一個值同時被多個線程訪問,可能會發生意外。
因此,訪問單線程特性的屬性速度會更快,並且,例如,讀者可以將自定義的存方法和編譯器合成的取方法結合起來使用,例如:

@interface XYZObject : NSObject
@property (nonatomic) NSObject *nonatomicObject;
@end
@implementation XYZObject
- (NSObject *)nonatomicObject {
      return _nonatomicObject;
  }
  // setter will be synthesized automatically
  @end

注:屬性的線程特性並不意味着對象的線程安全性。假設一個XYZPerson對象的姓和名這兩個屬性在一個線程裏發生了改變。如果另一個線程在同一時刻也訪問了姓名這一屬性,那麼多線程的取方法將會返回完整的字符串,但是卻不能保證這些值是正確的。如名在改變之前就受到了訪問,但姓卻在改變之後受到了訪問,那麼最終將得到一個錯誤的姓名。這個例非常簡單,但線程的安全性在考慮到整個對象網絡全局時會變的非常複雜,詳情請參考Concurrency Programming Guide

通過所有權和負責制管理對象圖關係圖

正如讀者已經見到的,Objective-C對象的內存是在上分配的,這意味着你必需使用指針來追蹤對象的地址。同標量值不同,很難用指針變量的聲明週期去判定一個對象的生命週期。相反,只要其他對象還需要一個對象,這個對象就必須活躍在內存當中。

不用去考慮單個對象的聲明週期管理問題,讀者應該去考慮對象之間的關係。

在XYZPerson對象的例子當中,兩個字符串屬性firstName和lastName都被XYZPerson的實例對象所擁有,這意味着只要XYZPerson對象存在於內從中,它們就應該存在於內存中。

當一個對象以這種方式依賴於其它對象存在,擁有其他對象,那麼就可以說第一個對象對其他對象擁有強引用。在Objective-C中,只要一個對象還擁有來自另一個對象的強引用(即至少被一個對象擁有),那麼它就要一直存在下去。XYZPerson對象和兩個NSString對象的關係如圖3-2所示。

當一個XYZPerson對象在內存中被撤銷時,兩個字符串對象也隨之被撤銷,前提是沒有其他的強引用指向它們。

爲了給例子增加一點難度,請試着思考如圖3-3的一個應用的對象關係圖。

當用戶點擊Update鍵,徽章預覽將會跟新響應的信息。

第一次輸入個人信息,並點擊Update鍵後,簡易的關係圖如圖3-4所示。

當用戶改動了人的名,對象關係圖入圖3-5所示。

顯示徽章的視圖仍然對字符串對象@"John"維持着強引用,即使在XYZPerson對象那裏字符串firstName已經改變了。這意味着@"John"將會繼續停留在內存裏,徽章視圖用它來進行顯示。

一定用戶第二次點擊Update鍵,徽章視圖會被告知更新它的內部屬性來和person對象同步,所以對象關係如圖3-6所示。

此時,原來的@"original"對象已經沒有強引用指向它了,所以它被移除了內存。

默認情況下,Objective-C中的屬性和變量對其他對象的引用都是強引用。這在大多數情況下沒有問題,但的確會有形成潛在的強引用循環的可能性。

避免強引用循環

儘管強應用對於對象之間的單向關係來說沒有問題,但當程序中有多個互相關聯的對象讀者要小心。如果一組對象由強引用相互聯繫,那麼即使所有的對象都沒有來自外界的強引用,那麼這些對象還是會因爲彼此之間的強引用而一直存在下去。

一個很明顯的關於強在強應用循環的例子是:一個列表視圖對象和它的代理對象。爲了讓一個通用的列表視圖對象能夠適應多個場合,它就要指派一個代理代替其完成某些任務。也就是說,視圖對象將依賴另一個對象決定其顯示的內容,用戶交互時的反應等。

一個常見的場景是列表視圖有指向代理的強引用,而其代理也有指向列表視圖的強引用,如圖3-7所示。

此時如果其他對象放棄了指向列表視圖和其代理的強應用,那麼就會出現一個問題,如圖3-8所示。

即使此時這兩個對象已經沒有來自外界的強引用,應該被釋放了,但是由於他們內部之間存在互相指向對方的強引用,這兩個對象還是不會被釋放,而會一直存在於內存之中,這就是強引用循環。

解決這個問題的方法是將其中一個強引用替換爲弱引用。一個弱引用不會產生所有權,也不會負責釋放被指向的對象,所以不會保留被指向的對象。

如果列表視圖對象經過修改,使用弱引用指向它的代理(這也是UITableView和NSTableView實際的解決方法),那麼一開始的對象關係如圖3-9所示。

當關系圖中的其他對象放棄指向列表視圖和其代理的強引用後,此時左邊的代理對象已經沒有指向它的強引用,如圖3-10所示。

這意味着代理對象的內存將被撤銷,因此指向列表視圖對象的強引用也將消失,如圖3-11所示。

一旦代理對象的內存被撤銷,也就沒有了指向視圖對象的強引用,所以視圖對象也被撤銷了。

在屬性中聲明強弱特性來管理所有關係

默認情況下,對象的屬性都是這樣聲明的:

@property id delegate;

屬性默認合成的實例變量會對指向的對象擁有強引用特性。爲了聲明一個弱引用,需要爲屬性添加一個特性,如:

@property id delegate;

注:與弱引用特性相對的是強引用特性。沒有必要專門聲明強引用特性,因爲它是默認值。

本地變量(以及不經過屬性聲明的實例變量)同樣在默認情況下對所指向的對象擁有強引用。這意味着以下的代碼工作方式會同讀者的預期一樣:

NSDate *originalDate = self.lastModificationDate;
self.lastModificationDate = [NSDate date];
NSLog(@"Last modification date changed from %@ to %@",
                    originalDate, self.lastModificationDate);

在上面的例子中,本地變量originalDate對一開始的lastModificationDate對象(實例變量)擁有強引用。當名爲lastModificationDate的屬性改變時,屬性就不再對原本的日期對象保持強引用,但是本地變量originalDate仍然對其保持強引用。

注:一個指針變量將再其作用域內對一個對象保持強引用,直到它指針消息,或者指針被賦予另一個對象或者指向nil。

如果讀者不想讓一個變量擁有強引用,那麼可以用__weak關鍵字加以聲明,如:

NSObject * __weak weakVariable;

由於弱引用並不會維持一個對象的激活狀態,所以就可能出現在對象還在處於使用狀態時,就被釋放了。爲了避免懸垂指針(dangling pointer)的情況發生(即指針指向了在內存中原本存在,但突然被撤銷了的對象),弱引用的指針將在對象被撤銷時自動被設置爲nil。

這意味着在上述的日期例子中,如果你使用如引用的變量:

NSDate * __weak originalDate = self.lastModificationDate;
self.lastModificationDate = [NSDate date];

原本的orginalDate變量可能會被設置爲nil。當self.lastModificationDate會指向新的對象時,那麼屬性就不會再對以前的日期對象維持強引用。如果沒有其他的強引用,原本的日期對象會被撤銷,originalDate變量也會指向nil。

弱引用變量可能會早以下的代碼中造成誤解:

NSObject * __weak someObject = [[NSObject alloc] init];

在上面的代碼中,由於新創建的對象沒有任何強引用指向它,所以它被創建之初就會被撤銷,然後someObject變量將會指向nil。

注:weak相對的是strong。讀者無需專門聲明__strong,因爲它是默認值。

還需要考慮到如果在一個方法的實現中,需要訪問若干次弱引用屬性,例如:

- (void)someMethod {
    [self.weakProperty doSomething];
    ...
    [self.weakProperty doSomethingElse];
}

在這種情況下,你需要使用本地變量臨時指向弱引用屬性,以便利用強引用保存對象,確保在方法實現的過程中對象不會突然消失:

- (void)someMethod {
    NSObject *cachedObject = self.weakProperty;
    [cachedObject doSomething];
    ...
    [cachedObject doSomethingElse];
}

在上面的例子中,cacheObject變量對原本的弱引用屬性施加了一個強引用,這樣對象就不會在cacheObject的作用於中被撤銷(當然這個指針也不能被重新賦給另一個對象)。

要記住讀者需要確保一個弱引用屬性指向的對象在其被使用的時候不會被撤銷。如果只是做到以下的代碼是不夠的:

if (self.someWeakProperty) {
    [someObject doSomethingImportantWith:self.someWeakProperty];
}

因爲在一個多線程應用中,屬性指向的對象可能在條件判斷和方法調用之間的過程中被撤銷掉,從而使條件控制失去作用。相應的,所以讀者必需給要使用的對象施加一個強引用,例如:

NSObject *cachedObject = self.someWeakProperty;         //1 
if (cachedObject) {                                     //2 
    [someObject doSomethingImportantWith:cachedObject]; //3 
}                                                       //4 
cachedObject = nil;                                     //5

在上面的例子中,第一行創造了一個強引用,意味着確保對象在條件控制和方法調用之家存活。第五行,cachObject被設置爲指向nil,因此指向對象的強引用小時。如果原本的對象此時沒有任何強引用指向,它將被撤銷,而someWeakProperty屬性(實例變量)將被設置爲nil。

對某些類使用Unsafe Unretained特性

在Cocoa / Cocoa Touch中有一些類還不支持弱引用,這意味着你無法通過聲明弱引用的屬性或本地變量來追中這些類的對象。這些類包括NSTextView,NSFont和NSColorSpace;要獲取完整信息,詳見相關文章Transitioning to ARC Realease Notes

如果這着需要用弱音用指向這些類,那麼就必須使用unsafe來引用。對於一個屬性來說,這意味着使用unsafe_unretained特性家:

@property (unsafe_unretained) NSObject *unsafeProperty;

對於變量來說,你需要使用__unsafe_unretained來聲明變量:

NSObject * __unsafe_unretained unsafeReference;

一個不安全的引用類似類似於一個弱音用(不會維持所指向對象的激活狀態),但是若其指向的對象被以外撤銷,這個指針又不會被自動設置爲nil。這意味着你會獲得一個懸垂指針,因爲是不安全。向懸垂指針發送消息會引發程序崩潰。

對屬性進行復制會產生屬性的副本

在有些情況下,一個對象可能會希望對任何設置爲其屬性的對象,都複製一個只屬於它自己的副本。

例如,XYZBadgeView類的接口如圖3-4,代碼類似於:

@interface XYZBadgeView : NSView
@property NSString *firstName;
@property NSString *lastName;
@end

上面聲明瞭兩個NSString屬性,都對所指向的對象有着隱形的強引用。

現在思考一下如果有一個代表字符串的對象被創建出來,並被設置爲徽章視圖的屬性,如:

NSMutableString *nameString = [NSMutableString stringWithString:@"John"];
self.badgeView.firstName = nameString;

這是完全合法,因爲NSMutableString是NSString的子類。雖然徽章視圖認爲它是在處理一個NSString對象,但它其實是在處理一個NSMutableString對象。

這意味着字符串可以隨時被修改:

[nameString appendString:@"ny"];

這樣,儘管對於徽章視圖的firstName屬性原本設置的值是"John",由於字符串可變的緣故,現在變成了"Johnny"。

讀者可以讓徽章視圖擁有一份firstName和lastName屬性指向的實例變量的拷貝,這樣也就相當於在屬性被設置之時,字符串對象會被及時保存下來。通過向屬性添加copy特性重新聲明這兩個變量:

@interface XYZBadgeView : NSView
@property (copy) NSString *firstName;
@property (copy) NSString *lastName;
@end

現在視圖對象擁有了只屬於它自己的兩個字符串拷貝。即使可變字符串在城市設置之後隨後又被改變了,徽章視圖仍然擁有內容爲可變字符串初始值的字符串對象。例如:

NSMutableString *nameString = [NSMutableString stringWithString:@"John"];
self.badgeView.firstName = nameString;
[nameString appendString:@"ny"];

此時,徽章視圖的firstName屬性仍然是未受可變字符串改變影響的“John”字符串。

copy特性下的屬性擁有強引用,因爲屬性必須控制複製生成的對象。

注:若想把帶有copy特性的屬性指向一個對象,則這個對象壁紙支持NSCopying協議。協議在相關章節協議定義了消息發送的規則。若想了解更多關於NSCopying的信息,參見NSCopying參考或相關文章Advanced Memory Management Programming Guide

如果讀者需要直接設置帶有copy特性的屬性的實例變量,例如在一個初始化方法中,不要忘記對初始的對象進行復制操作:

- (id)initWithSomeOriginalString:(NSString *)aString {
    self = [super init];
    if (self) {
        _instanceVariableForCopyProperty = [aString copy];
    }
    return self;
}

自定義現有類

對象應該被賦予明確的任務,例如容納特定的信息,顯示視圖內容或者控制信息流。正如讀者所見,類的接口文件定義了其他對象如何與一個類對象互動,從而幫助其完成這些任務。

有時,你可能會發現你希望對現有類進行擴張,增加它的在某些情況下適用的行爲。例如,你覺得你的應用經常需要在交互界面上顯示一個字符串。相比於專門創建一個能夠繪製字符串在屏幕上的對象,倒不如給現有的NSString類添加功能,讓其可以在屏幕上繪製出自己。

諸如這樣的情況,並不總是需要向現存基本類的源代碼裏面整合進新的功能。以NSString對象爲例,繪圖功能對於大多數字符串對象來說都不是必須的,並且,你無法更改框架提供的類的接口和實現。

更進一步講,你可能也不想創建現存類的子類,因爲你希望繪圖功能不僅能夠在NSString類上實現,也能在它的子類裏實現,例如NSMutableString。並且,儘管NSString類能夠在OS X / iOS 上使用,但繪圖代碼在不同平臺上是不同的,這就導致在不同平臺上你需要創建不同的子類。

萬幸的是,Objective-C允許讀者在現存類的基礎之上通過範疇和類擴展添加自己的方法。

利用範疇想現有類添加方法

如果讀者需要想一個現有類添加方法,或許是爲了添加某些功能以便能夠在讀者自己的應用裏能夠更方便的使用,最簡單的方法就是使用範疇。

聲明範疇的語法是使用@interface關鍵字,這同標準的Objective-C類描述一樣,但不包括繼承的父類。而是應該在原本標明父類的位置寫上範疇名,入:

@interface ClassName (CategoryName)

@end

可以給任何一個類聲明範疇,即使你不知道這個類的實現源代碼(例如爲標準的Cocoa / Cocoa Touch 類聲明)。任何一個在範疇內聲明的方法對於其所在的類來說都是可用的,並且對於這個類的子類來說也都是可用的。在運行時,通過範疇添加的方法和類原本實現的方法沒有任何區別。

現在思考一下上一章的XYZPerson類,其有姓和名的屬性。如果讀者要編寫一個記錄軟件,那麼就會需要頻繁的現實一個人名列表,如:

Appleseed, John
Doe, Jane
Smith, Bob
Warwick, Kate

不需要每次都編寫代碼產生合適的姓名字符串,讀者可以給XYZPerson類添加一個範疇,如:

#import "XYZPerson.h"
@interface XYZPerson (XYZPersonNameDisplayAdditions)
- (NSString *)lastNameFirstNameString;
@end

在這個例子中,範疇XYZPersonNameDisplayAdditions聲明瞭一個額外的方法,以返回所需的字符串。

範疇通暢被聲明在一個淡出的.h文件中,並在一個單獨的.m文件中實現。對於XYZPerson類來說,讀者可以在一個名爲XYZPerson+XYZPersonNameDisplayAdditions.h的文件中來聲明。

即使由範疇添加的任何方法對於所有的類對象和子類對象都適用,但是如果讀者向在其他源代碼文件中使用範疇內聲明的方法,還是要將範疇的頭文件添加進源代碼文件,否則會收到編譯器的警告。

範疇的實現代碼類似於:

#import "XYZPerson+XYZPersonNameDisplayAdditions.h"

@implementation XYZPerson (XYZPersonNameDisplayAdditions)
  - (NSString *)lastNameFirstNameString {
      return [NSString stringWithFormat:@"%@, %@", self.lastName, self.firstName];
  }
@end

一旦讀者聲明瞭範疇,並實現了方法,就可以針對類的任何對象使用範疇內的方法,就好像類本身的方法一樣:

#import "XYZPerson+XYZPersonNameDisplayAdditions.h"
@implementation SomeObject
- (void)someMethod {
    XYZPerson *person = [[XYZPerson alloc] initWithFirstName:@"John"
                                                    lastName:@"Doe"];
    XYZShoutingPerson *shoutingPerson =
                        [[XYZShoutingPerson alloc] initWithFirstName:@"Monica"
                                                           lastName:@"Robinson"];
NSLog(@"The two people are %@ and %@",
[person lastNameFirstNameString], [shoutingPerson lastNameFirstNameString]);
} @end

除了向現存類添加方法以外,讀者還可以利用範疇來分割複雜類的實現,將多個方法按照使用目的和場合不同安置在多個原文件當中。例如,但一個類含有多種類型的方法時,比如幾何圖形計算,顏色調控,漸變控制等,非常複雜,此時就可以將一部分實現代碼(例如專門用來繪圖的那部分代碼)利用範疇單獨放置在一個文件中。而且,你可以利用範疇區分不同種類的實現代碼,例如同個代碼的OS X 和ISO版本。

範疇可以聲明類方法和實例方法,但很少會聲明額外的屬性。從語法上來講在範疇內聲明屬性是可行的,但聲明實例變量是行不通的。這意味着編譯器不會爲聲明在範疇內的屬性合成實例變量,也不會合成存取方法。讀者可以在範疇內編寫自己的存取方法,但是除非屬性的值在類的原文件中被聲明,讀者無法追蹤這個屬性的值。

唯一能夠像現存類添加屬性(含有實例變量的屬性)的方法是類擴展,詳見章節類擴展擴展了內部實現

注:在Cocoa / Cocoa Touch中,許多基礎類都有不止一個類擴展。事實上,本章介紹的字符串自我繪製功能,在OS X 中,已經有NSString的NSStringDrawing範疇實現,包括drawAtPoint:withAttributes: 和 drawInRect:withAttributes:方法。在iOS中,由UIStringDrawing實現,包括drawAtPoint:withFont: 和 drawInRect:withFont:方法。

避免範疇方法名衝突

由於範疇所聲明的方法會被添加至現有類,所以讀者要小心方法名衝突。

如果範疇內一個方法的名稱同類中原油的方法相同,或者說類不止有一個範疇,一個範疇中的一個方法的名稱和另一範疇內的一個方法的名稱相同(甚至說和一個子類的範疇的方法同名),在運行時哪個方法將會被調用就變的不確定。這在讀者針對自定義類使用範疇時發生的機率很小,但如果是針對Cocoa / Cocoa Touch
的類時,就會造成問題。

例如,有一個應用需要用到網頁的遠程服務,就需要對字符串進行基於Base64的編碼。可以針對NSString添加一個範疇,裏面定義一個可以返回Base64編碼版本的字符串,所以這個方法的名稱可以叫做base64EncodedString

這是如果另有一個框架內定了NSString類的範疇,恰巧還有一個名爲base64EncodedString的方法。在運行時,這兩個同名方法中,只有一個會被調用,但具體是哪一個並不明確。

另一個問題是如果讀者向Cocoa / Cocoa Touch內的類添加了便捷方法,在隨後的版本更新中,Apple將這些便捷方法正式加入了框架所提供的類中。以NSSortDescriptor類爲例,這個類描述了一個結合內的對象是怎樣排列的,一直有一個名爲initWithKey:ascending:的初始化方法,但在早期的OS X和ISO版本中,一直沒有提供相對應的工廠方法。

根據慣例,工廠方法應該被叫做sortDescriptorWithKey:ascending:;所以此時讀者可能選擇利用範疇自行爲其添加一個工廠方法。這在早期的OS X和iOS版本中沒有問題,但是隨着OS Xversion 10.6 and iOS 4.0的發佈,sortDescriptorWithKey:ascending:方法被添加到了NSSortDescriptor類的源代碼中,這意味着當你在這些平臺運行程序時,會發生衝突。

爲了避免這一現象發生,最好的解決方法是爲範疇內的方法名添加前綴,就好像給自定義的類添加前綴一樣。讀者可以選擇同自定義一樣的三個字母的前綴,但字母要小寫,以便遵循方法命名的慣例,然後在前綴和正式的方法名之間添加一個下劃線。以NSSortDescriptor爲例,自定義的範疇類似於:

@interface NSSortDescriptor (XYZAdditions)
+ (id)xyz_sortDescriptorWithKey:(NSString *)key ascending:(BOOL)ascending;
@end

這樣就可以保證方法在運行時能夠被正確調用。命名衝突的可能性被消除了:

NSSortDescriptor *descriptor =
[NSSortDescriptor xyz_sortDescriptorWithKey:@"name" ascending:YES];

類擴展擴展了內部實現

類擴展同範疇的作用類似,但只能用在編譯時已知源代碼的類上(類擴展和類將被一同編譯)。由類擴展聲明的方法將在類的@implementation部分被實現,所以你無法爲框架類聲明類擴展,諸如Cocoa / Cocoa Touch的NSString類。

聲明類擴展的語法同聲明範疇的語法相似,類似於:

@interface ClassName ()
@end

由於括號內總是爲空,所以類擴展又被稱爲匿名範疇.

同一般範疇不同,類擴展可以向類添加屬性和實例變量。如果你在類擴展中聲明瞭一個屬性,如:

@interface XYZPerson ()
@property NSObject *extraProperty;
@end

編譯器將會,在類的實現部分裏,自動合成相應的存取方法以及實例變量。

如果讀者向類擴展添加方法,則這些方法一定要在類的實現部分進行實現。

可以使用類擴展添加自定義的實例變量。這些實例變量要聲明在類擴展的大括號裏:

@interface XYZPerson () {
    id _someCustomInstanceVariable;
} ... @end

使用類擴展來隱藏私有信息

類的基礎接口是用來定義其他對象同類對象的互動方式的。換言之,它是類的公共接口。

類擴展通常被用來向類的公共接口添加額外的私有方法和屬性,這些方法和屬性僅供類的實現使用。例如,通常會在公共接口中將一個屬性定義爲只讀,而在實現代碼的上方的類擴展中將其定義爲讀寫。這是爲了讓內部方法可以直接修改屬性值。

以XYZPerson類爲例,可以向其添加一個名爲uniqueIdentifier的屬性,用來追蹤像社會保障號這樣的信息。

在現實世界中,給一個人分配一個社會保障好需要登記大量的檔案,所以XYZPerson類的公共接口就將其特定定義爲只讀,並且還要提供某個方法來爲對象分配識別符,如:

@interface XYZPerson : NSObject
  ...
@property (readonly) NSString *uniqueIdentifier;

- (void)assignUniqueIdentifier;
@end

這就意味着不能直接設置uniqueIdentifier的值。如果一個人還沒有識別符,就需要請求這個方法賦給其一個。

爲了能夠讓XYZPerson類在內部修改這個屬性,就可以在類擴展中再次聲明這個屬性(類擴展在實現代碼上部):

@interface XYZPerson ()
@property (readwrite) NSString *uniqueIdentifier;
@end
@implementation XYZPerson
...
@end

注:可以不用特意聲明可讀寫屬性,因爲它是默認值。但爲了強調期間,讀者可以將它明確的寫出。

這意味着編譯器將會自動合成存方法,所以XYZPerson類實現文件內的任何一個方法都可以利用存方法或點語法來直接設置這個屬性的值。

通過聲明在源代碼文件內部的類擴展,信息只在XYZPerson類內部流通,保持了私有。如果外部的其他類型的對象試圖直接設置屬性值,編譯器就會報錯。

注:通過添加上面的類擴展,重新將屬性uniqueIdentifier聲明爲可讀寫屬性,這樣,在運行時,一個名爲setUniqueIdentifier的存方法對於所有的XYZPerson類對象就會處於可用狀態,不管其它源代碼文件是否知曉這個類擴展的存在(即是否將含有類擴展的.m文件導入了其它源代碼文件)。

如果在其它源代碼文件中,有代碼試圖調用私有方法或直接設置只讀屬性,編譯器會發出警告。有辦法避免編譯器的警告,並且切可以利用運行時的動態特性來調用這些私有方法,例如使用NSObject類定義的performSelector方法。但是,在設計類的結構之初,讀者應該避免這種情況發生;總的來說,類的基礎接口所定義的內容應該總是被遵守。

如果讀者打算讓私有化方法或屬性可以被其他類所用,例如同一個框架中其它相關的類,可以在一個單獨的頭文件中聲明類擴展,,並在需要使用這些方法和屬性的源代碼文件中導入。對於一個類來說,很少會同時使用兩個頭文件,例如一個XYZPerson.h和一個XYZPersonPrivate。當你放出可用的框架頭文件時,只需要放出XYZPerson.h即可。

自定義現有類的其它方式

範疇和類擴展使得爲現有類添加行爲變的簡單,但是有些這並不是最佳的方式。

面向對象編程的基本要求之一是編寫可重複使用的代碼,這意味着類應該在各種環境下都能夠重複使用。如果讀者要編寫一個視圖類來描述一個能夠在屏幕上顯示顯示信息的對象,就應該思考一下這個類是否能夠滿足多個場合的需要。

與其絞盡腦汁去思考代碼的佈局和內容,不如利用繼承,將這些問題交給子類,用覆蓋方法的方式來爲特定的問題專門編寫代碼。不過這樣雖說讓類的使用變的簡單,但是讀者可能需要每次都相應的子類。

另一個解決方法是使用代理。任何會對類的適用性產生限制的功能都交由代理在運行時實現。一個常見的例子是標準的列表視圖類(NSTableView for OS X and UITableView for iOS)。爲了讓一個通用的列表視圖(一個可以利用行和列顯示信息的對象)具備可用性,可以讓視圖對象的代理來控制在運行時它所顯示的內容。代理機制將在下一張詳細介紹使用代理

利用Objective-C運行時的特性

Objective-C通過其運行時系統來實現它的動態特性。

許多決議,例如在發送消息時哪個方法會被調用,並不是在編譯時決定的,而是在應用運行的過程中決定的。Objective-C不僅僅只是一個可編譯至機器語言的編程語言。相反,它需要一個運行時系統來執行自己的代碼。

可以直接同運行時系統互動,例如想一個對象加入關聯引用associative reference。同類擴展不同,關聯引用並不影響類原本的聲明和擴展,這意味着你可以在不知曉源代碼的框架類上使用它們。

一個關聯引用將一個對象同另一個聯繫起來,它們之間的關係同屬性和實例變量的關係類似。要了解更多信息,參見關聯引用。學習更多關於Objective-C運行時的內容,參加相關文章Objective-C Runtime Programming Guide

使用協議

在真實的世界裏,政府部門在辦事時通暢會遵循一系列非常嚴格的程序。以執法部門爲例,正調查或收集證據時就必須遵守“章程“。

在面相對象編程的世界裏,定義一組一個對象在某個特定的情況下能夠執行的行爲是非常重要的。例如,一個列表視圖可以同一個數據元對象溝通,這樣它就可以得到要顯示的內容。這意味着數據源米需能夠迴應一組由視圖對象發送的特定的消息。

數據源可以是任何類的實例對象,例如視圖控制器類(NSViewController的子類 on OS X or UIViewController的子類 on iOS),或者一個繼承自NSObject類的專門作爲數據源的類。爲了讓列表視圖對象知道一個對象是否適合作爲數據源,這個對象就必須能夠實現必須的方法。

Objective-C允許讀者自定義協議,其中聲明的方法被指定用於一個特定情況。本章講講解定義正式語法的語法,以及怎樣在一個類的接口中讓其遵守協議,這意味着這個類必需實現協議中的必要方法。

協議定義了消息發送的規則

類接口中聲明的方法和屬性同這個類相關。而一個協議,相反,是用來聲明一組獨立於任何類的方法和屬性。

定義個一個協議的基本語法類似於:

@protocol ProtocolName
// list of methods and properties
@end

協議可以包括實例方法,類方法以及屬性的聲明。

例如,想象一個自定義的視圖子類,用來顯示一個餅狀圖,如圖5-1所示。

爲了儘可能的重複使用視圖,所有關於顯示信息內容的決議都被交由另一個對象負責,即數據源。這意味着同一個視圖類的多個對象可以顯示完全不同的內容,因爲他們的數據源各不相同。

顯示餅狀圖所需的最基本信息包括每個部分的數字,每個部分的相對大小,以及各自的標題。因此,餅狀圖的數據源協議,應該類似於:

@protocol XYZPieChartViewDataSource
- (NSUInteger)numberOfSegments;
- (CGFloat)sizeOfSegmentAtIndex:(NSUInteger)segmentIndex;
- (NSString *)titleForSegmentAtIndex:(NSUInteger)segmentIndex;
@end

注:上面的協議中使用NSUInteger來表示整數標量的值。這個類型將在下一章做進一步的討論。

餅狀圖的視圖類的接口需要一個屬性了追蹤數據源對象。這個對象可以屬於任意類,所以基本的屬性類型應該是id。關於這個對象唯一已知的是它遵守相關的協議。

聲明數據源屬性的語法類似於:

@interface XYZPieChartView : UIView
@property (weak) id <XYZPieChartViewDataSource> dataSource;
...
@end

Objective-C用尖括號來表示所遵守的協議。這個例子中聲明對一個指向通用類型對象的指針聲明瞭一個弱引用,並且其遵守XYZPieChartViewDataSource協議。

注:出於對象關係管理的需要,指向代理和數據源的屬性通暢都被聲明爲弱引用。詳見章節避免強引用循環

通過聲明屬性時指名其所遵守的協議,如果讀者視圖將屬性指向一個不遵守協議的對象,就會受到編譯器的警告,即使基本的屬性類型爲通用。屬性所指向的對象是UIViewController類還是NSObject類的實例對象並不重要。只要其遵守協議即可,這樣餅狀圖視圖就知道它可以請求需要顯示的信息。

協議可以有可選方法

  • 優點:1.Cateogies 2.Posing 3.動態識別 4.指標計算 5.彈性訊息傳遞 6.不是一個過度複雜的C衍生語言 7.Objective-C與C++可混合編程

  • 缺點:1.不支持命名空間 2.不支持運算符重載 3.不支持多重繼承 4.使用動態運行時類型,所有的方法都是函數調用,所以很多編譯時優化方法都用不到。(如內聯函數等),性能低劣。





文/fever105(簡書作者)
原文鏈接:http://www.jianshu.com/p/b6434c2997d1
著作權歸作者所有,轉載請聯繫作者獲得授權,並標註“簡書作者”。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章