塊編程指南

——譯自Apple Reference Library《Blocks Programming Topic》

簡介

塊對象是C語言的句法和運行時特性。它類似於標準C函數,但可以將代碼、變量綁定到堆(heap)、棧(stack)。一個塊還維護了一系列的狀態,這些狀態或數據影響着執行的結果。

可以把塊組成函數表達式,用於傳遞給API,或者使用在多線程裏。最有用的是回調,因爲塊在回調時能把代碼和數據一起傳送。

OSX 10.6的Xcode中,可以使用塊,它隨GCC和 Clang 一起集成。在OSX 10.6及iOS 4.0以後支持塊語法。 塊運行時是開源的,它能被集成到 LLVM’s compiler-rt subproject repository 中。標準C工作組的 N1370: Apple’s Extensions to C ( 其中也包括垃圾回收 ) 對塊進行了定義。O-C和C++都來自於C,塊在3種語言(包括O-C++)都能工作。

這篇文檔中,你會學習到什麼是塊對象,以及怎樣在C,C++和O-C中使用它,使代碼的性能和可維護性更高。

開始

聲明塊

^ 操作符聲明一個塊變量的開始(跟C一樣用; 來表示表達式結束),如代碼所示:

int multiplier = 7;

int (^myBlock)(int) = ^(int num) {

return num * multiplier;

};

解釋 :

注意,塊可以使用同一作用域內定義的變量。

一旦聲明瞭塊,你可以象使用函數一樣調用它:

int multiplier = 7;

int (^myBlock)(int) = ^(int num) {

return num * multiplier;

};

printf("%d", myBlock(3));

直接使用塊

很多情況下,你不必聲明塊變量,而簡單地寫一個行內塊並把它當作一個參數,如下面的代碼所示。

gsort_b類似標準的 gsort_r 函數,但它最後一個參數是一個塊。

char *myCharacters[3] = { "TomJohn", "George", "Charles Condomine" };

qsort_b(myCharacters, 3, sizeof(char *), ^(const void *l, const void *r) {

char *left = *(char **)l;

char *right = *(char **)r;

return strncmp(left, right, 1);

});

// myCharacters is now { "Charles Condomine", "George", TomJohn" }

Cocoa 和塊

Cocoa框架中,有幾種把塊作爲參數的方法。典型的是在集合中進行一個操作,或者在操作完成後作爲一個回調。下列代碼顯示如何在NSArray的sortedArrayUsingComparator方法中使用塊。這個方法使用了一個塊參數。爲了演示,在這裏把塊定義爲一個NSComparator本地變量。

NSArray *stringsArray = [NSArray arrayWithObjects: @"string 1", @"String 21",@"string 12",

@"String 11", @"String 02", nil];

static NSStringCompareOptions comparisonOptions = NSCaseInsensitiveSearch | NSNumericSearch |

NSWidthInsensitiveSearch | NSForcedOrderingSearch;

NSLocale *currentLocale = [NSLocale currentLocale];

NSComparator finderSortBlock = ^(id string1, id string2) {

NSRange string1Range = NSMakeRange(0, [string1 length]);

return [string1 compare:string2 options:comparisonOptions range:string1Range locale:currentLocale];

};

NSArray *finderSortArray = [stringsArray sortedArrayUsingComparator:finderSortBlock];

NSLog(@"finderSortArray: %@", finderSortArray);

/*Output:

finderSortArray: (

"string 1",

"String 02",

"String 11",

"string 12",

"String 21"

)*/

塊變量

塊的一個強大功能它可以改變在同一作用域內的變量。用__block修飾符來標識一個變量能夠被塊改變。使用下面的代碼,你可以用一個塊變量計算進行比較的字符串中有多少是相同的。爲了演示,塊是直接使用的,同時currentLocal變量對於塊來說是隻讀的。

NSArray *stringsArray = [NSArray arrayWithObjects:

@"string 1", @"String 21", // <-

@"string 12", @"String 11",@"Strîng 21", // <-

@"Striñg 21", // <-

@"String 02", nil];

NSLocale *currentLocale = [NSLocale currentLocale];

__block NSUInteger orderedSameCount = 0;

NSArray *diacriticInsensitiveSortArray = [stringsArray sortedArrayUsingComparator:^(id string1, id string2) {

NSRange string1Range = NSMakeRange(0, [string1 length]);

NSComparisonResult comparisonResult = [string1 compare:string2 options:NSDiacriticInsensitiveSearch range:string1Range locale:currentLocale];

if (comparisonResult == NSOrderedSame) {

orderedSameCount++;

}

return comparisonResult;

}];

