在前兩篇中,我們介紹了一些關(guān)于C語言的重要概念,指針跟struct,這些基礎(chǔ)知識是我們深入學習block的前提,在這里將盡可能的將關(guān)于block的相關(guān)知識講解的詳細一些,分享給那些想要提升自己的小伙伴們,文章會比較長,一下子寫完實在是有些力不從心,所以會采取不斷更新的方式。
首先看一下什么是Blocks
Blocks是C語言的擴充功能。可以用一句話來表示Blocks的擴充功能:帶有自動變量(局部變量)的匿名函數(shù)。
顧名思義,所謂匿名函數(shù)就是不帶有名稱的函數(shù)。C語言的標準不允許存在這樣的函數(shù)。例如如下的源代碼:
int func(int count);
它聲明了名為func的函數(shù)。下面的源代碼中為了調(diào)用該函數(shù),必須使用該函數(shù)的名稱func。
int result = func(10);
如果像下面這樣,使用函數(shù)指針來代替直接調(diào)用函數(shù),那么似乎不用知道函數(shù)名也能夠使用該函數(shù)。
int result = (*funcptr)(10);
但其實使用函數(shù)指針也仍然要知道函數(shù)名稱。像以下的源代碼一樣,在賦值給函數(shù)指針時,若不使用想賦值的函數(shù)的名稱,就無法獲得該函數(shù)的地址。
int (*funcptr)(int) = &func;
int result = (*funcptr)(10);
而通過Blocks,源代碼中就能夠使用匿名函數(shù)了,即不帶名稱的函數(shù)。對于程序員而言,命名就是工作的本質(zhì),函數(shù)名、變量名、方法名、屬性名、類名和框架等都必須具備。而能夠編寫不帶名稱的函數(shù)對程序員來說相當具有吸引力。
到這里,我們知道了“帶有自動變量值的匿名函數(shù)”中“匿名函數(shù)”的概念。那么“帶有自動變量值”究竟是什么呢?
首先回顧一下在C語言的函數(shù)中可能使用的變量。(以下的變量在內(nèi)存中的分配是不一樣的,可以參考這篇文章了解 C語言中的內(nèi)存分配。)
- 自動變量(局部變量)
- 函數(shù)的參數(shù)
- 靜態(tài)變量(靜態(tài)局部變量,方法中用static聲明的變量)
- 靜態(tài)全局變量(方法外用static聲明的變量)
- 全局變量
其中,在函數(shù)的多次調(diào)用之間能夠傳遞的變量有:
- 靜態(tài)變量(靜態(tài)局部變量)
- 靜態(tài)全局變量(只能在本文件中使用的全局變量)
- 全局變量(外接也可以使用的全局變量)
雖然這些變量的作用域不同,但在整個程序中,一個變量總保持在一個內(nèi)存區(qū)域。因此,雖然多次調(diào)用函數(shù),但該變量值總能保持不變,在任何時候以任何狀態(tài)調(diào)用,使用的都是同樣的變量值。
另外,“帶有自動變量值的匿名函數(shù)”這一概念并不僅指Blocks,它還存在于其他許多程序語言中。
| 程序語言 | Block的名稱 |
|---|---|
| C + Blocks | Block |
| Smalltalk | Block |
| Ruby | Block |
| LISP | Lambda |
| Python | Lambda |
| C++11 | Lambda |
| Javascript | Anonymous function |
Blocks 模式
一、Block語法
下面我們詳細講解一下帶有自動變量值得匿名函數(shù)Block的函數(shù),即Block表達式語法。先看一個簡單點的形式:
^(int event){
printf("event:%d",event);
}
實際上,上述Block語法使用了省略方式,其完整形式如下:
^void(int event){
printf("event:%d",event);
}
如上所示,完整形式的Block語法與一般的C語言函數(shù)定義相比,僅僅有兩點不同。
- 沒有函數(shù)名
- 帶有“^”
第一點不同是沒有函數(shù)名,因為它是匿名函數(shù)。第二點不同是返回值類型前帶有“^”(插入記號,caret)記號。因為OS X、iOS應(yīng)用程序的源代碼中將大量使用Block,所以插入該記號便于查找。
Block的一般范式如下
^ 返回值類型 參數(shù)列表 表達式
返回值類型同C語言函數(shù)的返回值類型,“參數(shù)列表”同C語言的參數(shù)列表,“表達式”同C語言函數(shù)中允許使用的表達式。當然與C語言函數(shù)一樣,表達式中含有return語句時,其類型必須與返回值類型相同。
例如寫出如下形式的Block語法:
^ int (int count){return count+1};
返回值的類型可以省略
雖然前面出現(xiàn)過省略方式,但Block的語法可省略好幾個項目。首先是返回值類型,如下所示:
^ 參數(shù)列表 表達式(返回值類型被省略)
當你省略返回值類型時,如果表達式中有return語句就使用該返回值的類型,如果表達式中沒有return語句就使用void類型。表達式中含有多個return語句時,所有return的返回值類型必須相同。
前面的^ int (int count){return count+1};源代碼省略其返回值類型時如下所示:
^ (int count){return count+1};
該block語法將按照return語句的類型,返回int形的返回值。
如果不使用參數(shù),參數(shù)列表也可以省略。以下為不使用參數(shù)的Block語法:
^ void(void){printf("blocks\n")};
可以進一步的省略:
^ {printf("blocks\n")};
二、Block類型變量
上面提到的Block語法單單從其技術(shù)方式上來看,除了沒有名稱以及帶有“^”以外,其他都與C語言函數(shù)定義相同。在定義C語言函數(shù)時,就可以將所定義函數(shù)的地址賦值給函數(shù)指針類型變量中。
int func(int count){
return count+1;
}
int (*funcptr)(int) = &func;
這樣一來,函數(shù)func的地址就能賦值給函數(shù)指針類型變量funcptr中了。
同樣地,在Block語法下,可將Block語法賦值給聲明為Block類型的變量中。即源代碼中一旦使用Block語法就相當于生成了可賦值給Block類型變量的“值”。Blocks中由Block語法生成的值也被稱為“Block”。在有關(guān)Blocks的文檔中,“Block”既指源代碼中的Block語法,也指由Block語法所生成的值。
聲明Block類型變量的示例如下:
int (^blk)(int);
與前面的使用函數(shù)指針的源代碼對比可知,聲明Block類型變量僅僅是將聲明函數(shù)指針類型變量*變?yōu)?code>^。該Block類型變量與一般的C語言變量完全相同,可作為以下用途使用。
- 自動變量
- 函數(shù)參數(shù)
- 靜態(tài)變量
- 靜態(tài)全局變量
- 全局變量
那么,下面我們就試著使用Block語法將Block賦值為Block類型變量。
int (^blk)(int) = ^(int count){
return count +1;
};
由“^”開始的block語法生成的Block被賦值給變量blk中。因為與通常的變量相同,所以當然也可以由block類型變量向block類型變量賦值。
int (^blk1)(int) = blk;//聲明一個block類型變量blk1,并將blk賦值給blk1。
int (^blk2)(int);//聲明一個block類型變量blk2。
blk2 = blk1;
在函數(shù)參數(shù)中使用Block類型變量可以向函數(shù)傳遞Block。
void func(int(^blk)(int)){}//int(^blk)(int)可以看做是一個block類型的變量blk。
在函數(shù)返回值中指定block類型,可以將block作為函數(shù)的返回值返回。
int (^func())(int){
return ^(int count){
return count+1
};//block類型為int (^func()),參數(shù)列表為空,不是沒有參數(shù),而是可以任意參數(shù)。返回的^(int count){return count+1}可以作為是上述類型的block變量。
}
從上面可以看到,在函數(shù)參數(shù)和返回值中使用Block類型變量時,記述方式比較復雜,這時候,可以向使用函數(shù)指針類型時那樣,使用typedef來解決該問題。
typedef int(^blk_t)(int);
如上所示,通過使用typedef可以聲明“blk_t”類型變量。我們試著在上面例子中的函數(shù)參數(shù)和函數(shù)返回值部分里使用一下。
/*原來的記述方式
void func(int (^blk)(int))
*/
//現(xiàn)在的
void func(blk_t blk)
/*原來的記述方式
int (^func()(int))
*/
//現(xiàn)在的
blk_t func();
通過使用typedef,函數(shù)定義就變得更容易理解了。
另外,將賦值給Block類型變量中Block方法像C語言通常的函數(shù)調(diào)用那樣使用,這種方法與使用函數(shù)指針類型變量調(diào)用函數(shù)的方法幾乎完全相同。變量funcptr為函數(shù)指針類型時,像下面這樣調(diào)用函數(shù)指針類型變量。
int result = (*funcptr)(10);
變量blk為Block類型的情況下,這樣調(diào)用block類型變量:
int result = blk(10);
通過Block類型變量調(diào)用Block與C語言通常的函數(shù)調(diào)用沒有區(qū)別。在函數(shù)參數(shù)中使用Block類型變量并在函數(shù)中執(zhí)行Block的例子如下:
int func(blk_t blk,int rate){
return blk(rate);
}
當然,在Objective-C的方法中也可使用
- (int)methodUsingBlock:(blk_t)blk rate:(int)rate{
return blk(rate);
}
Block類型變量可完全像通常的C語言變量一樣使用,因此也可以使用指向Block類型變量的指針,即Block的指針類型變量。
typedef int (^blk_t)(int);
blk_t blk = ^(int count){
return count +1;
}
blk_t *blkptr = &blk;
(*blkptr)(10);
由此可知Block類型變量可像C語言中其他類型變量一樣使用。
三、截獲自動變量值
通過Block的語法和Block類型變量的說明,我們已經(jīng)理解了“帶有自動變量值的匿名函數(shù)”中的“匿名函數(shù)”。而“帶有自動變量值”究竟是什么呢?“帶有自動變量值”在Blocks中表現(xiàn)為“截獲自動變量值”。截獲自動變量值的實例如下:
int main(){
int dmy = 256;
int val = 10;
const char *fmt = "val = %d\n";
void (^blk)(void) = ^{printf(fmt,val)};
val = 2;
fmt = "These values were changed . val = %d\n";
blk();
return 0;
}
該源代碼中,Block語法的表達式使用的是它之前聲明的自動變量fmt和val。Blocks中,Block表達式截獲所使用的自動變量的值,即保存該自動變量的瞬間值。因為Block表達式保存了自動變量的值,所以在執(zhí)行Block語法后,即使改寫B(tài)lock中使用的自動變量的值也不會影響B(tài)lock執(zhí)行時自動變量的值。該源代碼就在Block語法后改寫了Block中的自動變量val和fmt。下面我們一起看下執(zhí)行結(jié)果。
val = 10;
執(zhí)行結(jié)果并不是改寫后的值,而是執(zhí)行Block愈發(fā)時候的自動變量的瞬間值。該Block語法在執(zhí)行時,字符串指針被賦值到自動變量fmt中,int值10被賦值到自動變量val中,因此這些值被保存(即被截獲),從而在執(zhí)行塊時使用。
這就是自動變量值的截獲。
四、__block 說明符
實際上,自動變量值截獲只能保存執(zhí)行Block語法瞬間的值。保存后就不能改寫該值。下面我們嘗試改寫截獲的自動變量值,看看會出現(xiàn)什么結(jié)果。下面的源代碼中,Block語法之前聲明的自動變量val值被賦予1。
int val = 0;
void (^blk)(void) = ^{val = 1;};
blk();
printf ("val = %d\n",val);
以上為在Block語法外聲明的給自動變量賦值的源代碼。該源代碼會產(chǎn)生編譯錯誤。
error: variable is not assignable(missing __block type specifier)
void (^blk)(void) = {val = 1; };
~~~ ^
若想在Block語法的表達式中將值賦給在Block語法外聲明的自動變量,需要在該自動變量上附加__block說明符。該源代碼中,如果給自動變量聲明int val附加__block說明符,就能實現(xiàn)在Block內(nèi)賦值。
__block int val = 0;
void(^blk)(void) = ^{val = 1;};
blk();
printf("val = %d\n",val);
該源代碼的執(zhí)行結(jié)果為
val = 1;
使用附有__block說明符的自動變量可在Block中賦值,該變量稱為__block變量。
五、 截獲的自動變量
如果將值賦值給Block中截獲的自動變量,就會產(chǎn)生編譯錯誤。
int val = 0;
void(^blk)(void) = ^{val = 1;}
該源代碼會產(chǎn)生以下編譯錯誤:
error: variable is not assignable(missing __block type specifier)
void (^blk)(void) = {val = 1; };
~~~ ^
那么截獲Objective-C對象,調(diào)用變更該對象的方法也會產(chǎn)生編譯錯誤么?
id array = [[NSMutableArray alloc]init];
void (^blk)(void) = ^{
id obj = [[NSObject alloc]init];
[array addObject:obj];
};
這是沒有問題的,而向截獲的變量array賦值會產(chǎn)生編譯錯誤。該源代碼中截獲的變量值為NSMutableArray類的對象。如果用C語言來描述,即是截獲NSMutableArray類對象用的結(jié)構(gòu)體實例指針。雖然賦值給介乎跌自動變量array的操作會產(chǎn)生編譯錯誤,但使用截獲的值卻不會有任何問題。下面的源代碼向截獲的自動變量進行賦值,因此會產(chǎn)生編譯錯誤。
id array = [[NSMutableArray alloc]init];
void (^blk)(void) = ^{
array = [[NSMutableArray alloc]init];
};
error: variable is not assignable(missing __block type specifier)
array = [[NSMutableArray alloc]init];
~~~ ^
這種情況下,需要給截獲的自動變量附加__block說明符。
__block id array = [[NSMutableArray alloc]init];
void (^blk)(void) = ^{
array = [[NSMutableArray alloc]init];
};
接下來會介紹block是如何實現(xiàn)的,會從它的c++源碼層面進行分析。(未完待續(xù)。。)
參考書籍:Objective-C高級編程