Apple Inc. 為 C、C++、Objective-C 等語言加入了 Block 這個非標準的擴充功能。
簡單來說,Block 是一種可以當作變數傳遞的函數,而且具有 Closure(閉包)
特性。
Closure 是指一個包裹了區域變數的函數物件。
1. Block Literal:
Block Literal 的語法:
^ 返回值型別 (參數列) { 表達式 }
其中的返回值型別
可以省略不寫,如果該 Block 沒有使用到參數,那麼參數列
也可以省略不寫。
所以,完整的 Block 寫法會長得像這樣:
^int (int a, int b) { return a + b; }
如果省略返回值型別
不寫,那麼編譯器會使用表達式裡 return 的型別來當作返回值型別;如果表達式裡沒有 return 語句,那返回值型別就會使用 void。所以,上面那個 Block 的寫法可以再簡化成:
^(int a, int b) { return a + b; }
另外,沒有參數列
的 Block 可以這樣寫:
^{ NSLog(@"Hello!"); }
2. Block 變數的型別宣告:
Block 變數的型別宣告與函數指標的宣告長得很像,只是把變數名稱前的 *
換成 ^
而已,所以,Block 變數宣告的語法為:
返回值型別 (^變數名稱) (參數列)
因此,Block 變數宣告的寫法會像這樣:
void (^foo)(void)
宣告 Block 變數,並且賦值:
// 無參數、無返回值的 Block:
void (^blk1)(void) = ^{ NSLog(@"I'm blk1"); };
// 有參數、有返回值的 Block:
int (^blk2)(int, int) = ^(int a, int b) { return a + b; };
3. 使用 Block
Block 的使用方式就如同函數呼叫:
int (^add)(int, int) = ^(int a, int b) { return a + b; };
int sum = add(10, 20); // call add()
NSLog(@"sum = %d", sum);
執行結果:
sum = 30
或者,可以直接調用一個匿名 Block:
^(int a, int b) {
NSLog(@"=> %d", a + b);
}(11, 22);
執行結果:
=> 33
4. __block 修飾符
Block 可以捕捉外層範疇(scope)內的變數,所以可以這麼使用:
int number = 100;
void (^blk)(void) = ^{ NSLog(@"=> %d", number); };
blk();
執行結果:
=> 100
但是,如果想修改捕捉到的變數,會發生編譯錯誤,例如:
int number = 100;
void (^blk)(void) = ^{ NSLog(@"=> %d", ++number); /*把 number 加 1*/ }; // Error!
blk();
錯誤訊息如下:
Variable is not assignable (missing __block type specifier)
這時就必須在要修改的變數前面加上 __block
修飾符:
__block int number = 100; // 前面加上 __block
void (^blk)(void) = ^{ NSLog(@"=> %d", ++number); };
blk();
執行結果:
=> 101
Q:如果是 Cocoa 物件也需要加上 __block 才能修改嗎?
請看以下程式碼:
NSMutableArray *array = [NSMutableArray array];
^{
[array addObject:@1];
[array addObject:@2];
}();
NSLog(@"%@", array);
執行結果:
(
1,
2
)
沒有編譯錯誤,輸出結果也正確。
再來看看以下程式碼:
NSMutableArray *array = [NSMutableArray array];
^{
array = [NSMutableArray arrayWithArray:@[@1, @2, @3]]; // Error!
}();
NSLog(@"%@", array);
發生編譯錯誤,錯誤訊息如下:
Variable is not assignable (missing __block type specifier)
加上 __block 修飾符即可通過編譯:
__block NSMutableArray *array = [NSMutableArray array];
^{
array = [NSMutableArray arrayWithArray:@[@1, @2, @3]];
}();
NSLog(@"%@", array);
執行結果:
(
1,
2,
3
)
因為 Block 捕捉到的是指向物件的指標(看到物件宣告時前面那個*
,就應該知道那是個指標 😛 ),所以如果想修改指標的值,就需要加上 __block 修飾符。
接著再看看以下程式碼:
int count = 0;
void (^blk)(void) = ^{
NSLog(@"-> %d", count);
};
count = 100;
NSLog(@"%d", count); //印出目前 count 變數的值
blk(); //調用 blk
執行結果:
100
-> 0
調用 blk() 後印出的結果是 0,而不是 100。
因此,我們可以這麼認為:
在沒有使用 __block 修飾符的情況下:
對於內建型別(int, float…等),Block 捕捉到的是變數的值
;
對於物件型別,Block 捕捉到的是物件的參考
。
5. 使用 typedef 定義 Block 型別
如果想把 Block 當作參數傳給函數,可以這麼寫:
void func(int(^add)(int, int)) {
NSLog(@"sum = %d", add(10, 20));
}
int main() {
@autoreleasepool {
func(^(int a, int b){ return a + b; });
}
return 0;
}
執行結果:
sum = 30
如果想把一個 Block 當作函數返回值傳回呢?可以這麼寫:
int (^makeAdd(int c))(int, int) {
return ^(int a, int b) { return a + b + c; };
}
int main() {
@autoreleasepool {
int (^add)(int, int) = makeAdd(1000);
NSLog(@"=> %d", add(11, 22));
}
return 0;
}
執行結果:
=> 1033
看到這些寫法,頭都開始暈了@@…
這時可以使用 typedef
定義 Block 型別,讓程式碼比較好寫好讀:
typedef int (^AddBlock)(int a, int b);
void func(AddBlock add) {
NSLog(@"=> %d", add(5500, 66));
}
AddBlock makeAdd(int c) {
return ^(int a, int b) { return a + b + c; };
}
int main() {
@autoreleasepool {
func(^(int a, int b){ return a + b; });
AddBlock add = makeAdd(5500);
NSLog(@"=> %d", add(11, 55));
}
return 0;
}
執行結果:
=> 5566
=> 5566
(56 不能亡!)
6. Block 的範疇(scope)與記憶體管理
Block 有三種類型,分別為_NSConcreteGlobalBlock
、_NSConcreteStackBlock
、_NSConcreteMallocBlock
。
寫在全域空間(即函數外面)的 Block 是 _NSConcreteGlobalBlock 類別的一個實例,會儲存在 globals 區段裡。Global Block 因為是寫在全域空間,所以無法捕捉區域變數,且從程式開始執行一直到程式結束前都會存在,因此可以視為是一個單體物件(Singleton object)。
寫在函數或是程式碼區塊(即一對{})內的 Block 一般會儲存在 stack 區段,但是如果該 Block 並沒有使用到外層範疇內的變數(換句話說,就是不需要捕捉外層的區域變數),那麼該 Block 會被儲存在 globals 區段裡(_NSConcreteGlobalBlock 類別的實例)。
儲存在 stack 區段的 Block 是 _NSConcreteStackBlock 類別的實例。如果對一個 Stack Block 物件發送 copy 訊息,那麼該 Block 會被拷貝到 heap 區段上,儲存在 heap 區段上的 Block 是 _NSConcreteMallocBlock 類別的實例。
Stack Block 就跟自動變數
一樣,會在離開其生存空間時被移除,如果想在生存空間外繼續使用,就必須拷貝到 heap 區段上。儲存在 heap 區段上的 Block 如果不再使用,那麼要對它發送 release 訊息釋放其所佔用的記憶體空間。
如果程式的記憶體管理方式是使用 ARC(Automatic Reference Counting),那麼大多不用擔心 Block 何時需要 copy/release,因為編譯器會在必要時自動拷貝或釋放 Block。如果記憶體管理方式是使用 MRR(Manual Retain-Release),那麼必須自行管理 Block 的 copy/release 訊息。
先來看看以下程式碼:
int main() {
@autoreleasepool {
void(^blk)(void) = ^{ NSLog(@"Hello!"); }; //這是一個 Global Block
blk();
}
return 0;
}
blk 是一個 Global Block,因為它沒捕捉任何變數,也就是說,它不需要在執行時期設置任何狀態,所以被編譯成一個 Global Block。
那麼,以下這段程式碼裡的 blk2 會被編譯成什麼類型的Block呢?:
int main() {
@autoreleasepool {
int i = 100;
void(^blk2)(void) = ^{ NSLog(@"i = %d", i); }; //這個 Block 會被編譯成什麼類型呢?
blk2();
}
return 0;
}
如果是在 ARC 的情況下,那 blk2 會是 Malloc Block。
如果是在 MRR 的情況下,那 blk2 會是 Stack Block。
在 MRR 的情況下,函數內宣告的 Block 會是 Stack Block。問題來了,請看以下程式碼:
typedef void(^blk_t)(void);
blk_t func() {
int i = 10;
return ^{ NSLog(@"i = %d", i); };
}
int main() {
@autoreleasepool {
blk_t blk = func();
blk();
}
return 0;
}
上面這段程式碼在 ARC 下,可以正常編譯且執行。但在 MRR 下,會發生編譯錯誤,錯誤訊息如下:
Returning block that lives on the local stack
這時必須把 Block 拷貝到 heap 區段上才行:
typedef void(^blk_t)(void);
blk_t func() {
int i = 10;
return [[^{ NSLog(@"i = %d", i); } copy] autorelease]; // copy and autorelease
}
再來看一些在 MRR 下,需要自行 copy/release 的情況:
/**
* 將 Block 存入 NSArray/NSDictionary 前,
* 需要把 Block 從 stack 區段拷貝到 heap 區段上。
*
* 註:
* NSArray/NSDictionary 只能存放 Objective-C 物件,
* 而所有的 Objective-C 物件都是配置在 heap 區段上的。
*/
NSMutableArray *array = [NSMutableArray array];
[array addObject:[^(int a){NSLog(@"%d", a);} copy]];
// Block 離開生存空間後要繼續使用,需要 copy,使用完要 release
void(^myBlock)(void);
if (isTrue) {
//...
myBlock = [^{ /* ... */ } copy];
} else {
//...
myBlock = [^{ /* ... */ } copy];
}
//...
myBlock();
[myBlock release];
總之,如果是使用 ARC 管理記憶體,那麼不用擔心 Block 拷貝和釋放的問題,一切就交給編譯器處理吧。
如果是用 MRR 管理記憶體,那麼請務必留意 Block 何時需要拷貝,何時需要釋放。
7. Block 保留循環的問題
之前有提到,Block 可以捕捉外層範疇內的變數。
如果捕捉到的變數是一個 Objective-C 物件,那麼該物件會被保留(retain),等到 Block 執行完畢後,該物件會被釋放(release)。
假設有一個 ViewController,這個 ViewController 有一個 property 是 Block 物件,程式碼看起來像這樣:
typedef void (^MyCompletionBlock)(void);
@interface MyViewController : UIViewController
@property (nonatomic, copy) MyCompletionBlock completionBlock;
//...
@end
之後,在程式當中使用了這個 ViewController:
MyViewController *myViewController = [MyViewController alloc] init];
myViewController.completionBlock = ^{
[myViewController doSomething];
};
有看出上面這段程式碼有什麼問題嗎?這段程式碼裡發生了保留循環
(或稱為強參考循環 (Strong Reference Cycles) )! 因為 myViewController 保留了 completionBlock,而 completionBlock 也保留了 myViewController,所以形成了保留循環。
產生保留循環表示這些互相保留對方的物件將無法被釋放,會一直佔用記憶體空間,即使這些物件已不再使用。這種情況稱為記憶體洩漏
。
ARC 下的解決方法是使用一個弱參考(Weak reference)指標指向 myViewController,並在 Block 裡使用這個弱參考指標取代原本的強參考(Strong reference)指標:
// 如果記憶體管理方式是使用 ARC,可以用這種方式解決強參考循環:
MyViewController *myViewController = [MyViewController alloc] init];
__weak MyViewController *weakMyViewController = myViewController;
myViewController.completionBlock = ^{
[weakMyViewController doSomething];
};
MRR 下的解決方式:
// 在 MRR 下使用 __block 修飾符,表示這個物件的參考計數不要增加
__block MyViewController *myViewController = [MyViewController alloc] init];
myViewController.completionBlock = ^{
[myViewController doSomething];
myViewController = nil;
};
另外,附帶一提:Block 執行完畢後,如果 Block 不需要再使用,將 Block 釋放可能是一個好習慣:
if (self.completionBlock) {
self.completionBlock();
self.completionBlock = nil;
}
8. Closure
最常見的 Closure 範例應該就是計數器了吧。先用 JavaScript 示範計數器要怎麼寫吧:
// JavaScript 的計數器寫法
function makeCounter() {
var n = 0;
return function() {
n++;
console.log("n = " + n);
}
}
f = makeCounter(); // 傳回一個函數物件
f(); // 調用 f() 三次...
f();
f();
執行結果:
n = 1
n = 2
n = 3
注意 makeCounter() 函數裡 n 變數的生命週期。照理說,n 變數應該在 makeCounter() 結束後就應該消失了,但是為何 n 變數可以持續累加呢?因為這是 Closure(閉包)造成的效果。
makeCounter() 會回傳一個函數物件,而這個物件會將它定義當時範疇內的變數與函數繫結在一起。換句話說,就是會將該函數物件上層範疇內的區域變數「封閉」到該函數物件裡,因此稱為 Closure(閉包)。
所以,這就是 n 變數繼續存活的原因。
接著來把這段程式碼改成 Objective-C 版本:
// Objective-C 的計數器寫法
typedef void(^CounterBlock)(void);
CounterBlock makeCounter() {
__block int n = 0;
return ^{ NSLog(@"n = %d", ++n); };
}
int main() {
@autoreleasepool {
CounterBlock counter = makeCounter();
counter();
counter();
counter();
}
return 0;
}
執行結果:
n = 1
n = 2
n = 3
再來看一個 Closure 範例:
typedef NSString* (^SayBlock)(NSString *sentence);
SayBlock whoSay(NSString *name) {
return ^(NSString *sentence) { return [NSString stringWithFormat:@"%@ say: %@", name, sentence]; };
}
int main() {
@autoreleasepool {
SayBlock johnSay = whoSay(@"John");
SayBlock marySay = whoSay(@"Mary");
NSLog(@"%@", johnSay(@"Hello!"));
NSLog(@"%@", marySay(@"Hi!"));
NSLog(@"\n");
NSLog(@"%@", johnSay(@"ABCD狗咬豬!"));
NSLog(@"%@", marySay(@"1234567..."));
}
return 0;
}
執行結果:
John say: Hello!
Mary say: Hi!
John say: ABCD狗咬豬!
Mary say: 1234567...
在第一個範例裡用了 JavaScript 來示範計數器的寫法,再來看看一個 JavaScript 範例吧(但是跟 Closure 無關… XD):
var op = {
"+" : function(a,b) { return a + b; },
"-" : function(a,b) { return a - b; },
"*" : function(a,b) { return a * b; },
"/" : function(a,b) { return a / b; }
};
op 是一個 JavaScript 物件,JavaScript 的物件是鍵值對的集合,也就是一般俗稱的 Hash 或是 Dictionary。
然後可以這樣使用 op 物件:
var sum = op["+"](100, 20);
sum 的值是 120。
如果想在 Objective-C 裡實現這種寫法呢?可以這麼寫:
NSDictionary *operation = @{ @"+" : ^(int a, int b){return a + b;},
@"-" : ^(int a, int b){return a - b;},
@"*" : ^(int a, int b){return a * b;},
@"/" : ^(int a, int b){return a / b;} };
然後來試著調用 Key 為 @”+” 的 Block:
int sum = operation[@"+"](100, 20);
結果發生編譯失敗,錯誤訊息如下:
Called object type 'id' is not a function or function pointer
因為無法對一個id
型別進行函數呼叫的動作。
所以,必須先將 id 轉型為 Block,然後再調用它:
int sum = ((int(^)(int,int))operation[@"+"])(100, 20);
sum 的值為 120。
但是這個寫法真的很醜啊~~~~~
另一個寫法:
int (^add)(int, int) = operation[@"+"];
int sum = add(100, 20);
這個寫法看起來好多了,但是有點囉唆…
同場加映:C++ 的寫法:
map<string, function<int (int,int)>> operation = {
{ "+", [](int a, int b) {return a + b;} },
{ "-", [](int a, int b) {return a - b;} },
{ "*", [](int a, int b) {return a * b;} },
{ "/", [](int a, int b) {return a / b;} }
};
int a = 100, b = 20;
string opcodes[4] = {"+", "-", "*", "/"};
for (auto opcode : opcodes) {
cout << a << " " << opcode << " " << b << " = " << operation[opcode](a, b) << endl;
}
執行結果:
100 + 20 = 120
100 - 20 = 80
100 * 20 = 2000
100 / 20 = 5
個人認為,C++ 的 Lambda 表達式寫法看起來比 Objective-C Block 好看多了…
9. 對 Block 的愛與怨
對 Block 的愛:
身為一個 Ruby 程式員(自以為,但其實沒寫過幾行 Ruby 程式啊… XD),當然會對 Block 很有愛呀(羞) >////<
(旁白哥:可是 Ruby 的 Block 跟 Objective-C 的 Block 好像不太一樣呀… )*
對 Block 的怨:
「話那欲講透更,目屎是掰袂離…」 所以,不說也罷(逃~)。
*註:Objective-C 的 Block 行為上跟 Ruby 的 lambda 比較像。