NSLog(@"diacriticInsensitiveSortArray: %@", diacriticInsensitiveSortArray);

NSLog(@"orderedSameCount: %d", orderedSameCount);

/*Output:

diacriticInsensitiveSortArray: (

"String 02",

"string 1",

"String 11",

"string 12",

"String 21",

"Str/U00eeng 21",

"Stri/U00f1g 21"

)

orderedSameCount: 2

*/

相關概念

塊提供了一種方法,允許你創建一種特殊的函數體,在C及C派生語言如O-C和C++中,可以把塊視爲表達式。其他語言中,爲了不與C術語中的塊混淆,塊也被稱作closure(國內譯作閉包),這裏它們都稱做blocks。

塊的功能

塊是行內的代碼集合:

同函數一樣,有類型化參數列表

有返回結果或者要申明返回類型

能獲取同一作用域(定義塊的相同作用域)內的狀態

可以修改同一作用域的狀態(變量)

與同一範圍內的其他塊同享變量

在作用域釋放後能繼續共享和改變同一範圍內的變量

甚至可以複製塊並傳遞到其他後續執行的線程。編譯器和運行時負責把所有塊引用的變量保護在所有塊的拷貝的生命週期內。對於C和C++,塊是變量,但對於O-C ,塊仍然是對象。

塊的使用

塊通常代表小段的、自包含的代碼片段。

因此,它們封裝爲可以並行執行的工作單元額外有用,要麼用於在集合中進行遍歷,要麼在其他操作完成使作爲回調。

塊代替傳統回調函數的意義有兩個:

1. 它們允許在方法實現的調用中就近地寫入代碼。而且塊經常被作爲框架中一些方法的參數。

2. 它們允許訪問本地變量。在進行線程操作時,相比回調函數需要把所需的上下文信息植入數據結構中而言,塊直接訪問本地變量顯然更加簡單。

塊的聲明和創建

聲明塊變量

塊變量引用了塊。它的聲明語法類似函數指針,除了需要使用^代替*。

void (^blockReturningVoidWithVoidArgument)(void);

int (^blockReturningIntWithIntAndCharArguments)(int, char);

void (^arrayOfTenBlocksReturningVoidWithIntArgument[10])(int);

塊支持可變參數(…)。如果塊沒有參數,則必需使用void來代替整個參數列表。

塊是類型安全的,通過設置編譯選項,編譯器會檢查塊的調用、參數和返回類型。可以把塊變量轉換爲指針類型,但不能使用*對其解除引用——塊的長度在編譯時無法確定。

可以創建一個塊類型,這樣你就可以把塊當作一個可以反覆多次使用的符號:

typedef float (^MyBlockType)(float, float);

MyBlockType myFirstBlock = // ... ;

MyBlockType mySecondBlock = // ... ;

創建塊

塊以^開始,以;結束。下面顯示了塊的定義:

int (^oneFrom)(int);

oneFrom = ^(int anInt) {

return anInt - 1;

};

如果未顯式地聲明塊的返回值類型,可能會自動從塊代碼中推斷返回類型。如果參數列表爲void,而且返回類型依靠推斷,你可以省略參數列表的void。否則,當塊中存在return語句時,它們應當是精確匹配的(可能需要必要的類型轉換)。

全局塊

可以把塊定義爲全局變量,在文件級別上使用。

#import <stdio.h>

int GlobalInt = 0;

int (^getGlobalInt)(void) = ^{ return GlobalInt; };

塊和變量

本節描述塊和變量之間的交互,包括內存管理。

變量類型

在塊代碼內部,變量會被處理爲5種不同情況。

就像函數一樣,可以引用3種標準的變量:

全局變量,包括靜態變量

全局函數

本地變量及參數(在塊範圍內)

此外塊還支持兩種變量:

1. 在函數級別,是__block變量。它們在塊範圍內是可變的,如果所引用的塊被複制到堆後,它們也是被保護的。

2. const imports.

在方法體內,塊還可以引用O-C 實例變量,見 對象和塊變量 」.

在塊中使用變量有以下規則:

1. 可訪問在同一範圍內的全局變量包括靜態變量。

2. 可以訪問傳遞給塊的參數(如同函數參數)。

3. 同一範圍的棧(非static)變量視作const變量。它們的值類似塊表達式。嵌套塊時,從最近的作用域取值。

4. 在同一範圍內聲明的變量,如果有__block修飾符修飾,則值是可變的。在該範圍內包括同一範圍內的其他塊對該變量的改變,都將影響該作用域。具體見「__block 存儲類型」。

