1. 程式人生 > >多用塊列舉,少用for迴圈

多用塊列舉,少用for迴圈

原文連結:http://www.jianshu.com/p/f76e8c62a755
著作權歸作者所有,轉載請聯絡作者獲得授權,並標註“簡書作者”。
       注:本文來自《Effective Objective-C 2.0編寫高質量iOS 與 OS X程式碼的52個有效方法》,第47個方法,作者:Matt Galloway。

  在程式設計中經常需要列舉collection中得元素,在當前的Objective-C語言中又多重辦法實現此功能,可以用C語言迴圈,也可以用Objective-C 1.0中得NSEnumerator以及Objective-C 2.0的快速遍歷(fast enumeration)。在引入了“block”之後,又多出來了集中的的遍歷方式。下面詳細說明。 

for迴圈

    NSArray *anArray = /*...*/;
    for (int i = 0; i < anArray.count; i++)
    {
        id object = anArray[i];
        //Do something with 'object'
    }

  這麼寫還好,不過如果要遍歷字典或者set,就要複雜一點了:

    //Dictionary
    NSDictionary *aDictionary = /*...*/;
    NSArray *keys = [aDictionary allKeys];
    for (int i = 0
; i < keys.count; i++) { id key = keys[i]; id value = aDictionary[key]; //Do something with 'key' and 'value' }
    //Set
    NSSet *aSet = /*...*/;
    NSArray *objects = [aSet allObjects];
    for (int i = 0; i < objects.count; i++)
    {
        id object = objects[i];
        //Do something with 'object'
}

  字典與set都是無序的。所以無法根據特定的整數下表來直接訪問其中的值。於是,就需要先獲取字典裡的所有鍵或是set裡的所有物件,這兩種情況下,都可以在獲取到的有序陣列上遍歷,以便藉此訪問原字典及原set中得值。建立這個附加陣列會有額外的開銷,而且還會多建立一個數組物件,它會保留collection中得所有元素物件。當然了,釋放陣列時這些附加物件也要釋放,可以要呼叫本來不需要執行的方法。其它各種便利方式都無需建立這種中介陣列。
  for迴圈也可以實現反向遍歷,計數器的值從“元素個數減1”,每次迭代時遞減直到0為止。執行反向遍歷時,使用for迴圈會比其它方式簡單許多。
  用Objective-C 1.0中的 NSEnumerator 來遍歷NSEnumerator 是個抽象基類,其中只定義了兩個方法,供其具體子類來實現:

-(NSAraay *)allObjects;
- (id)nextObject;

  其中關鍵的方法是nextObject,它返回列舉物件裡的下個物件。每次呼叫該方法時,其內部的資料結構都會更新,使得下次呼叫方法時能返回下一個物件。等到列舉中得全部物件都已返回之後,再呼叫就將返回nil,這表示達到列舉末端了。
  Foundation框架中內建的collection類都實現了這種遍歷方式。例如,想遍歷陣列,可以這樣寫程式碼:

    NSArray *anArray = /* ... */;
    NSEnumerator *enumerator = [anArray objectEnumerator];
    id object;
    while ((object = [enumerator nextObject]) != nil)
    {
        // Do something with 'object'
    }

  這種寫法的功能與標準的for迴圈相似,但是程式碼卻多了一些。其真正優勢在於:不論遍歷哪種collection,都可以採用這套相似的語法。比方說,遍歷字典及set時也可以按照這種寫法來做:

    // Dictionary
    NSDictionary *aDictionary = /* ... */;
    NSEnumerator *enumerator = [aDictionary keyEnumerator];
    id key;
    while ((key = [enumerator nextObject]) != nil)
    {
        id value = aDictionary[key];
        // Do something with 'key' and 'value'
    }
    // Set
    NSSet *aSet = /* ... */;
    NSEnumerator *enumerator = [aSet objectEnumerator];
    id object;
    while ((object = [enumerator nextObject]) != nil)
    {
        // Do something with 'object'
    }

  遍歷字典的方式與陣列和set略有不同,因為字典裡既有鍵也有值,所以要根據給定的鍵把對應的值提取出來。使用NSEnumerator 還有個好處,就是有多種“列舉器”(enumerator)可供使用。比方說,有反向遍歷陣列所用的列舉器,如果拿它來遍歷,就可以按反向來迭代collection中得元素了。例如:

NSArray *anArray = /* ... */;
    NSEnumerator *enumerator = [anArray reverseObjectEnumerator];
    id object;
    while ((object = [enumerator nextObject]) != nil)
    {
        // Do something with 'object'
    }

  與採用for 迴圈的等效寫法相比,上面這段程式碼讀起來更順暢。

快速遍歷

  Objective-C 2.0引入了快速遍歷這一功能。快速遍歷與使用NSEnumerator來遍歷差不多,然而語法更簡潔,它為for迴圈開設了in關鍵字。這個關鍵字大幅簡化了遍歷collection所需的語法,比方說要遍歷陣列,就可以這麼寫:

    NSArray *anArray = /* ... */;
    for (id object in anArray)
    {
        // Do something with 'object'
    }

  這樣寫簡單多了。如果某個類的物件支援快速遍歷,那麼就可以宣稱自己遵從名為NSFastEnumeration的協議,從而令開發者可以採用此語法來迭代該物件。此協議只定義了一個方法:

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
                                  objects:(id*)stackbuffer
                                    count:(NSUInteger)length;

  該方法的工作原理不在本條目所述方位內。不過網上能找到一些優秀的教程,它們會把這個問題解釋的很清楚,。其要點在於:該方法允許類例項同時返回多個物件,這就使得迴圈遍歷操作更為高效了。
  遍歷字典與set也很簡單:

    // Dictionary
    NSDictionary *aDictionary = /* ... */;
    for (id key in aDictionary)
    {
        id value = aDictionary[key];
        // Do something with 'key' and 'value'
    }
    // Set
    NSSet *aSet = /* ... */;
    for (id object in aSet)
    {
        // Do something with 'object'
    }

  由於NSEnumerator物件也實現了NSFastEnumeration協議,所以能用來執行反向遍歷陣列,可採用下面這種寫法:

    NSArray *anArray = /* ... */;
    for (id object in [anArray reverseObjectEnumerator])
    {
        // Do something with 'object'
    }

  在目前所介紹的遍歷方式中,這種辦法是語法最簡單且效率最高的,然而如果在遍歷字典時需要同時獲取鍵與值,那麼會多出來一步。而且,與傳統for迴圈不同,這種遍歷方式無法輕鬆獲取當前遍歷操作所針對的下標。遍歷時通常會用到這個下標,比如很多演算法都需要它。

基於block的遍歷方式

  在當前的Objective-C 語言中美最新引入的一種做法就是基於block來遍歷。NSArray中定義了下面這個方法,它可以實現最基本的遍歷功能:

- (void)enumerateObjectsUsingBlock:(void(^)(id object, NSUInteger idx, BOOL *stop))block;

  除此之外還有一些列類似的遍歷方法,它們可以接受各種選項,以控制遍歷操作,稍後將會討論那些方法。
  在遍歷陣列及set時,每次迭代都要執行由block引數所傳入的快,這個塊有三個引數,分別是當前迭代所針對的物件、所針對的下標,以及指向布林值的指標。前兩個引數的含義不言而喻。而通過第三個引數所提供的機制,開發者可以終止遍歷操作。
  例如,下面這段程式碼用此方法來遍歷陣列:

    NSArray *anArray = /* ... */;
    [anArray enumerateObjectsUsingBlock:^(id object, NSUInteger idx, BOOL *stop)
     {
         // Do something with 'object'
         if (shouldStop)
         {
             *stop = YES;
         }
     }];

  這種寫法稍微多了幾行程式碼,但是依然清晰明瞭,而且遍歷時既能獲取物件,也能知道其下標。此方法還提供了一種優雅的機制,用於終止遍歷操作,開發者可以通過設定stop變數值來實現,當然,使用其它幾種遍歷方式時,也可以通過break來終止迴圈,那樣做也很好。
  此方式不僅可用來遍歷陣列。NSSet裡面也有同樣的塊列舉方法,NSDictionary也是這樣,只是略有不同:

- (void)enumerateKeysAndObjectsUsingBlock:(void(^)(id key, id object, BOOL *stop))block;

  因此,遍歷字典與set也同樣簡單:

    // Dictionary
    NSDictionary *aDictionary = /* ... */;
    [aDictionary enumerateKeysAndObjectsUsingBlock:^(id key, id object, BOOL *stop)
     {
         // Do something with 'key' and 'object'
         if (shouldStop)
         {
             *stop = YES;
         }
     }];
    // Set
    NSSet *aSet = /* ... */;
    [aSet enumerateObjectsUsingBlock:^(id object, BOOL *stop)
     {
         // Do something with 'object'
         if (shouldStop)
         {
             *stop = YES;
         }
     }];

  此方式大大勝過方式的地方在於:遍歷時可以直接從block裡獲取更多資訊。在遍歷陣列時,可以知道當前所針對的下標。遍歷有序set(NSOrderedSet)時也一樣。而在遍歷字典時,無須額外編碼,即可同事獲取鍵與值,因而省去了根據給定鍵來獲取對應值這一步。用這種方式遍歷字典,可以同事得知鍵與值,這很可能比其他方式快很多,因為在字典內部的資料結構中,鍵與值本來就是儲存在一起的。
  另外一個好處是,能夠修改block的方法名,以免進行型別轉換的操作,從效果上講,相當於把本來需要執行的型別轉換操作交給block方法簽名來做。比方說,要用“快速遍歷法”來遍歷字典。若已知字典中得物件必為字串,則可以這樣編碼:

    NSDictionary *aDictionary = /* ... */;
    for (NSString *key in aDictionary)
    {
        NSString *object = (NSString*)aDictionary[key];
        // Do something with 'key' and 'object'
    }

  如果改用基於block的方式來遍歷,那麼就可以在block方法簽名中直接轉換:

    [aDictionary enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop)
     {
         // Do something with 'key' and 'obj'
     }];

  之所以能如此,是因為id型別相當特殊,它可以像本例這樣,為其他型別所覆寫。要是原來的block簽名把鍵與值都定義成NSObject *,那麼些就不行了。此技巧出刊不甚顯眼,實則相當有用。指定物件的精確型別之後,編譯器就可以檢測出開發者是否呼叫了該物件所不具備的方法,並在發現這種問題時報錯。如果能夠確知某collection裡的物件是什麼型別,那就應該使用這種方法指明其型別。
  用此方式也可以執行反向遍歷。陣列、字典、set都實現了前述方法的另一個版本,使開發者可向其傳入“選項掩碼”(option mask):

