天天看點

ios-block swift-blockiOS block的用法

iOS block的用法

本文來自台灣的某開發人員的部落格,被牆,感覺講的比較易懂,是以引過來。文字簡體化了,原來是繁體,變數=變量,這個注意一下。

本文的順序是層層深入的,要想簡單的了解,隻看X.1 初探Block就好了

本章學習目标:

1. 了解何謂block。

2. 了解block的使用方法。

Block 是iOS在4.0之後新增的程式文法,嚴格來說block的概念并不算是基礎程式設計的範圍,對初學者來說也不是很容易了解,但是在iOS SDK 4.0之後,block幾乎出現在所有新版的API之中,換句話說,如果不了解block這個概念就無法使用SDK 4.0版本以後的新功能,是以雖然block本身的文法有點難度,但為了使用iOS的新功能我們還是得硬着頭皮去了解這個新的程式概念。

在這一章的目标以了解如何使用block為主而不深入探讨block底層的運作方式,至于有些初學者較少遇到的辭彙如「詞法作用域(lexical scope)」等,本章将不再多做解釋,待有興趣的讀者去請教Google大神吧。

X.1 初探Block

在這一小節我們先用一些簡單範例來導入block的概念。

X.1.1 宣告和使用Block

我們使用「^」運算子來宣告一個block變數,而且在block的定義最後面要加上「;」來表示一個完整的述句(也就是将整個block定義視為前面章節所介紹的簡單述句,因為整個定義必須是一個完整的句子,是以必須在最後面加上分号),下面是一個block的範例:

1: int multiplier = 7 ;      
2: int (^myBlock)( int ) = ^( int num)      
3: {      
4:     return num * multiplier;      
5: };      

我們使用下圖來解釋這個範例(請将文字框的字翻譯如下):

我們宣告一個「myBlock」變數,用「^」符号來表示這是一個block。

這是block的完整定義,這個定義将會指定給「myBlock」變數。

表示「myBlock」是一個回傳值為整數(int)的block。

它有一個參數,型态也是整數。

這個參數的名字叫做「num」。

這是block的内容。

值得注意的地方是block可以使用和本身定義範圍相同的變數,可以想像在上面的例子中 multiplier 和 myBlock 都是某一個函數内定義的兩個變數也就是這個變數都在某個函數兩個大括号「{」和「 }」中間的區塊,因為它們的有效範圍是相同的,是以在block中就可以直接使用 multiplier 這個變數,此外當把block定義成一個變數的時,我們可以直接像使用一般函數般的方式使用它:

1: int multiplier = 7 ;      
2: int (^myBlock)( int ) = ^( int num)      
3: {      
4:     return num * multiplier;      
5: };      
6: printf ( "%d" , myBlock( 3 ));      
7: //結果會列印出21      

X.1.2 直接使用Block

在很多情況下,我們并不需要将block宣告成變數,反之我們可以直接在需要使用block的地方直接用内嵌的方式将block的内容寫出來,在下面的例子中qsort_b函數,這是一個類似傳統的qsort_t函數,但是直接使用block做為它的參數:

1: char *myCharacters[ 3 ] = { "TomJohn" , "George" , "Charles Condomine" };      
2: qsort_b (myCharacters, 3 ,      
3:          sizeof ( char *),      
4:          ^( const void *l, const void *r)//block部分      
5:             {      
6:                 char *left = *( char **)l;      
7:                 char *right = *( char **)r;      
8:                 return strncmp (left, right, 1 );      
9:             }                            //end      
10: );      

X.1.3 __block 變量

一般來說,在block内隻能讀取在同一個作用域的變數而且沒有辦法修改在block外定義的任何變數,此時若我們想要這些變數能夠在block中被修改,就必須在前面挂上__block的修飾詞,以上面第一個例子中的 multiplier 來說,這個變數在 block 中是唯讀的,是以 multiplier = 7 指定完後,在 block 中的 multiplier 就隻能是 7 不能修改,若我們在 block 中修改 multiplier ,在編輯時就會産生錯誤,是以若想要在 block 中修改 multiplier ,就必須在 multiplier 前面加上 __block 的修飾詞,請參考下面的範例:

1: __block int multiplier = 7 ;      
2: int (^myBlock)( int ) = ^( int num)      
3:                         {      
4:                             if (num > 5 )      
5:                             {      
6:                                   multiplier = 7 ;      
7:                             }      
8:                             else      
9:                             {      
10:                                   multiplier = 10 ;      
11:                             }      
12:                             return num * multiplier;      
13:                         };      