5. 在塊的範圍內(塊體)聲明的本地變量,類似於函數中的本地變量。塊的每次調用都會導致重新拷貝這些變量。這些變量可作爲const或參考(by-reference)變量。

下面演示本地非靜態變量的使用:

int x = 123;

void (^printXAndY)(int) = ^(int y) {

printf("%d %d/n", x, y);

};

printXAndY(456); // prints: 123 456

注意,試圖向x進行賦值將導致錯誤:

int x = 123;

void (^printXAndY)(int) = ^(int y) {

x = x + y; // error

printf("%d %d/n", x, y);

};

要想在塊內改變x的值,需要使用__block修飾x。見「__block存儲類型」。

__block 存儲類型

你可以規定一個外部的變量是否可變——可讀寫——通過使用__block存儲類型修飾符。__block存儲類似但不同於register,auto和static存儲類型。

__block變量在變量聲明的作用域、所有同一作用域內的塊,以及塊拷貝之間同享存儲。而且這個存儲將在棧幀(stack frame)釋放時得以保留,只要同一幀內申明的塊的拷貝仍然存活(例如,被入棧以便再次使用)。在指定作用域內的多個塊能同時使用共享變量。

作爲一種優化,塊存儲使用棧存儲,就如同塊自身一樣。如果使用Block_copy拷貝塊(或者在O-C向塊發送copy消息),變量被拷貝到堆裏。而且,__block變量的地址隨後就會改變。

__block變量有兩個限制:不能是可變長度的數組,也不能是包含C99可變長度數組的結構體。

下面顯示了__block變量的使用:

__block int x = 123; // x lives in block storage

void (^printXAndY)(int) = ^(int y) {

x = x + y;

printf("%d %d/n", x, y);

};

printXAndY(456); // prints: 579 456

// x is now 579

下面顯示了在塊中使用多種類型的變量:

extern NSInteger CounterGlobal;

static NSInteger CounterStatic;

{

NSInteger localCounter = 42;

__block char localCharacter;

void (^aBlock)(void) = ^(void) {

++CounterGlobal;

++CounterStatic;

CounterGlobal = localCounter; // localCounter fixed at block creation

localCharacter = 'a'; // sets localCharacter in enclosing scope

};

++localCounter; // unseen by the block

localCharacter = 'b';

aBlock(); // execute the block

// localCharacter now 'a'

}

對象和塊變量

塊提供了對O-C和C++對象的支持 。

O-C對象

在引用計數的情況下,當你在塊中引用一個O-C對象,對象會被retained。甚至只是簡單引用這個對象的實例變量,也是一樣的。

但對於__block標記的對象變量,就不一樣了。

注意:在垃圾回收的情況下,如果同時用__weak和__block修飾變量,塊可能不一定保證它是 可用 的。

如果在方法體中使用塊,對象實例變量的內存管理規則 比較微妙:

如果通過對象引用方式訪問實例變量,self retained;

如果通過值引用方式訪問實例變量,變量是retained;

下面代碼演示了這2種情況:

dispatch_async(queue, ^{

// instanceVariable is used by reference, self is retained

doSomethingWithObject(instanceVariable);

});

id localVariable = instanceVariable;

dispatch_async(queue, ^{

// localVariable is used by value, localVariable is retained (not self)

doSomethingWithObject(localVariable);

});

C++ 對象

一般,可以在塊中使用C++對象。在成員函數中對成員變量進行引用,儼然是對指針的引用,可以對其進行改變。如果塊被拷貝,有兩種結果:

如果有__block存儲類型的類,該類是基於棧的C++對象,通常會使用複製構造函數;

如果使用了其他塊中的基於棧的C++對象,它必需有一個const的複製構造函數。該C++對象使用該構造函數進行拷貝。

拷貝塊時,其引用的其它塊可能也被拷貝(從頂部開始)。如果有塊變量,並且在這個塊中引用了一個塊,那個塊也會被拷貝。

拷貝一個基於棧的塊時,你得到的是新的塊。拷貝一個基於堆的塊時,只是簡單的增加了retain數,然後把copy方法/函數的結果返回這個塊。

使用塊

塊的調用

如果把塊申明爲變量,可以把它當成函數使用,例如:

int (^oneFrom)(int) = ^(int anInt) {

return anInt - 1;

};

printf("1 from 10 is %d", oneFrom(10));

// Prints "1 from 10 is 9"

float (^distanceTraveled) (float, float, float) =

^(float startingSpeed, float acceleration, float time) {

float distance = (startingSpeed * time) + (0.5 * acceleration * time * time);

return distance;

};

