本章學習目標:
1. 了解何謂block。
2. 了解block 的使用方法。
Block是iOS在4.0 之後新增的程式語法,嚴格來說block的概念並不算是基礎程式設計的範圍,對初學者來說也不是很容易了解,但是在iOS SDK 4.0之後,block幾乎出現在所有新版的API之中,換句話說,如果不了解block這個概念就無法使用 SDK 4.0 版本以後的新功能,因此雖然block本身的語法有點難度,但為了使用iOS的新功能我們還是得硬著頭皮去了解這個新的程式概念。
在這一章的目標以了解如何使用block為主而不深入探討block底層的運作方式,至於有些初學者較少遇到的辭彙如「詞法作用域(lexical scope)」等,本章將不再多做解釋,待有興趣的讀者去請教Google大神吧。
X.1 初探Block
在這一小節我們先用一些簡單範例來導入 block的概念。
X.1.1 宣告和使用Block
我們使用「^」運算子來宣告一個block變數,而且在block的定義最後面要加上「;」來表示一個完整的述句(也就是將整個block定義視為前面章節所介紹的簡單述句,因為整個定義必須是一個完整的句子,所以必須在最後面加上分號),下面是一個block的範例:
int multiplier = 7;
int (^myBlock)(int) = ^(int num)
{
return num * multiplier;
};
我們使用下圖來解釋這個範例(請將文字框的字翻譯如下):
4 |
2 |
5 |
6 |
3 |
1 |
- 我們宣告一個「myBlock」變數,用「^」符號來表示這是一個block。
- 這是block的完整定義,這個定義將會指定給「myBlock」變數。
- 表示「myBlock」是一個回傳值為整數(int)的block。
- 它有一個參數,型態也是整數。
- 這個參數的名字叫做「num」。
- 這是block的內容。
值得注意的地方是block可以使用和本身定義範圍相同的變數,可以想像在上面的例子中multiplier和myBlock都是某一個函數內定義的兩個變數也就是這個變數都在某個函數兩個大括號「{」 和「 }」中間的區塊,因為它們的有效範圍是相同的,因此在block中就可以直接使用multiplier這個變數,此外當把block定義成一個變數的時,我們可以直接像使用一般函數般的方式使用它:
int multiplier = 7;
int (^myBlock)(int) = ^(int num)
{
return num * multiplier;
};
printf("%d", myBlock(3));
//結果會印出 21
X.1.2 直接使用 Block
在很多情況下,我們並不需要將block宣告成變數,反之我們可以直接在需要使用block的地方直接用內嵌的方式將block的內容寫出來,在下面的例子中qsort_b函數,這是一個類似傳統的qsort_t函數,但是直接使用block做為它的參數:
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);
}
);
X.1.3 __block 變數
一般來說,在block內只能讀取在同一個作用域的變數而且沒有辦法修改在block外定義的任何變數,此時若我們想要這些變數能夠在block中被修改,就必須在前面掛上 __block 的修飾詞,以上面第一個例子中的multiplier來說,這個變數在block中是唯讀的,所以multiplier = 7指定完後,在block中的multiplier就只能是7不能修改,若我們在block中修改multiplier,在編輯時就會產生錯誤,因此若想要在block中修改multiplier,就必須在multiplier前面加上 __block 的修飾詞,請參考下面的範例:
__block int multiplier = 7;
int (^myBlock)(int) = ^(int num)
{
if(num > 5)
{
multiplier = 7;
}
else
{
multiplier = 10;
}
return num * multiplier;
};
X.2 Block 概要
Block提供我們一種能夠將函數程式碼內嵌在一般述句中的方法,在其他語言中也有類似的概念稱做「closure」,但是為了配合Objective-C的貫例,我們一律將這種用法稱為「block」
X.2.1 Block 的功能
Block是一種具有匿名功能的內嵌函數,它的特性如下:
- 如一般的函數般能擁有帶有型態的參數。
- 擁有回傳值。
- 可以擷取被定義的詞法作用域(lexical scope)狀態。
- 可以選擇性地修改詞法作用域的狀態。
註:詞法作用域(lexical scope)可以想像成是某個函數兩個大括號中間的區塊,這個區塊在程式執行時,系統會將這個區塊放入堆疊記憶體中,在這個區塊中的宣告的變數就像是我們常聽到的區域變數,當我們說block可以擷取同一詞法作用域的狀態時可以想像block變數和其他區域變數是同一個層級的區域變數(位於同一層的堆疊裏),而block的內容可以讀取到和他同一層級的其他區域變數。 |
我們可以拷貝一個block,也可以將它丟到其他的執行緒中使用,基本上雖然block在iOS程式開發中可以使用在C/C++開發的程式片段,也可以在Objective-C中使用,不過在系統的定義上,block永遠會被視為是一個Objective-C的物件。
X.2.2 Block 的使用時機
Block一般是用來表示、簡化一小段的程式碼,它特別適合用來建立一些同步執行的程式片段、封裝一些小型的工作或是用來做為某一個工作完成時的回傳呼叫(callback)。
在新的iOS API中block被大量用來取代傳統的delegate和callback,而新的API會大量使用block主要是基於以下兩個原因:
- 可以直接在程式碼中撰寫等會要接著執行的程式,直接將程式碼變成函數的參數傳入函數中,這是新API最常使用block的地方。
- 可以存取區域變數,在傳統的callback實作時,若想要存取區域變數得將變數封裝成結構才能使用,而block則是可以很方便地直接存取區域變數。
X.3 宣告和建立 Block
X.3.1 宣告Block的參考(Reference)
Block變數儲存的是一個block的參考,我們使用類似宣告指標的方式來宣告,不同的是這時block變數指到的地方是一個函數,而指標使用的是「*」,block則是使用「^」來宣告,下面是一些合法的block宣告:
/*回傳void,參數也是void的block*/
void (^blockReturningVoidWithVoidArgument)(void);
/*回傳整數,兩個參數分別是整數和字元型態的block*/
int (^blockReturningIntWithIntAndCharArguments)(int, char);
/*回傳void,含有10個block的陣列,每個block都有一個型態為整數的參數*/
void (^arrayOfTenBlocksReturningVoidWinIntArgument[10])(int);
X.3.2 建立一個Block
我們使用「^」來開始一個block,並在最後使用「;」來表示結束,下面的範例示範了一個block變數,然後再定義一個block把它指定給block變數:
int (^oneFrom)(int); /*宣告 block 變數*/
/*定義block的內容並指定給上面宣告的變數*/
oneFrom = ^(int anInt)
{
return anInt = -1;
};
X.3.3 全域的Block
我在可以在檔案中宣告一個全域的block,請參考以下範例:
int GlobalInt = 0;
int (^getGlobalInt)(void) = ^(void){return GlobalInt;};
X.4 Block 和變數
接下來的這一小節我們將會介紹block和變數之間的互動。
X.4.1 變數的型態
我們可以在block中遇到平常在函數中會遇到的變數類型:
l 全域(global)變數或是靜態的區域變數(static local)。
l 全域的函數。
l 區域變數和由封閉領域(enclosing scope)傳入的參數。
除了上述之外block額外支援了另外兩種變數:
- 在函數內可以使用 __block 變數,這些變數在block中是可被修改的。
- 滙入常數(const imports)。
此外,在方法的實作裏,block可以使用Objective-C的實體變數(instance variable)。
下列的規則可以套用到在block中變數的使用:
- 可以存取全域變數和在同一領域(enclosing lexical scope)中的靜態變數。
- 可以存取傳入block的參數(使用方式和傳入函數的參數相同)。
- 在同一領域的區域變數在block中將視為常數(const)。
- 可以存取在同一領域中以 __block 為修飾詞的變數。
- 在block中宣告的區域變數,使用方式和平常函數使用區域變數的方式相同。
下面的例子介紹了區域變數(上述第三點)的使用方式:
int x = 123;
void (^printXAndY)(int) = ^(int y)
{
printf("%d %d\n", x, y);
};
//將會印出 123 456
printXAndY(456);
就如上面第三點所提到的,在上例中的int x = 123的變數x,在傳入block後將視同常數,因此若我們在block中試著去修改x的值時就會產生錯誤,下面的例子將會無法通過編譯:
int x = 123;
void (^printXAndY)(int) = ^(int y)
{
//下面這一行是錯的,因為x在這是一個常數不能被修改。
x = x + y;
printf("%d %d\n", x, y);
};
若在block中想要修改上面的變數x,必須將x宣告加上修飾詞 __block,請參考接下來這一小節的介紹。
X.4.2 __block 型態變數
我們可以藉由將一個由外部滙入block的變數放上修飾詞__block來讓這個變數由唯讀變成可以讀和寫,不過有一個限制就是傳入的變數在記憶體中必須是一個佔有固定長度記憶體的變數,__block修飾詞無法使用於像是變動長度的陣列這類不定長度的變數,請參考下面的範例:
//加上__block修飾詞,所以可以在block中被修改。
__block int x = 123;
void (^printXAndY)(int) = ^(int y)
{
x = x + y;
printf("%d %d\n", x, y);
};
//將會印出 579 456
printXAndY(456);
//x 將會變成 579;
下面我們使用一個範例來介紹各類型的變數和block之間的互動:
extern NSInteger CounterGlobal;
static NSInteger CounterStatic;
{
NSInteger localCounter = 42;
__block char localCharacter;
void (^aBlock)(void) = ^(void)
{
++CounterGlobal; //可以存取。
++CounterStatic; //可以存取。
CounterGlobal = localCounter; //localCounter在block建立時就不可變了。
localCharacter = 'a'; //設定外面定義的 localCharacter 變數。
};
++localCounter; //不會影響的block中的值。
localCharacter = 'b';
aBlock(); //執行block的內容。
//執行完後,localCharachter 會變成 'a'
}
X.4.3 物件和Block變數
Block支援在Objective-C、C++物件和其他block中當作變數來使用,不過因為在大部分的情況我們都是使用Objective-C的撰寫程式,因此在這一小節我們僅針對Objective-C的情況進行介紹,至於其他兩種情況就留給有興趣的讀者再自行深入研究了。
x.4.3.1 Objective-C 物件
在擁有參考計數(reference-counted)的環境中,若我們在block中參考到Objective-C的物件,在一般的情況下它將會自動增加物件的參考計數,不過若以__block為修飾詞的物件,參考計數則是不受影響。
如果我們在Objective-C的方法中使用block時,以下幾個和記憶體管理的事是需要額外注意的:
l 若直接存取實體變數(instance variable),self的參考計數將被加1。
l 若透過變數存取實體變數的值,則只變數的參考計數將被加1。
以下程式碼說明上面兩種情況,在這個假設instanceVariable是實體變數:
dispatch_async(queue, ^{
//因為直接存取實體變數instanceVariable,所以self的retain count會加1
doSomethingWithObject(instanceVariable);
});
id localVaribale = instanceVariable;
dispatch_async(queue, ^{
//localVariable 是存取值,所以這時只有localVariable的retain count加1
//self 的 return count 並不會增加。
doSomethingWithObject(localVaribale);
});
X.5 使用Block
這一小節我們將會對block的使用方式做一些初步的介紹
X.5.1 呼叫一個Block
當block宣告成一個變數時,我們可以像使用一般函數的方式來使用它,請參考下面兩個範例:
int (^oneFrom)(int) = ^(int anInt) {
return anInt - 1;
};
printf("1 from 10 is %d", oneFrom(10));
//結果會顯示: 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
在一般常見的情況中,若是將block當做是參數傳入函數,我們通常會使用「內嵌」的方式來使用block。
X.5.2 將Block當作函數的參數
我們可以像使用一般函數使用參數的方式,將block以函數參數的型式傳入函數中,在這種情況下,大多數我們使用block的方式將不會傾向宣告block而是直接以內嵌的方式來將block傳入,這也是目前新版SDK中主流的做法,我們將補充前面章節的例子來說明:
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的終點。
);
//最後的結果為:{"Charles Condomine", "George", "TomJohn"}
在上面的例子中,block本身就是函數參數的一部分,在下一個例子中dispatch_apply函數中使用block,dispatch_apply的定義如下:
void
dispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block)(size_t));
這個函數將一個block提交到發送佇列(dispatch queue)中來執行多重的呼叫,只有當佇列中的工作都執行完成後才會回傳,這個函數擁有三個變數,而最後一個參數就是block,請參考下面的範例:
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);
});
X.5.3 將 Block 當作方法的參數
在SDK中提供了許多使用block的方法,我們可以像傳遞一般參數的方式來傳遞block,下面這個範例示範如何在一個陣列的前5筆資料中取出我們想要的資料的索引值:
//所有的資料
NSArray *array = [NSArray arrayWithObjects: @"A", @"B", @"C", @"A", @"B", @"Z",@"G", @"are", @"Q", nil];
//我們只要這個集合內的資料
NSSet *filterSet = [NSSet setWithObjects: @"A", @"B", @"Z", @"Q", nil];
BOOL (^test)(id obj, NSUInteger idx, BOOL *stop);
test = ^ (id obj, NSUInteger idx, BOOL *stop) {
//只對前5筆資料做檢查
if (idx < 5) {
if ([filterSet containsObject: obj]) {
return YES;
}
}
return NO;
};
NSIndexSet *indexes = [array indexesOfObjectsPassingTest:test];
NSLog(@"indexes: %@", indexes);
//結果:indexes: <NSIndexSet: 0x6101ff0>[number of indexes: 4 (in 2 ranges), indexes: (0-1 3-4)]
//前5筆資料中,有4筆符合條件,它們的索引值分別是 0-1, 3-4
X.5.4 該避免的使用方式
在下面的例子中,block是for迴圈的區域變數因此在使用上必須避免將區域的block指定給外面宣告的block:
//這是錯誤的範例,請勿在程式中使用這些語法!!
void dontDoThis() {
void (^blockArray[3])(void); // 3個block的陣列
for (int i = 0; i < 3; ++i) {
blockArray[i] = ^{ printf("hello, %d\n", i); };
//注意: 這個block定義僅在for迴圈有效。
}
}
void dontDoThisEither() {
void (^block)(void);
int i = random():
if (i > 1000) {
block = ^{ printf("got i at: %d\n", i); };
// 注意: 這個block定義僅在if後的兩個大括號中有效。
}
// ...
}
留言列表