Objective-C——在Cocoa Touch框架中使用迭代器模式

出版圈郭志敏發表於2012-03-27

蘋果公司用自己的命名規則“列舉器/列舉”改寫了迭代器模式,用於相關基礎類的各種方法。從現在開始,我將使用“列舉”一詞,它就是蘋果版本的“迭代”。在這個模式中它們是一個意思。基礎框架中的NSEnumerator類實現了迭代器模式。抽象NSEnumerator類的私有具體子類返回列舉器物件,能夠順序遍歷各種集合——陣列、集(set)、字典(值與鍵),把集合中的物件返回給客戶端。

NSDirectoryEnumerator是個關係較遠的類。這個類的例項遞迴列舉檔案系統中一個目錄的內容。

NSArray、NSSet和NSDictionary這樣的集合類,定義了返回與集合的型別相應的NSEnumerator子類例項的方法。所有的列舉器都以同樣的方式工作。可以在一個迴圈中向列舉器傳送nextObject訊息,從列舉器取得物件,直到它返回nil表示遍歷結束。

NSEnumerator

從iOS 2.0開始,可以使用NSEnumerator來列舉NSArray、NSDictionary和NSSet物件中的元素。NSEnumerator本身是個抽象類。它依靠幾個工廠方法(見工廠方法模式,第4章),如objectEnumerator或keyEnumerator,來建立並返回相應的具體列舉器物件。客戶端用返回的列舉器物件遍歷集合中的元素,如下面的程式碼段所示。

NSArray *anArray = ... ; 
NSEnumerator *itemEnumerator = [anArray objectEnumerator]     
NSString *item;
while (item = [itemEnumerator nextObject])
{ 
  // 對item作些處理
}

假設anArray儲存著一些NSString物件。在while迴圈中用NSString的方法對每個item進行處理。

當陣列的內容被取完之後,訊息呼叫[itemEnumerator nextObject]會返回nil,然後列舉過程就結束了。

從iOS 4開始,有了另一種列舉Cocoa Touch框架中集合物件的方法,它叫做基於塊的列舉。

基於塊的列舉

在iOS 4中為Cocoa Touch框架中的集合物件引入了基於塊的列舉(Block-Based Enumeration)。塊是Objective-C的一項語言功能(本書寫作時,蘋果公司還在爭取把塊作為對C語言的擴充套件而標準化)。塊是一種型別化的函式,就是說塊是函式也是型別。定義好的塊是一個可在方法呼叫之間傳遞的變數,就跟物件中的其他變數一樣。同時,塊變數在方法中可作為函式使用。當把塊作為引數傳遞給方法時,塊可以像C程式中的函式指標那樣被用作回撥函式。因此塊正適合於實現內部迭代器(列舉器)。客戶端不再需要手動生成迭代器,只需要提供一個符合目標集合物件所要求的簽名的塊。然後塊將在每個遍歷步驟中被呼叫。在每次塊被目標集合物件呼叫時,定義塊的演算法可以對返回的元素進行處理。

塊是Objective-C語言中很酷的一項功能。它讓我們可以把回撥演算法的定義內嵌在訊息呼叫之中。如果不使用塊,在Cocoa Touch框架中實現“回撥”的傳統方式是使用委託(見介面卡模式,第8章)。需要為要響應客戶端回撥的所有物件(介面卡)單獨定義一個協議(目標)。要是應用程式的這個部分複雜到需要另外的介面卡機制的程度,那也未嘗不可。有時塊可以提供一種比列舉器更漂亮的解決方案。

在iOS 4中,蘋果公司在NSArray、NSDictionary和NSSet物件中引入了新方法,用於基於塊的列舉。其中一個方法叫enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block。我們可以把自己的演算法定義在內嵌到訊息呼叫之中的塊裡,或者在別的什麼地方預先定義一個塊,然後作為引數傳給訊息呼叫。下面的程式碼段通過一個NSArray物件演示了它是如何在程式碼中實現的。

NSArray *anArray=[NSArray arrayWithObjects:@"This", @"is", @"a", @"test", nil]; 
NSString *string=@"test";
[anArray enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop)
{
  if([obj localizedCaseInsensitiveCompare:string] == NSOrderedSame)
  {
    // 對返回的obj做點別的事情
  *stop=YES;

  }

要是anArray物件中有個單詞是@"test",那麼就把指標*stop設定為YES,以通知anArray物件提前停止列舉。塊除了id obj和BOOL *stop引數,還有一個NSUInteger index引數。index引數讓塊中的演算法知道當前元素的位置,這對這樣的併發列舉非常有用。要是沒有這個引數,訪問索引的唯一方式就是使用indexOfObject:方法,這樣影響效率。

NSSet物件中基於塊的列舉與NSArray物件中的非常類似,只是在塊簽名中沒有index引數。NSSet物件是一種模擬“集合”(set)的資料結構,集合中的元素沒有表示元素在結構中位置的索引。

使用NSArray、NSDictionary和NSSet的內部迭代器的一個重要好處是,處理其內容的演算法可在其他地方由其他開發人員來定義。與傳統的for迴圈中定義的演算法不同,定義清晰的塊可被複用。當塊逐漸變大時,可把它們放到單獨的實現檔案中,不跟其他程式碼擠在一起。雖然塊是一種為複雜的事物新增內聯演算法的方便途徑,無需定義單獨的委託協議,但是當塊過大而難以維護時,應該考慮使用策略模式(第19章)。

快速列舉

Objective-C 2.0提供了一種列舉,稱為快速列舉。它是蘋果公司推薦的列舉方法。它允許把對集合物件的列舉直接用作for迴圈的一部分,無需使用其他列舉器物件,而且比傳統的基於索引的for迴圈效率更高。快速列舉的語法如下。

NSArray * anArray = ... ;
for (NSString * item in anArray)
{
  // 對item作些處理
}

現在列舉迴圈使用指標運算(pointer arithmetic),讓它比使用NSEnumerator的標準方法效率更高。

要利用快速列舉,集合類需要實現NSFastEnumeration協議,以向執行庫提供關於集合的必要資訊。基礎框架中的所有集合類與NSEnumerator類都支援快速列舉。因此不必使用while迴圈從NSEnumerator列舉每個元素,直到nextObject返回nil,我們可以使用其快速列舉的版本,如下面的程式碼段所示。

NSArray * anArray = ... ;
NSEnumerator * itemEnumerator = [anArray objectEnumerator];
for (NSString * item in itemEnumerator)
{
  // 對item作些處理 
}

雖然既可以使用集合物件的快速列舉,也可以使用列舉器的快速列舉,但如果只需要預設遍歷(通常只按升序),直接對集合物件進行快速列舉更為合理。NSEnumerator使用其nextObject方法實現NSFastEnumeration協議。從效能上說,它比直接在while迴圈中手動呼叫這個方法好不了多少。儘管跟傳統的使用nextObject的while迴圈相比,快速列舉中的for迴圈顯得更為整潔。

實現NSFastEnumeration不在本書的範圍,所以不在此討論它。

內部列舉

NSArray有個例項方法叫(void)makeObjectsPerformSelector:(SEL)aSelector,它允許客戶端向陣列中每個元素髮送一個訊息,讓每個元素執行指定的aSelector(假定元素支援它)。可以用前面提到的任何一種列舉方法讓每個元素執行相同的選擇器,達到相同的目的。這個方法在內部列舉集合並向每個元素髮送performSelector:訊息。這種方式的缺點是如果集合中任何元素不響應選擇器,就會丟擲異常。因此它主要適用於不需太多執行時檢查的簡單操作。

相關文章