float howFar = distanceTraveled(0.0, 9.8, 1.0);

// howFar = 4.9

但時常會將塊以參數形式傳遞給一個函數或方法,這樣,就會使用行內(inline)塊。

把塊作爲函數參數

在這種情況下,不需要塊申明。簡單地在需要把它作爲參數的地方實現它就行。如下所示,gsort_b是一個類似標準gsort_r的函數,它的最後一個參數使用了塊。

char *myCharacters[3] = { "TomJohn", "George", "Charles Condomine" };

qsort_b(myCharacters, 3, sizeof(char *), ^(const void *l, const void *r) {

char *left = *(char **)l;

char *right = *(char **)r;

return strncmp(left, right, 1);

});

// Block implementation ends at "}"

// myCharacters is now { "Charles Condomine", "George", TomJohn" }

注意,塊包含在函數的參數列表中。

接下來的例子顯示如何在dispath_apply函數中使用塊。dispatch_apply的聲明是:

void dispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block)(size_t));

這個函數把塊提交給dispatch隊列以進行調用。它有3個參數:要操作的次數;塊被提交到的隊列;塊——這個塊有一個參數——遍歷操作的當前次數。

可以用dispatch_apply簡單地打印出遍歷操作的索引:

#include <dispatch/dispatch.h>

size_t count = 10;

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_apply(count, queue, ^(size_t i) {

printf("%u/n", i);

});

把塊作爲參數使用

Cocoa提供了大量使用塊的方法。把塊作爲參數使用與使用其他類型的參數並無不同。

以下代碼判斷數組中前5個元素中含有給定filter集合的索引。

NSArray *array = [NSArray arrayWithObjects: @"A", @"B", @"C", @"A", @"B", @"Z",@"G", @"are", @"Q", nil];

NSSet *filterSet = [NSSet setWithObjects: @"A", @"Z", @"Q", nil];

BOOL (^test)(id obj, NSUInteger idx, BOOL *stop);

test = ^ (id obj, NSUInteger idx, BOOL *stop) {

if (idx < 5) {

if ([filterSet containsObject: obj]) {

return YES;

}

}

return NO;

};

NSIndexSet *indexes = [array indexesOfObjectsPassingTest:test];

NSLog(@"indexes: %@", indexes);

/*Output:

indexes: <NSIndexSet: 0x10236f0>[number of indexes: 2 (in 2 ranges), indexes: (0 3)]

*/

以下代碼判斷一個NSSet對象中是否包含指定的本地變量,如果是的話把另一個本地變量(found)設置爲YES(並停止搜索)。注意found被聲明爲__block變量,塊是在行內聲明的:

__block BOOL found = NO;

NSSet *aSet = [NSSet setWithObjects: @"Alpha", @"Beta", @"Gamma", @"X", nil];

NSString *string = @"gamma";

[aSet enumerateObjectsUsingBlock:^(id obj, BOOL *stop) {

if ([obj localizedCaseInsensitiveCompare:string] ==NSOrderedSame) {

*stop = YES;

found = YES;

}

}];

// At this point, found == YES

塊複製

一般,你不需要複製塊。只有當你希望在這個塊申明的範圍外使用它時需要複製它。複製將導致塊移動到堆中。

可以使用C函數釋放和複製塊。

Block_copy();

Block_release();

對於O-C,則可向塊發送copy,retain和release(以及autorelease)消息。

爲避免內存泄露,一個Block_copy()總是對應一個Block_release()。每個copy/retain總是有對應的release(或autorelease)——使用垃圾回收則例外。

避免的用法

一個塊聲明(即^{…})是一個本地棧式數據結構(stack-local data structure)的地址,這個地址就代表了塊。本地棧式數據結構是{}圍住的複合語句,因此應該避免如下用法:

void dontDoThis() {

void (^blockArray[3])(void);// array of 3 block references

for (int i = 0; i < 3; ++i) {

blockArray[i] = ^{ printf("hello, %d/n", i); };

// WRONG: The block literal scope is the "for" loop

}

}

void dontDoThisEither() {

void (^block)(void);

int i = random():

if (i > 1000) {

block = ^{ printf("got i at: %d/n", i); };

// WRONG: The block literal scope is the "then" clause

}

// ...

}

調試

可以在塊內設置斷點,並進行單步調試。在GDB會話中,使用invoke-block調用塊,比如:

$ invoke-block myBlock 10 20

如果需要傳遞C字符串,必需用雙引號把它引住。例如,向doSomethignWithString塊傳遞一個字符串:

$ invoke-block doSomethingWithString "/"this string/""