前言:
以下文字都是摘自ios与osx多线程和内存管理一书,方便以后自己过来查阅的.
本文梗概:什么是Blocks以及Blocks的语法和类型变量。
1. 什么是Blocks
:
Blocks是c语言的扩充功能。用一句话来表示Blocks的扩充功能:带有自动变量(局部变量)的匿名函数(不带有名称的函数,c语言的标准不允许存在这样的函数,而通过blocks,源代码中就能够使用匿名函数。)
int func(int count);
声明一个名为func的函数
int result = func(10);
通过使用func名称来调用该函数
int result = (*funcptr)(10);
像上面这样子,使用函数指针来代替直接调用函数,似乎不用知道函数名。但其实使用函数指针也需要知道函数名:像下面代码这样子,在赋值给函数指针时,若不使用想赋值的函数的名称,就无法取得该函数的地址。
int (*funcptr) (int) = &func;
int result = (*funcptr) (10);
#####知道了什么是匿名函数,那么“带有自动变量值”是什么?
首先回顾c的函数中可能使用的变量.
- 自动变量(局部变量)
- 函数的参数
- 静态变量(静态局部变量)
- 静态全局变量
- 全局变量
其中,在函数的多次调用之间能够传递值的变量有: - 静态变量(静态局部变量)
- 静态全局变量
- 全局变量
虽然这些变量的作用域不同,但在整个程序当中,一个变量总保持在一个内存区域。因此,虽然多次调用函数,但该变量值总能保持不变。
下面是案例分析
:
int buttonId = 0;
void buttonCallback(int event)
{
printf("buttonId:%d enent=%d\n",buttonId,event);
}
如果只有一个按钮,该源码无问题。但是多个按钮呢?
int buttonId;
void buttonCallback(int event)
{
printf("buttonId:%d event=%d\n",buttonId,event);
}
void setButtonCallbacks()
{
for (int i = 0;i < BUTTON_MAX; ++i)
{
buttonId = i;
setButtonCallback(BUTTON_IDOFFSET + i,&buttonCallback);
}
}
该源码的问题很明显。全局变量buttonId只有一个,所有回调都使用for循环最后的值。当然如果不使用全局变量,回调方会将按钮的ID作为函数参数传递,就能解决该问题。
void buttonCallback(int buttonId, int event)
{
printf("buttonId:%d enent=%d\n",buttonId,event);
}
但是,回调方在保持回调函数的指针以外,还必须保持回调方的按钮ID。
c++和objective-c使用类可保持变量值且能够多次持有该变量自身。它会声明持有成员变量的类,由类生成的实例或对象保持该成员变量的值。
思考下刚才例子中用来回调按钮的类。
但是,使用类来做刚才例子中的回调按钮,可以想象到,声明并实现c++,Objective-c的类增加了代码的长度。
Blocks提供了类似由c++和objective-c类生成实例或对象来保持变量值的方法,其代码量与编写c语言函数差不多。如“带有自动变量值”,Blocks保持自动变量的值。
下面我们使用Blocks实现上面的按钮回调:
void setButtonCallbacks()
{
for (int i = 0; i < BUTTON_MAX; ++i)
{
setButtonCallbackUsingBlock(BUTTON_IDOFFSET + i,^(int event){
printf("buttonId:%d event=%d\n",i,event);
});
}
}
该源码将“带有自动变量i值的匿名函数”设定为按钮的回调。Blocks中将该匿名函数部分称为“Block literal”,或简称为“Block”。
像这样,使用Blocks可以不声明c++和objective-c类,也没有使用静态变量,静态全局变量或全局变量时的问题,仅用编写c语言函数的源代码量即可使用带有自动变量值的匿名函数。
另外“带有自动变量值的匿名函数”这一概念并不是指Blocks,它还存在于其他许多程序语言中。在计算机科学中,此概念也称为闭包(Closure),lambda计算()等。
objective-c的block在其他程序语言中的名称:
程序语言 | Block的名称 |
---|---|
C + Blocks | Block |
Smalltalk | Block |
Ruby | Block |
LISP | Lambda |
Python | Lambda |
C++11 | Lambda |
Javascript | Anonymous function |
2. Blocks模式
:
Block语法
:
下面讲解带有自动变量值的匿名函数Block的语法,即Block表达式语法(Block Literal Syntax).
完整形式的Block语法与一般的c语言函数定义相比,仅有2点不同。
1. 没有函数名.
因为它是匿名函数。
2. 带有"^".
返回值类型前带有”^”(插入记号,caret)记号,因为OS X,IOS应用程序的源代码中将大量使用Block,所以插入该记号便于查找。
以下为Block语法的BN范式
Block_literal_expression ::= ^ block_decl compound_statement_body
block_decl ::=
block_decl ::= parameter_list
block_decl ::= type_expression
即使不了解BN范式,通过说明也能有个概念,如下所示:^
返回值类型
参数列表
表达式
- 返回值类型:同c语言函数的返回值类型.
- 参数列表:同c语言函数的参数列表.
- 表达式:同c语言函数中允许的表达式. 当然与c函数一样,表达式中含有return语句时,其类型必须与返回值类型相同.
例如可以写出如下形式的Block语法:
^int (int count){return count + 1;}
Block语法可省略好几个项目。
省略返回值类型
^
参数列表
表达式
省略返回值类型时,如果表达式中有return语句就使用该返回值的类型,如果表达式中没有return语句久使用void类型。表达式中含有多个return语句时,所有return的返回值类型必须相同。
前面源码省略其返回值类型时如下:^(int count){return count + 1;}
如果不使用参数,参数列表也可省略
^
表达式
以下为不使用参数的Block语法:
^void (void){ printf("Blocks\n");}
以下为省略参数的Block:
^{printf("Blocks\n");}
Block类型变量
:
在定义c语言函数时,就可以将所定义函数的地址赋值给函数指针类型变量中
int func(int count)
{
return count + 1;
}
int (*funcptr)(int) = &func;
这样一来,函数func的地址就能赋值给函数指针类型变量funcptr中了。
同样,也可将Block语法赋值给声明为Block类型的变量中。在有关Blocks的文档中,“Block”既指代码中的Block语法,也指由Block语法所生成的值。
声明Block类型变量的示例如下:
int (^blk)(int);
与前面使用函数指针的代码对比可知,声明Block类型变量仅仅是将声明函数指针类型变量的“*”变为“^”。
该Block类型变量与一般的c语言变量完全相同,可作为以下用途使用。
*自动变量
*函数参数
*静态变量
*静态全局变量
*全局变量
使用Block语法将Block赋值为Block类型变量。
int (^blk)(int) = ^(int count){return count + 1;};
由“^”开始的Block语法生成的Block被赋值给变量blk中。因为与通常的变量相同,所以当然也可以由Block类型变量向Block类型变量赋值。
int (^blk1)(int) = blk;
int (^blk2)(int);
blk2 = blk1;
在函数参数中使用Block类型变量可以向函数传递Block。
void func(int (^blk)(int))
{
在函数返回值中指定Block类型,可以将Block作为函数的返回值返回。
int (^func()(int))
{
return ^(int count){return count + 1;};
}
由此可知,在函数参数和返回值中使用Block类型变量时,记述方式很复杂。
我们可以像使用函数指针类型时那样,使用typedef来解决该问题。
typedef int (^blk_t)(int);
如上,通过使用typedef可声明“blk_t”类型变量。我们试着在以上例子中的函数参数和函数返回值部分里使用一下。
/*原来的记述方式
void func(int (^blk)(int))
{
*/
void func(blk_t blk)
{
/*原来的记述方式
int (^func()(int))
{
*/
blk_t func()
{
另外,将赋值给Block类型变量中的Block方法像c语言通常的函数调用那样使用,这中方法与使用函数指针类型变量调用函数的方法几乎完全相同。变量funcptr为函数指针类型时,这样调用函数指针类型变量:
int result = (*funcptr)(10);
变量blk为Block类型的情况下,这样调用Block类型变量:
int result = blk(10);
通过Block类型变量调用Block与c语言通常的函数调用没有区别。在函数参数中使用Block类型变量并在函数中执行Block的例子如下:
int func(blk_t blk, int rate)
{
return blk(rate);
}
在objective-c中也可使用:
- (int) methodUsingBlock:(blk_t)blk rate:(int)rate
{
return blk(rate);
}
Block类型变量可完全像通常的c语言变量一样使用,因此也可以使用指向Block类型变量的指针,即Block的指针类型变量。
typedef int (^blk_t)(int);
blk_t blk = ^(int count){return count + 1;};
blk_t *blkptr = &blk;
(*blkptr)(10);
由此可知Block类型变量可像c语言中其他类型变量一样使用。
截获自动变量值
:
通过Block语法和Block类型变量的说明,我们已经理解了“带有自动变量值的匿名函数”中匿名函数。“带有自动变量值”在Blocks中表现为“截获自动变量值”。截获自动变量值的实例如下:
int main()
{
int dmy = 256;
int val = 10;
const char *fmt = "val = %d\n";
void (^blk)(void) = ^{printf(fmt, val);};
val = 2;
fmt = "These values were changed. val = %d\n";
blk();
return 0;
}
代码中,Block语法的表达式使用的是它之前声明的自动变量fmt和val。Blocks中,Block表达式截获所使用的自动变量的值,即保存该自动变量的瞬间值。因为Block表达式保存了自动变量的值,所以在执行Block语法后,即使改写了Block中使用的自动变量的值也不会影响Block执行时自动变量的值。代码中就在Block语法后改写了Block中的自动变量val和fmt。
执行结果为:val = 10;
执行结果并不是我们期望的那样。而是执行Block语法时的自动变量的瞬间值。 该Block语法在执行时,字符串指针“val = %d\n”被赋值到自动变量fmt中,int值10被赋值到自动变量val中,因此这些值被保存(即被截获),从而在执行块时使用。
这就是自动变量值的截获
_
_block说明符
_:实际上,自动变量值只能保存执行Block语法瞬间的值。保存后就不能改写该值。
若想在Block语法的表达式中将值赋给在Block语法外声明的自动变量,需要在该自动变量上附加__block说明符,就能在Block内赋值。
__block int val = 0;
val称为__block变量。
截获的自动变量
:如果将值赋值给Block中截获的自动变量,就会产生编译错误。
int val = 0; void (^blk)(void) = ^{val = 1;};
会产生如下的编译错误:
error: variable is not assignable (missing __block type specifier)
void (^blk)(void) = ^(val = 1;);
~~~ ^
那么截获Objective-C对象,调用变更该对象的方法也会产生编译错误吗?
id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
id obj = [[NSObject alloc] init];
[array addObject:obj];
};
这是没有问题的,但向截获的变量array赋值则会产生编译错误。代码中截获的变量值为NSMutableArray类的对象。如果用C语言来描述,即是截获NSMutableArray类对象用的结构体实例指针。虽然赋值给截获的自动变量array的操作会产生编译错误,但使用截获的值却不会存在任何问题。下面代码向截获的自动变量赋值,会产生编译错误。
id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
array = [[NSMutableArray alloc] init];
};
这时候加上__block说明符就好了。
另外,在使用c语言数组时必须小心使用其指针,代码如下:
const char text[] = "hello";
void (^blk)(void) = ^{
printf("%c\n",text[2]);
};
只是使用C语言的字符串字面量数组,并没有向截获的自动变量赋值,因此应该没有问题?但实际上会产生以下编译错误:
error: cannot refer to declaration with an array type inside block
printf("%c\n",text[2]);
note: declared here
const char text[] = "hello";
^
这是因为在现在的Blocks中,截获自动变量的方法并没有实现对c语言数组的截获。这时使用指针就可以解决问题。
const char *text[] = "hello";
void (^blk)(void) = ^{
printf("%c\n",text[2]);
};