X.2 Block 概要

Block 提供我們一種能夠将函數程式碼内嵌在一般述句中的方法,在其他語言中也有類似的概念稱做「closure」,但是為了配合Objective-C的貫例,我們一律将這種用法稱為「block」

X.2.1 Block 的功能

Block 是一種具有匿名功能的内嵌函數,它的特性如下:

如一般的函數般能擁有帶有型态的參數。

擁有回傳值。

可以撷取被定義的詞法作用域(lexical scope)狀态。

可以選擇性地修改詞法作用域的狀态。

注:詞法作用域(lexical scope)可以想像成是某個函數兩個大括号中間的區塊,這個區塊在程式執行時,系統會将這個區塊放入堆疊記憶體中,在這個區塊中的宣告的變數就像是我們常聽到的區域變數,當我們說block可以撷取同一詞法作用域的狀态時可以想像block變數和其他區域變數是同一個層級的區域變數(位于同一層的堆疊裡),而block的内容可以讀取到和他同一層級的其他區域變數。

我們可以拷貝一個block,也可以将它丢到其他的執行緒中使用,基本上雖然block在iOS程式開發中可以使用在C/C++開發的程式片段,也可以在Objective-C中使用,不過在系統的定義上,block永遠會被視為是一個Objective-C的物件。

X.2.2 Block 的使用時機

Block 一般是用來表示、簡化一小段的程式碼,它特别适合用來建立一些同步執行的程式片段、封裝一些小型的工作或是用來做為某一個工作完成時的回傳呼叫(callback) 。

在新的iOS API中block被大量用來取代傳統的delegate和callback,而新的API會大量使用block主要是基于以下兩個原因:

可以直接在程式碼中撰寫等會要接着執行的程式,直接将程式碼變成函數的參數傳入函數中,這是新API最常使用block的地方。

可以存取區域變數,在傳統的callback實作時,若想要存取區域變數得将變數封裝成結構才能使用,而block則是可以很友善地直接存取區域變數。

X.3 宣告和建立Block

X.3.1 宣告Block的參考(Reference)

Block 變數儲存的是一個block的參考,我們使用類似宣告名額的方式來宣告,不同的是這時block變數指到的地方是一個函數,而名額使用的是「*」,block則是使用「^」來宣告,下面是一些合法的block宣告:

1: /* 回傳void ,參數也是void 的block*/      
2: void (^blockReturningVoidWithVoidArgument)( void );      
3: /* 回傳整數,兩個參數分别是整數和字元型态的block*/      
4: int   (^blockReturningIntWithIntAndCharArguments)( int , char );      
5: /* 回傳void ,含有10 個block 的陣列,每個block 都有一個型态為整數的參數*/      
6: void (^arrayOfTenBlocksReturningVoidWinIntArgument[ 10 ])( int );      
7: X.3.2 建立一個Block       
8:        
9: 我們使用「^」來開始一個block,并在最後使用「;」來表示結束,下面的範例示範了一個block變數,然後再定義一個block把它指定給block變數:       
10:        
11: int (^oneFrom)( int ); /* 宣告block 變數*/      
12:     /* 定義block 的内容并指定給上面宣告的變數*/      
13:     oneFrom = ^(int anInt)      
14:                 {      
15:                     return anInt = - 1 ;       
16:                 };      

X.3.3 全域的Block

我在可以在檔案中宣告一個全域的block,請參考以下範例:

1: int GlobalInt = 0 ;      
2: int (^getGlobalInt)( void ) = ^ ( void ) { return GlobalInt ;};      

X.4 Block 和變量

接下來的這一小節我們将會介紹block和變數之間的互動。

X.4.1 變數的型态

我們可以在block中遇到平常在函數中會遇到的變數類型:

l 全域(global)變數或是靜态的區域變數(static local)。

l 全域的函數。

l 區域變數和由封閉領域(enclosing scope)傳入的參數。

除了上述之外block額外支援了另外兩種變數:

在函數内可以使用__block 變數,這些變數在block中是可被修改的。

彙入常數(const imports)。

此外,在方法的實作裡,block可以使用Objective-C的實體變數(instance variable)。

下列的規則可以套用到在block中變數的使用:

可以存取全域變數和在同一領域(enclosing lexical scope)中的靜态變數。

可以存取傳入block的參數(使用方式和傳入函數的參數相同)。

在同一領域的區域變數在block中将視為常數(const)。

可以存取在同一領域中以__block 為修飾詞的變數。

在block中宣告的區域變數,使用方式和平常函數使用區域變數的方式相同。

