在前两篇中,我们介绍了一些关于C语言的重要概念,指针跟struct,这些基础知识是我们深入学习block的前提,在这里将尽可能的将关于block的相关知识讲解的详细一些,分享给那些想要提升自己的小伙伴们,文章会比较长,一下子写完实在是有些力不从心,所以会采取不断更新的方式。
首先看一下什么是Blocks
Blocks是C语言的扩充功能。可以用一句话来表示Blocks的扩充功能:带有自动变量(局部变量)的匿名函数。
顾名思义,所谓匿名函数就是不带有名称的函数。C语言的标准不允许存在这样的函数。例如如下的源代码:
int func(int count);
它声明了名为func的函数。下面的源代码中为了调用该函数,必须使用该函数的名称func。
int result = func(10);
如果像下面这样,使用函数指针来代替直接调用函数,那么似乎不用知道函数名也能够使用该函数。
int result = (*funcptr)(10);
但其实使用函数指针也仍然要知道函数名称。像以下的源代码一样,在赋值给函数指针时,若不使用想赋值的函数的名称,就无法获得该函数的地址。
int (*funcptr)(int) = &func;
int result = (*funcptr)(10);
而通过Blocks,源代码中就能够使用匿名函数了,即不带名称的函数。对于程序员而言,命名就是工作的本质,函数名、变量名、方法名、属性名、类名和框架等都必须具备。而能够编写不带名称的函数对程序员来说相当具有吸引力。
到这里,我们知道了“带有自动变量值的匿名函数”中“匿名函数”的概念。那么“带有自动变量值”究竟是什么呢?
首先回顾一下在C语言的函数中可能使用的变量。(以下的变量在内存中的分配是不一样的,可以参考这篇文章了解 C语言中的内存分配。)
- 自动变量(局部变量)
- 函数的参数
- 静态变量(静态局部变量,方法中用static声明的变量)
- 静态全局变量(方法外用static声明的变量)
- 全局变量
其中,在函数的多次调用之间能够传递的变量有:
- 静态变量(静态局部变量)
- 静态全局变量(只能在本文件中使用的全局变量)
- 全局变量(外接也可以使用的全局变量)
虽然这些变量的作用域不同,但在整个程序中,一个变量总保持在一个内存区域。因此,虽然多次调用函数,但该变量值总能保持不变,在任何时候以任何状态调用,使用的都是同样的变量值。
另外,“带有自动变量值的匿名函数”这一概念并不仅指Blocks,它还存在于其他许多程序语言中。
程序语言 | Block的名称 |
---|---|
C + Blocks | Block |
Smalltalk | Block |
Ruby | Block |
LISP | Lambda |
Python | Lambda |
C++11 | Lambda |
Javascript | Anonymous function |
Blocks 模式
一、Block语法
下面我们详细讲解一下带有自动变量值得匿名函数Block的函数,即Block表达式语法。先看一个简单点的形式:
^(int event){
printf("event:%d",event);
}
实际上,上述Block语法使用了省略方式,其完整形式如下:
^void(int event){
printf("event:%d",event);
}
如上所示,完整形式的Block语法与一般的C语言函数定义相比,仅仅有两点不同。
- 没有函数名
- 带有“^”
第一点不同是没有函数名,因为它是匿名函数。第二点不同是返回值类型前带有“^”(插入记号,caret)记号。因为OS X、iOS应用程序的源代码中将大量使用Block,所以插入该记号便于查找。
Block的一般范式如下
^ 返回值类型 参数列表 表达式
返回值类型同C语言函数的返回值类型,“参数列表”同C语言的参数列表,“表达式”同C语言函数中允许使用的表达式。当然与C语言函数一样,表达式中含有return语句时,其类型必须与返回值类型相同。
例如写出如下形式的Block语法:
^ int (int count){return count+1};
返回值的类型可以省略
虽然前面出现过省略方式,但Block的语法可省略好几个项目。首先是返回值类型,如下所示:
^ 参数列表 表达式(返回值类型被省略)
当你省略返回值类型时,如果表达式中有return
语句就使用该返回值的类型,如果表达式中没有return
语句就使用void
类型。表达式中含有多个return
语句时,所有return
的返回值类型必须相同。
前面的^ int (int count){return count+1};
源代码省略其返回值类型时如下所示:
^ (int count){return count+1};
该block语法将按照return语句的类型,返回int形的返回值。
如果不使用参数,参数列表也可以省略。以下为不使用参数的Block语法:
^ void(void){printf("blocks\n")};
可以进一步的省略:
^ {printf("blocks\n")};
二、Block类型变量
上面提到的Block语法单单从其技术方式上来看,除了没有名称以及带有“^”以外,其他都与C语言函数定义相同。在定义C语言函数时,就可以将所定义函数的地址赋值给函数指针类型变量中。
int func(int count){
return count+1;
}
int (*funcptr)(int) = &func;
这样一来,函数func的地址就能赋值给函数指针类型变量funcptr中了。
同样地,在Block语法下,可将Block语法赋值给声明为Block类型的变量中。即源代码中一旦使用Block语法就相当于生成了可赋值给Block类型变量的“值”。Blocks中由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;//声明一个block类型变量blk1,并将blk赋值给blk1。
int (^blk2)(int);//声明一个block类型变量blk2。
blk2 = blk1;
在函数参数中使用Block类型变量可以向函数传递Block。
void func(int(^blk)(int)){}//int(^blk)(int)可以看做是一个block类型的变量blk。
在函数返回值中指定block类型,可以将block作为函数的返回值返回。
int (^func())(int){
return ^(int count){
return count+1
};//block类型为int (^func()),参数列表为空,不是没有参数,而是可以任意参数。返回的^(int count){return count+1}可以作为是上述类型的block变量。
}
从上面可以看到,在函数参数和返回值中使用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();
通过使用typedef
,函数定义就变得更容易理解了。
另外,将赋值给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语法在执行时,字符串指针被赋值到自动变量fmt中,int值10被赋值到自动变量val中,因此这些值被保存(即被截获),从而在执行块时使用。
这就是自动变量值的截获。
四、__block 说明符
实际上,自动变量值截获只能保存执行Block语法瞬间的值。保存后就不能改写该值。下面我们尝试改写截获的自动变量值,看看会出现什么结果。下面的源代码中,Block语法之前声明的自动变量val值被赋予1。
int val = 0;
void (^blk)(void) = ^{val = 1;};
blk();
printf ("val = %d\n",val);
以上为在Block语法外声明的给自动变量赋值的源代码。该源代码会产生编译错误。
error: variable is not assignable(missing __block type specifier)
void (^blk)(void) = {val = 1; };
~~~ ^
若想在Block语法的表达式中将值赋给在Block语法外声明的自动变量,需要在该自动变量上附加__block
说明符。该源代码中,如果给自动变量声明int val
附加__block
说明符,就能实现在Block内赋值。
__block int val = 0;
void(^blk)(void) = ^{val = 1;};
blk();
printf("val = %d\n",val);
该源代码的执行结果为
val = 1;
使用附有__block
说明符的自动变量可在Block中赋值,该变量称为__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];
};
error: variable is not assignable(missing __block type specifier)
array = [[NSMutableArray alloc]init];
~~~ ^
这种情况下,需要给截获的自动变量附加__block
说明符。
__block id array = [[NSMutableArray alloc]init];
void (^blk)(void) = ^{
array = [[NSMutableArray alloc]init];
};
接下来会介绍block是如何实现的,会从它的c++源码层面进行分析。(未完待续。。)
参考书籍:Objective-C高级编程
网友评论