- (void)enumerateObjectsWithOptions:(NSEnumerationOptions)options
                         usingBlock:(void(^)(id obj, NSUInteger idx, BOOL *stop))block;

- (void)enumerateKeysAndObjectsWithOptions:(NSEnumerationOptions)options
                                usingBlock: (void(^)(id key, id obj, BOOL *stop))block;

  NSEnumerationOptions型別是個enum,其各種取值可用“按位或”(bitwise OR)連線,用以表明遍歷方式。例如,開發者可以請求以冰法方式執行各輪迭代,也就是說,如果當前系統資源狀況允許,那麼執行每次迭代所用的block就可以並行執行了。通過NSEnumerationConcurrent選項即可開啟此功能。如果使用此選項,那麼底層會通過GCD來處理冰法執行事宜,具體實現時很可能會用到dispatch group。不過,到底如何來實現,不是本條索要討論的內容。反向遍歷是通過 NSEnumerationReverse選項來實現的。要注意:只有遍歷陣列或有序set等有順序的collection時,這麼做才有意義。
  總體來看,block列舉法擁有其他遍歷方式都具備的優勢,而且還能帶來更多好處。與快速遍歷法相比,它要多用一些程式碼,可是卻能提供遍歷時所針對的下標,在,在遍歷字典時也能同時提供鍵與值,而且還有選項可以開啟併發迭代功能,所以多寫這點程式碼還是值得的。

要點

  ■遍歷collection有四種方式。最基本的方法是for迴圈,其次是NSEnumerator遍歷法及快速遍歷法,最新最先進的的方式則是“block列舉法”。
  ■“block列舉法”本身就能通過GCD來併發執行遍歷操作,無需另行編寫程式碼。而採用其他遍歷方式則無法輕易實現這一點。
  ■若提前知道待遍歷的collection含有何種物件,則應修改block簽名,指出具體型別。