初識block
本文由破船譯自rypress轉載請註明出處!
小引
本週末微博上朋友發了一個關於block的MV,只能說老外太逗了。大家也可以去看看怎麼回事: Cocoa Got Blocks。雖然之前也有接觸過block,不過沒有深入完整的學習過,藉此機會來學習一下,順便翻譯幾篇block相關的文章,本文是第一篇,算是block的入門。本文的最後延伸閱讀給出了4篇相關文章,不出意外的話,本週大家能看到對應的中文版。
目錄:
- Block簡介
- Block的創建
- 不帶參數的Block
- Block的閉包性(closure)
- 修改非局部變量
- Block作爲函數的參數
- 定義Block類型
- 總結
- 延伸閱讀
正文
Block簡介
我們可以把Block當做Objective-C的匿名函數。Block允許開發者在兩個對象之間將任意的語句當做數據進行傳遞,往往這要比引用定義在別處的函數直觀。另外,block的實現具有封閉性(closure),而又能夠很容易獲取上下文的相關狀態信息。
Block的創建
實際上,block使用了與函數相同的機制:可以像聲明函數一樣,來聲明一個bock變量;可以利用定義一個函數的方法來定義一個block;也可以將block當做一個函數來調用。
// main.m #import <Foundation/Foundation.h> int main(int argc, const char * argv[]) { @autoreleasepool { // Declare the block variable double (^distanceFromRateAndTime)(double rate, double time); // Create and assign the block distanceFromRateAndTime = ^double(double rate, double time) { return rate * time; }; // Call the block double dx = distanceFromRateAndTime(35, 1.5); NSLog(@"A car driving 35 mph will travel " @"%.2f miles in 1.5 hours.", dx); } return 0; }
在上面的代碼中,利用插入符(^)將distanceFromRateAndTime變量標記爲一個block。就像聲明函數一樣,需要包含返回值的類型,以及參數的類型,這樣編譯器才能安全的進行強制類型轉換。插入符(^)跟指針(例如 int *aPointer)前面的星號(*)類似——只是在聲明的時候需要使用,之後用法跟普通的變量一樣。
block的定義本質上跟函數一樣——只不過不需要函數名。block以簽名字符串開始:double(double rate, double time)標示返回一個double,以及接收兩個同樣爲double的參數(如果不需要返回值,可以忽略掉)。在簽名後面是一個大括弧({}),在這個括弧裏面可以編寫任意的語句代碼,這跟普通的函數一樣。
當把block賦值給distanceFromRateAndTime後,我們就可以像調用函數一樣調用這個變量了。
不帶參數的Block
如果block不需要任何的參數,那麼可以忽略掉參數列表。另外,在定義block的時候,返回值的類型也是可選的,所以這樣情況下,block可以簡寫爲^ { … }:
double (^randomPercent)(void) = ^ { return (double)arc4random() / 4294967295; }; NSLog(@"Gas tank is %.1f%% full", randomPercent() * 100);
在上面的代碼中,利用內置的arc4random()方法返回一個32位的整型隨機數——爲了獲得0-1之間的一個值,通過除以arc4random()方法能夠獲取到的最大值(4294967295)。
到現在爲止,block看起來可能有點像利用一種複雜的方式來定義一個方法。事實上,block是被設計爲閉包的(closure)——這就提供了一種新的、令人興奮的編程方式。
Block的閉包性(closure)
在block內部,可以像普通函數一樣訪問數據:局部變量、傳遞給block的參數,全局變量/函數。並且由於block具有閉包性,所以還能訪問非局部變量(non-local variable)。非局部變量定義在block之外,但是在block內部有它的作用域。例如,getFullCarName可以使用定義在block前面的make變量:
NSString *make = @"Honda"; NSString *(^getFullCarName)(NSString *) = ^(NSString *model) { return [make stringByAppendingFormat:@" %@", model]; }; NSLog(@"%@", getFullCarName(@"Accord")); // Honda Accord
非局部變量會以const變量被拷貝並存儲到block中,也就是說block對其是隻讀的。如果嘗試在block內部給make變量賦值,會拋出編譯器錯誤。
以const拷貝的方式訪問非局部變量,意味着block實際上並不是真正的訪問了非局部變量——只不過在block中創建了非局部變量的一個快照。當定義block時,無論非局部變量的值是什麼,都將被凍結,並且block會一直使用這個值,即使在之後的代碼中修改了非局部變量的值。下面通過代碼來看看,在創建好block之後,修改make變量的值,會發生什麼:
NSString *make = @"Honda"; NSString *(^getFullCarName)(NSString *) = ^(NSString *model) { return [make stringByAppendingFormat:@" %@", model]; }; NSLog(@"%@", getFullCarName(@"Accord")); // Honda Accord // Try changing the non-local variable (it won't change the block) make = @"Porsche"; NSLog(@"%@", getFullCarName(@"911 Turbo")); // Honda 911 Turbo
block的閉包性爲block與上下文交互的時候帶來極大的便利性,當block需要額外的數據時,可以避免使用參數——只需要簡單的使用非局部變量即可。
修改非局部變量
凍結中的非局部變量是一個常量值,這也是一種默認的安全行爲——因爲這可以防止在block中的代碼對非局部變量做了意外的修改。那麼如果我們希望在block中對非局部變量值進行修改要如何做呢——用__block存儲修飾符(storage modifier)來聲明非局部變量:
__block NSString *make = @"Honda";
這將告訴block對非局部變量做引用處理,在block外部make變量和內部的make變量創建一個直接的鏈接(direct link)。現在就可以在block外部修改make,然後反應到block內部,反過來,也是一樣。
這跟普通函數中的靜態局部變量(static local variable)類似,用__block修飾符聲明的變量可以記錄着block多次調用的結果。例如下面的代碼創建了一個block,在block中對i進行累加。
__block int i = 0; int (^count)(void) = ^ { i += 1; return i; }; NSLog(@"%d", count()); // 1 NSLog(@"%d", count()); // 2 NSLog(@"%d", count()); // 3
Block作爲函數的參數
把block存儲在變量中有時候非常有用,比如將其用作函數的參數。這可以解決類似函數指針能解決的問題,不過我們也可以定義內聯的block,這樣代碼更加易讀。
例如下面Car interface中聲明瞭一個方法,該方法用來計算汽車的里程數。這裏並沒有強制要求調用者給該方法傳遞一個常量速度,相反可以改方法接收一個block——該block根據具體的時間來定義汽車的速度。
// Car.h #import <Foundation/Foundation.h> @interface Car : NSObject @property double odometer; - (void)driveForDuration:(double)duration withVariableSpeed:(double (^)(double time))speedFunction steps:(int)numSteps; @end
上面代碼中block的數據類型是double (^)(double time),也就是說block的調用者需要傳遞一個double類型的參數,並且該block的返回值爲double類型。注意:上面代碼中的語法基本與本文開頭介紹的block變量聲明相同,只不過沒有變量名字。
在函數的實現裏面可以通過speedFunction來調用block。下面的示例通過算法計算出汽車行駛的大約距離。其中steps參數是由調用者確定的一個準確值。
// Car.m #import "Car.h" @implementation Car @synthesize odometer = _odometer; - (void)driveForDuration:(double)duration withVariableSpeed:(double (^)(double time))speedFunction steps:(int)numSteps { double dt = duration / numSteps; for (int i=1; i<=numSteps; i++) { _odometer += speedFunction(i*dt) * dt; } } @end
在下面的代碼中,有一個main函數,在main函數中block定義在另一個函數的調用過程中。雖然理解其中的語法需要話幾秒鐘時間,不過這比起另外聲明一個函數,再定義withVariableSpeed參數要更加直觀。
// main.m #import <Foundation/Foundation.h> #import "Car.h" int main(int argc, const char * argv[]) { @autoreleasepool { Car *theCar = [[Car alloc] init]; // Drive for awhile with constant speed of 5.0 m/s [theCar driveForDuration:10.0 withVariableSpeed:^(double time) { return 5.0; } steps:100]; NSLog(@"The car has now driven %.2f meters", theCar.odometer); // Start accelerating at a rate of 1.0 m/s^2 [theCar driveForDuration:10.0 withVariableSpeed:^(double time) { return time + 5.0; } steps:100]; NSLog(@"The car has now driven %.2f meters", theCar.odometer); } return 0; }
上面利用一個簡單的示例演示了block的通用性。在iOS的SDK中有許多API都利用了block的其它一些功能。NSArray的sortedArrayUsingComparator:方法可以使用一個block對元素進行排序,而UIView的animateWithDuration:animations:方法使用了一個block來定義動畫的最終狀態。此外,block在併發編程中具有強大的作用。
定義Block類型
由於block數據類型的語法會很快把函數的聲明搞得難以閱讀,所以經常使用typedef對block的簽名(signature)做處理。例如,下面的代碼創建了一個叫做SpeedFunction的新類型,這樣我們就可以對withVariableSpeed參數使用一個更加有語義的數據類型。
// Car.h #import <Foundation/Foundation.h> // Define a new type for the block typedef double (^SpeedFunction)(double); @interface Car : NSObject @property double odometer; - (void)driveForDuration:(double)duration withVariableSpeed:(SpeedFunction)speedFunction steps:(int)numSteps; @end
許多標準的Objective-C框架也使用了這樣的技巧,例如NSComparator。
總結
Block不僅提供了C函數同樣的功能,而且block看起來更加直觀。block可以定義爲內聯(inline),這樣在函數內部調用的時候就非常方便,由於block具有閉包性(closure),所以block可以很容易獲得上下文信息,而又不會對這些數據產生負面影響。
延伸閱讀
- A look inside blocks: Episode 1
- A look inside blocks: Episode 2
- A look inside blocks: Episode 3 (Block_copy)
- Closure and anonymous functions in Objective-C
本文由破船翻譯●轉載請註明出處●2013-07-08