iOS塊語法詳解(block程式設計)
——譯自Apple Reference Library《Blocks Programming Topic》
簡介
塊物件是C語言的句法和執行時特性。它類似於標準C函式,但可以將程式碼、變數繫結到堆(heap)、棧(stack)。一個塊還維護了一系列的狀態,這些狀態或資料影響著執行的結果。
可以把塊組成函式表示式,用於傳遞給API,或者使用在多執行緒裡。最有用的是回撥,因為塊在回撥時能把程式碼和資料一起傳送。
在OSX 10.6的Xcode中,可以使用塊,它隨GCC和 Clang 一起整合。在OSX 10.6及iOS 4.0以後支援塊語法。 塊執行時是開源的,它能被整合到 中。標準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/""