下面的例子介紹了區域變數(上述第三點)的使用方式:

1: int x = 123 ;      
2: void (^printXAndY)( int ) = ^( int y)      
3:     {      
4: printf ( "%d %d\n" , x, y);       
5:     };      
6: // 将會印出123 456      
7:     printXAndY( 456 );      
8: 就如上面第三點所提到的,在上例中的int x = 123的變量x,在傳入block後将視同常數,是以若我們在block中試着去修改x的值時就會産生錯誤,下面的例子将會無法通過編譯:       
9:        
10: int x = 123 ;      
11: void (^printXAndY)( int ) = ^( int y)      
12: {      
13:     // 下面這一行是錯的,因為x 在這是一個常數不能被修改。      
14:     x = x + y;      
15:     printf ( "%d %d\n" , x, y);       
16:     };      

若在block中想要修改上面的變數x,必須将x宣告加上修飾詞__block,請參考接下來這一小節的介紹。

X.4.2 __block 型态變數

我們可以藉由将一個由外部彙入block的變數放上修飾詞__block來讓這個變數由唯讀變成可以讀和寫,不過有一個限制就是傳入的變數在記憶體中必須是一個占有固定長度記憶體的變數,__block修飾詞無法使用于像是變動長度的陣列這類不定長度的變數,請參考下面的範例:

1: // 加上__block 修飾詞,是以可以在block 中被修改。      
2: __block int x = 123 ;      
3: void (^printXAndY)( int ) = ^( int y)      
4:     {      
5:         x = x + y;       
6: printf ( "%d %d\n" , x, y);       
7:     };      
8: // 将會印出579 456      
9:     printXAndY( 456 );      
10: //x 将會變成 579;      
11: 下面我們使用一個範例來介紹各類型的變數和block之間的互動:       
12:        
13: extern NSInteger CounterGlobal;      
14: static NSInteger CounterStatic;      
15: {      
16: NSInteger localCounter = 42 ;      
17: __block char localCharacter;      
18: void (^aBlock)( void ) = ^( void )      
19:     {      
20:         ++ CounterGlobal ; //可以存取。      
21:         ++ CounterStatic ; //可以存取。       
22: CounterGlobal = localCounter; //localCounter在block 建立時就不可變了。      
23:         localCharacter = 'a' ; //設定外面定義的localCharacter 變數。      
24:     };      
25:     ++localCounter; //不會影響的block 中的值。      
26:     localCharacter = 'b' ;      
27:     aBlock(); //執行block 的内容。      
28: //執行完後,localCharachter 會變成'a'      
29: }      

X.4.3 物件和Block變數

Block 支援在Objective-C、C++物件和其他block中當作變數來使用,不過因為在大部分的情況我們都是使用Objective-C的撰寫程式,是以在這一小節我們僅針對Objective-C的情況進行介紹,至于其他兩種情況就留給有興趣的讀者再自行深入研究了。

x.4.3.1 Objective-C 物件

在擁有參考計數(reference-counted)的環境中,若我們在block中參考到Objective-C的物件,在一般的情況下它将會自動增加物件的參考計數,不過若以__block為修飾詞的物件,參考計數則是不受影響。

如果我們在Objective-C的方法中使用block時,以下幾個和記憶體管理的事是需要額外注意的:

l 若直接存取實體變數(instance variable),self的參考計數将被加1。

l 若透過變數存取實體變數的值,則隻變數的參考計數将被加1。

以下程式碼說明上面兩種情況,在這個假設instanceVariable是實體變數:

1: dispatch_async (queue, ^{      
2: // 因為直接存取實體變數instanceVariable ,是以self 的retain count 會加1      
3: doSomethingWithObject (instanceVariable);      
4:     });      
5: id localVaribale = instanceVariable;      
6: dispatch_async (queue, ^{      
7: //localVariable 是存取值,是以這時隻有localVariable 的retain count 加1      
8: //self 的 return count  并不會增加。      
9: doSomethingWithObject (localVaribale);      
10:     });      

X.5 使用Block

這一小節我們将會對block的使用方式做一些初步的介紹

X.5.1 呼叫一個Block

當block宣告成一個變數時,我們可以像使用一般函數的方式來使用它,請參考下面兩個範例:

