初識block

初識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-non-local-variablesconst-non-local-variables

以const拷貝的方式訪問非局部變量

 

以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內部,反過來,也是一樣。

mutable-non-local-variablesmutable-non-local-variables

通過引用的方式訪問非局部變量

這跟普通函數中的靜態局部變量(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可以很容易獲得上下文信息,而又不會對這些數據產生負面影響。

延伸閱讀

 

本文由破船翻譯●轉載請註明出處●2013-07-08

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章