http://pernghh.pixnet.net/blog/trackback/eac87d412e/33563409
本文來自臺(tái)灣的某開發(fā)人員的博客,被墻,感覺講的比較易懂,所以引過來。文字簡體化了,原來是繁體,變數(shù)=變量,這個(gè)注意一下。
本章學(xué)習(xí)目標(biāo):
1. 了解何謂block。
2. 了解block的使用方法。
Block 是iOS在4.0之后新增的程式語法,嚴(yán)格來說block的概念并不算是基礎(chǔ)程式設(shè)計(jì)的范圍,對(duì)初學(xué)者來說也不是很容易了解,但是在iOS SDK 4.0之后,block幾乎出現(xiàn)在所有新版的API之中,換句話說,如果不了解block這個(gè)概念就無法使用SDK 4.0版本以后的新功能,因此雖然block本身的語法有點(diǎn)難度,但為了使用iOS的新功能我們還是得硬著頭皮去了解這個(gè)新的程式概念。
在這一章的目標(biāo)以了解如何使用block為主而不深入探討block底層的運(yùn)作方式,至于有些初學(xué)者較少遇到的辭匯如「詞法作用域(lexical scope)」等,本章將不再多做解釋,待有興趣的讀者去請(qǐng)教Google大神吧。
X.1 初探Block
在這一小節(jié)我們先用一些簡單范例來導(dǎo)入block的概念。
X.1.1 宣告和使用Block
我們使用「^」運(yùn)算子來宣告一個(gè)block變數(shù),而且在block的定義最后面要加上「;」來表示一個(gè)完整的述句(也就是將整個(gè)block定義視為前面章節(jié)所介紹的簡單述句,因?yàn)檎麄€(gè)定義必須是一個(gè)完整的句子,所以必須在最后面加上分號(hào)),下面是一個(gè)block的范例:
1: int multiplier = 7 ;
2: int (^myBlock)( int ) = ^( int num)
3: {
4: return num * multiplier;
5: };
我們使用下圖來解釋這個(gè)范例(請(qǐng)將文字框的字翻譯如下):
我們宣告一個(gè)「myBlock」變數(shù),用「^」符號(hào)來表示這是一個(gè)block。
這是block的完整定義,這個(gè)定義將會(huì)指定給「myBlock」變數(shù)。
表示「myBlock」是一個(gè)回傳值為整數(shù)(int)的block。
它有一個(gè)參數(shù),型態(tài)也是整數(shù)。
這個(gè)參數(shù)的名字叫做「num」。
這是block的內(nèi)容。
值得注意的地方是block可以使用和本身定義范圍相同的變數(shù),可以想像在上面的例子中 multiplier 和 myBlock 都是某一個(gè)函數(shù)內(nèi)定義的兩個(gè)變數(shù)也就是這個(gè)變數(shù)都在某個(gè)函數(shù)兩個(gè)大括號(hào)「{」和「 }」中間的區(qū)塊,因?yàn)樗鼈兊挠行Х秶窍嗤?,因此在block中就可以直接使用 multiplier 這個(gè)變數(shù),此外當(dāng)把block定義成一個(gè)變數(shù)的時(shí),我們可以直接像使用一般函數(shù)般的方式使用它:
1: int multiplier = 7 ;
2: int (^myBlock)( int ) = ^( int num)
3: {
4: return num * multiplier;
5: };
6: printf ( "%d" , myBlock( 3 ));
7: //結(jié)果會(huì)打印出21
X.1.2 直接使用Block
在很多情況下,我們并不需要將block宣告成變數(shù),反之我們可以直接在需要使用block的地方直接用內(nèi)嵌的方式將block的內(nèi)容寫出來,在下面的例子中qsort_b函數(shù),這是一個(gè)類似傳統(tǒng)的qsort_t函數(shù),但是直接使用block做為它的參數(shù):
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內(nèi)只能讀取在同一個(gè)作用域的變數(shù)而且沒有辦法修改在block外定義的任何變數(shù),此時(shí)若我們想要這些變數(shù)能夠在block中被修改,就必須在前面掛上__block的修飾詞,以上面第一個(gè)例子中的 multiplier 來說,這個(gè)變數(shù)在 block 中是唯讀的,所以 multiplier = 7 指定完后,在 block 中的 multiplier 就只能是 7 不能修改,若我們?cè)?block 中修改 multiplier ,在編輯時(shí)就會(huì)產(chǎn)生錯(cuò)誤,因此若想要在 block 中修改 multiplier ,就必須在 multiplier 前面加上 __block 的修飾詞,請(qǐng)參考下面的范例:
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 提供我們一種能夠?qū)⒑瘮?shù)程式碼內(nèi)嵌在一般述句中的方法,在其他語言中也有類似的概念稱做「closure」,但是為了配合Objective-C的貫例,我們一律將這種用法稱為「block」
X.2.1 Block 的功能
Block 是一種具有匿名功能的內(nèi)嵌函數(shù),它的特性如下:
如一般的函數(shù)般能擁有帶有型態(tài)的參數(shù)。
擁有回傳值。
可以擷取被定義的詞法作用域(lexical scope)狀態(tài)。
可以選擇性地修改詞法作用域的狀態(tài)。
注:詞法作用域(lexical scope)可以想像成是某個(gè)函數(shù)兩個(gè)大括號(hào)中間的區(qū)塊,這個(gè)區(qū)塊在程式執(zhí)行時(shí),系統(tǒng)會(huì)將這個(gè)區(qū)塊放入堆疊記憶體中,在這個(gè)區(qū)塊中的宣告的變數(shù)就像是我們常聽到的區(qū)域變數(shù),當(dāng)我們說block可以擷取同一詞法作用域的狀態(tài)時(shí)可以想像block變數(shù)和其他區(qū)域變數(shù)是同一個(gè)層級(jí)的區(qū)域變數(shù)(位于同一層的堆疊里),而block的內(nèi)容可以讀取到和他同一層級(jí)的其他區(qū)域變數(shù)。
我們可以拷貝一個(gè)block,也可以將它丟到其他的執(zhí)行緒中使用,基本上雖然block在iOS程式開發(fā)中可以使用在C/C++開發(fā)的程式片段,也可以在Objective-C中使用,不過在系統(tǒng)的定義上,block永遠(yuǎn)會(huì)被視為是一個(gè)Objective-C的物件。
X.2.2 Block 的使用時(shí)機(jī)
Block 一般是用來表示、簡化一小段的程式碼,它特別適合用來建立一些同步執(zhí)行的程式片段、封裝一些小型的工作或是用來做為某一個(gè)工作完成時(shí)的回傳呼叫(callback) 。
在新的iOS API中block被大量用來取代傳統(tǒng)的delegate和callback,而新的API會(huì)大量使用block主要是基于以下兩個(gè)原因:
可以直接在程式碼中撰寫等會(huì)要接著執(zhí)行的程式,直接將程式碼變成函數(shù)的參數(shù)傳入函數(shù)中,這是新API最常使用block的地方。
可以存取區(qū)域變數(shù),在傳統(tǒng)的callback實(shí)作時(shí),若想要存取區(qū)域變數(shù)得將變數(shù)封裝成結(jié)構(gòu)才能使用,而block則是可以很方便地直接存取區(qū)域變數(shù)。
X.3 宣告和建立Block
X.3.1 宣告Block的參考(Reference)
Block 變數(shù)儲(chǔ)存的是一個(gè)block的參考,我們使用類似宣告指標(biāo)的方式來宣告,不同的是這時(shí)block變數(shù)指到的地方是一個(gè)函數(shù),而指標(biāo)使用的是「*」,block則是使用「^」來宣告,下面是一些合法的block宣告:
1: /* 回傳void ,參數(shù)也是void 的block*/
2: void (^blockReturningVoidWithVoidArgument)( void );
3: /* 回傳整數(shù),兩個(gè)參數(shù)分別是整數(shù)和字元型態(tài)的block*/
4: int (^blockReturningIntWithIntAndCharArguments)( int , char );
5: /* 回傳void ,含有10 個(gè)block 的陣列,每個(gè)block 都有一個(gè)型態(tài)為整數(shù)的參數(shù)*/
6: void (^arrayOfTenBlocksReturningVoidWinIntArgument[ 10 ])( int );
7: X.3.2 建立一個(gè)Block
8:
9: 我們使用「^」來開始一個(gè)block,并在最后使用「;」來表示結(jié)束,下面的范例示范了一個(gè)block變數(shù),然后再定義一個(gè)block把它指定給block變數(shù):
10:
11: int (^oneFrom)( int ); /* 宣告block 變數(shù)*/
12: /* 定義block 的內(nèi)容并指定給上面宣告的變數(shù)*/
13: oneFrom = ^(int anInt)
14: {
15: return anInt = - 1 ;
16: };
X.3.3 全域的Block
我在可以在檔案中宣告一個(gè)全域的block,請(qǐng)參考以下范例:
1: int GlobalInt = 0 ;
2: int (^getGlobalInt)( void ) = ^ ( void ) { return GlobalInt ;};
X.4 Block 和變量
接下來的這一小節(jié)我們將會(huì)介紹block和變數(shù)之間的互動(dòng)。
X.4.1 變數(shù)的型態(tài)
我們可以在block中遇到平常在函數(shù)中會(huì)遇到的變數(shù)類型:
l 全域(global)變數(shù)或是靜態(tài)的區(qū)域變數(shù)(static local)。
l 全域的函數(shù)。
l 區(qū)域變數(shù)和由封閉領(lǐng)域(enclosing scope)傳入的參數(shù)。
除了上述之外block額外支援了另外兩種變數(shù):
在函數(shù)內(nèi)可以使用__block 變數(shù),這些變數(shù)在block中是可被修改的。
匯入常數(shù)(const imports)。
此外,在方法的實(shí)作里,block可以使用Objective-C的實(shí)體變數(shù)(instance variable)。
下列的規(guī)則可以套用到在block中變數(shù)的使用:
可以存取全域變數(shù)和在同一領(lǐng)域(enclosing lexical scope)中的靜態(tài)變數(shù)。
可以存取傳入block的參數(shù)(使用方式和傳入函數(shù)的參數(shù)相同)。
在同一領(lǐng)域的區(qū)域變數(shù)在block中將視為常數(shù)(const)。
可以存取在同一領(lǐng)域中以__block 為修飾詞的變數(shù)。
在block中宣告的區(qū)域變數(shù),使用方式和平常函數(shù)使用區(qū)域變數(shù)的方式相同。
下面的例子介紹了區(qū)域變數(shù)(上述第三點(diǎn))的使用方式:
1: int x = 123 ;
2: void (^printXAndY)( int ) = ^( int y)
3: {
4: printf ( "%d %d\n" , x, y);
5: };
6: // 將會(huì)印出123 456
7: printXAndY( 456 );
8: 就如上面第三點(diǎn)所提到的,在上例中的int x = 123的變量x,在傳入block后將視同常數(shù),因此若我們?cè)赽lock中試著去修改x的值時(shí)就會(huì)產(chǎn)生錯(cuò)誤,下面的例子將會(huì)無法通過編譯:
9:
10: int x = 123 ;
11: void (^printXAndY)( int ) = ^( int y)
12: {
13: // 下面這一行是錯(cuò)的,因?yàn)閤 在這是一個(gè)常數(shù)不能被修改。
14: x = x + y;
15: printf ( "%d %d\n" , x, y);
16: };
若在block中想要修改上面的變數(shù)x,必須將x宣告加上修飾詞__block,請(qǐng)參考接下來這一小節(jié)的介紹。
X.4.2 __block 型態(tài)變數(shù)
我們可以藉由將一個(gè)由外部匯入block的變數(shù)放上修飾詞__block來讓這個(gè)變數(shù)由唯讀變成可以讀和寫,不過有一個(gè)限制就是傳入的變數(shù)在記憶體中必須是一個(gè)占有固定長度記憶體的變數(shù),__block修飾詞無法使用于像是變動(dòng)長度的陣列這類不定長度的變數(shù),請(qǐng)參考下面的范例:
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: // 將會(huì)印出579 456
9: printXAndY( 456 );
10: //x 將會(huì)變成 579;
11: 下面我們使用一個(gè)范例來介紹各類型的變數(shù)和block之間的互動(dòng):
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 ; //可以存取?!?/span>
22: CounterGlobal = localCounter; //localCounter在block 建立時(shí)就不可變了。
23: localCharacter = 'a' ; //設(shè)定外面定義的localCharacter 變數(shù)。
24: };
25: ++localCounter; //不會(huì)影響的block 中的值。
26: localCharacter = 'b' ;
27: aBlock(); //執(zhí)行block 的內(nèi)容。
28: //執(zhí)行完后,localCharachter 會(huì)變成'a'
29: }
X.4.3 物件和Block變數(shù)
Block 支援在Objective-C、C++物件和其他block中當(dāng)作變數(shù)來使用,不過因?yàn)樵诖蟛糠值那闆r我們都是使用Objective-C的撰寫程式,因此在這一小節(jié)我們僅針對(duì)Objective-C的情況進(jìn)行介紹,至于其他兩種情況就留給有興趣的讀者再自行深入研究了。
x.4.3.1 Objective-C 物件
在擁有參考計(jì)數(shù)(reference-counted)的環(huán)境中,若我們?cè)赽lock中參考到Objective-C的物件,在一般的情況下它將會(huì)自動(dòng)增加物件的參考計(jì)數(shù),不過若以__block為修飾詞的物件,參考計(jì)數(shù)則是不受影響。
如果我們?cè)贠bjective-C的方法中使用block時(shí),以下幾個(gè)和記憶體管理的事是需要額外注意的:
l 若直接存取實(shí)體變數(shù)(instance variable),self的參考計(jì)數(shù)將被加1。
l 若透過變數(shù)存取實(shí)體變數(shù)的值,則只變數(shù)的參考計(jì)數(shù)將被加1。
以下程式碼說明上面兩種情況,在這個(gè)假設(shè)instanceVariable是實(shí)體變數(shù):
1: dispatch_async (queue, ^{
2: // 因?yàn)橹苯哟嫒?shí)體變數(shù)instanceVariable ,所以self 的retain count 會(huì)加1
3: doSomethingWithObject (instanceVariable);
4: });
5: id localVaribale = instanceVariable;
6: dispatch_async (queue, ^{
7: //localVariable 是存取值,所以這時(shí)只有l(wèi)ocalVariable 的retain count 加1
8: //self 的 return count 并不會(huì)增加。
9: doSomethingWithObject (localVaribale);
10: });
X.5 使用Block
這一小節(jié)我們將會(huì)對(duì)block的使用方式做一些初步的介紹
X.5.1 呼叫一個(gè)Block
當(dāng)block宣告成一個(gè)變數(shù)時(shí),我們可以像使用一般函數(shù)的方式來使用它,請(qǐng)參考下面兩個(gè)范例:
1: int (^oneFrom)( int ) = ^( int anInt) {
2: return anInt - 1 ;
3: };
4: printf ( "1 from 10 is %d" , oneFrom( 10 ));
5: //結(jié)果會(huì)顯示: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會(huì)變成4.9
在一般常見的情況中,若是將block當(dāng)做是參數(shù)傳入函數(shù),我們通常會(huì)使用「內(nèi)嵌」的方式來使用block。
X.5.2 將Block當(dāng)作函數(shù)的參數(shù)
我們可以像使用一般函數(shù)使用參數(shù)的方式,將block以函數(shù)參數(shù)的型式傳入函數(shù)中,在這種情況下,大多數(shù)我們使用block的方式將不會(huì)傾向宣告block而是直接以內(nèi)嵌的方式來將block傳入,這也是目前新版SDK中主流的做法,我們將補(bǔ)充前面章節(jié)的例子來說明:
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 的終點(diǎn)。
9: );
10: // 最后的結(jié)果為:{"Charles Condomine", "George", "TomJohn"}
在上面的例子中,block本身就是函數(shù)參數(shù)的一部分,在下一個(gè)例子中dispatch_apply函數(shù)中使用block,dispatch_apply的定義如下:
1: void
2: dispatch_apply( size_t iterations, dispatch_queue_t queue, void (^block)( size_t ));
3: 這個(gè)函數(shù)將一個(gè)block提交到發(fā)送佇列(dispatch queue)中來執(zhí)行多重的呼叫,只有當(dāng)佇列中的工作都執(zhí)行完成后才會(huì)回傳,這個(gè)函數(shù)擁有三個(gè)變數(shù),而最后一個(gè)參數(shù)就是block ,請(qǐng)參考下面的范例:
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當(dāng)作方法的參數(shù)
在SDK中提供了許多使用block的方法,我們可以像傳遞一般參數(shù)的方式來傳遞block,下面這個(gè)范例示范如何在一個(gè)陣列的前5筆資料中取出我們想要的資料的索引值:
1: // 所有的資料
2: NSArray *array = [ NSArray arrayWithObjects : @"A" , @"B" , @"C" , @"A" , @"B" , @"Z" , @"G" , @"are" , @" Q" ,nil ];
3: // 我們只要這個(gè)集合內(nèi)的資料
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: // 只對(duì)前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: // 結(jié)果: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回圈的區(qū)域變數(shù)因此在使用上必須避免將區(qū)域的block指定給外面宣告的block:
1: // 這是錯(cuò)誤的范例,請(qǐng)勿在程式中使用這些語法!!
2: void dontDoThis() {
3: void (^blockArray[3])(void); // 3 個(gè)block 的陣列
4: for (int i = 0; i < 3; ++i) {
5: blockArray[i] = ^{ printf("hello, %d\n", i); };
6: // 注意: 這個(gè)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: // 注意: 這個(gè)block 定義僅在if 后的兩個(gè)大括號(hào)中有效。
15: }
16: // ...
17: }