1: int (^oneFrom)( int ) = ^( int anInt) {      
2: return anInt - 1 ;      
3:     };      
4: printf ( "1 from 10 is %d" , oneFrom( 10 ));      
5: //結果會顯示:1 from 10 is 9      
6: float (^distanceTraveled)( float , float , float ) = ^( float startingSpeed, float acceleration, float time)      
7:     {      
8: float distance = (startingSpeed * time) + ( 0.5 * acceleration * time * time);      
9: return distance;      
10:     };      
11: float howFar = distanceTraveled( 0.0 , 9.8 , 1.0 );      
12: //howFar會變成4.9      

在一般常見的情況中,若是将block當做是參數傳入函數,我們通常會使用「内嵌」的方式來使用block。

X.5.2 将Block當作函數的參數

我們可以像使用一般函數使用參數的方式,将block以函數參數的型式傳入函數中,在這種情況下,大多數我們使用block的方式将不會傾向宣告block而是直接以内嵌的方式來将block傳入,這也是目前新版SDK中主流的做法,我們将補充前面章節的例子來說明:

1: char *myCharacters[ 3 ] = { "TomJohn" , "George" , "Charles Condomine" };      
2: qsort_b (myCharacters, 3 , sizeof ( char *),      
3:             ^( const void *l, const void *r)      
4:             {      
5: char *left = *( char **)l;      
6: char *right = *( char **)r;      
7: return strncmp (left, right, 1 );      
8:             } // 這裡是block 的終點。      
9:             );      
10: // 最後的結果為:{"Charles Condomine", "George", "TomJohn"}      
在上面的例子中,block本身就是函數參數的一部分,在下一個例子中dispatch_apply函數中使用block,dispatch_apply的定義如下:
1: void      
2: dispatch_apply( size_t iterations, dispatch_queue_t queue, void (^block)( size_t ));      
3: 這個函數将一個block送出到發送伫列(dispatch queue)中來執行多重的呼叫,隻有當伫列中的工作都執行完成後才會回傳,這個函數擁有三個變數,而最後一個參數就是block ,請參考下面的範例:       
4:        
5: size_t count = 10 ;      
6: dispatch_queue_t queue =      
7: dispatch_get_global_queue ( DISPATCH_QUEUE_PRIORITY_DEFAULT , 0 );      
8: dispatch_apply (count, queue, ^( size_t i) {      
9: printf ( "%u\n" , i);      
10:     });      

X.5.3 将Block當作方法的參數

在SDK中提供了許多使用block的方法,我們可以像傳遞一般參數的方式來傳遞block,下面這個範例示範如何在一個陣列的前5筆資料中取出我們想要的資料的索引值:

1: // 所有的資料      
2: NSArray *array = [ NSArray arrayWithObjects : @"A" , @"B" , @"C" , @"A" , @"B" , @"Z" , @"G" , @"are" , @" Q" ,nil ];         
3: // 我們隻要這個集合内的資料      
4: NSSet *filterSet = [ NSSet setWithObjects : @"A" , @"B" , @"Z" , @"Q" , nil ];      
5: BOOL (^test)( id obj, NSUInteger idx, BOOL *stop);      
6: test = ^ ( id obj, NSUInteger idx, BOOL *stop) {      
7: // 隻對前5 筆資料做檢查      
8: if (idx < 5 ) {      
9: if ([filterSet containsObject : obj]) {      
10: return YES ;      
11:               }      
12:       }      
13: return NO ;      
14: };      
15: NSIndexSet *indexes = [array indexesOfObjectsPassingTest :test];      
16: NSLog ( @"indexes: %@" , indexes);         
17: // 結果:indexes: <NSIndexSet: 0x6101ff0>[number of indexes: 4 (in 2 ranges), indexes: (0-1 3-4)]      
18: // 前5筆資料中,有4筆符合條件,它們的索引值分别是0-1, 3-4      

X.5.4 該避免的使用方式

在下面的例子中,block是for回圈的區域變數是以在使用上必須避免将區域的block指定給外面宣告的block:

1: // 這是錯誤的範例,請勿在程式中使用這些文法!!      
2: void dontDoThis() {      
3:     void (^blockArray[3])(void); // 3 個block 的陣列      
4:     for (int i = 0; i < 3; ++i) {      
5:         blockArray[i] = ^{ printf("hello, %d\n", i); };      
6:         // 注意: 這個block 定義僅在for 回圈有效。      
7:     }      
8: }      
9: void dontDoThisEither() {      
10:     void (^block)(void);      
11:     int i = random():      
12:     if (i > 1000) {      
13:         block = ^{ printf("got i at: %d\n", i); };      
14:         // 注意: 這個block 定義僅在if 後的兩個大括号中有效。      
15:     }      
16:     // ...      
17: }