最近看了松本行弘(日)编写的代码的未来这本书,其中有一个章节详细介绍了有关函数闭包的内容,后来联想到IOS中的代码块(Block),其实它就是对闭包的一种实现。
那么究竟什么是函数的闭包呢?
一 函数对象
在一些编程语言中提供了函数对象的概念,有些人也将其叫做闭包(Closure),但其实函数对象不一定是闭包。
所谓函数对象,顾名思义,就是将对象当做函数来使用。不过这里的对象不一定是面向对象中所指的那个对象,而更像是编程语言所操作的数据这个意思。
(有关面向对象中函数对象的概念见下面的补充说明)。
例如在C 语言中,我们可以获取一个函数的指针,并通过指针间接调用该函数。这就是C语言概念中的对象。如下面代码所示:
void HelloWorld( NSString * hello)
{
NSLog ( @"Hello World!" );
}
void HelloHadoop( NSString *hadoop)
{
NSLog ( @"Hello Hadoop!" );
}
int main( int argc, const char * argv[])
{
@autoreleasepool {
void (*FuncPointer)( NSString *args) = NULL ;
FuncPointer = HelloWorld ;
FuncPointer( @"Hello World" );
FuncPointer = HelloHadoop ;
FuncPointer( @"Hello Hadoop" );
}
return 0 ;
}
FuncPointer是一个指向拥有一个NSString形式参数,返回值是void函数指针,而函数HelloWorld和HelloHadoop都满足以上的要求,可以被FuncPointer指针调用。
函数对象,也就是将函数作为值来进行利用的方法,函数对象对我们编程来说有什么用呢?最大的用途就是高阶函数,就是用函数作为参数的函数。
二 用函数参数提高通用性
在C语言标准函数库中,提供了一个通用性的排序函数,它的名字是qsort,函数原型为:
void qort(void *base,size_t nmemb,size_t size,int (*compar)(const void *,const void *)
第一个参数表示待排序数组的首地址,第二个参数表示数据元素的个数,第三个参数表示数组元素大小,关键是第四个参数为一个函数指针可以传入任意类型的两种元素的比较,当两个元素相等时返回0,a>b返回正整数,否则返回负数。
qsort函数是通过将另外一个函数作为参数使用,来实现通用排序功能。高阶函数这样的方式,通过将一部分处理以函数的形式转移到外部,从而实现了算法的通用化。
三 函数指针的局限
请细细分析下面的这段代码:
#include
<stdio.h>
#include
<stdlib.h>
struct node {
struct node *next;
int val;
}; /*结构体的定义*/
typedef
void
(*func_t) (
int
); /*函数指针类型*/
void foreach( struct node *list, func_t func)
{
while (list != NULL ) {
func(list-> val );
list = list-> next ;
}
}
void f( int n)
{
printf ( "node(?) = %d\n" ,n);
}
int main( int argc, const char * argv[])
{
@autoreleasepool {
struct node *list = NULL ,*l;
for ( int i= 0 ;i< 4 ;++i)
{
l = malloc ( sizeof ( struct node ));
l-> val = i;
l-> next = list;
list = l;
}
int i = 0 ;
l=list;
while (l != NULL)
{
printf("node(%d) = %d\n",i++,l->val);
l = l->next ;
}
foreach(list, f);
}
return 0 ;
node(0) = 3
node(1) = 2
node(2) = 1
node(3) = 0
node(?) = 3
node(?) = 2
#include
<stdio.h>
#include <stdlib.h>
struct node {
struct node *next;
int val;
};
typedef void(^func_t) (int);
void foreach(struct node *list,func_t func)
{
while (list != NULL) {
func(list->val);
list = list->next;
}
}
void f(int n)
{
printf("node(?) = %d\n",n);
}
int main(int argc, const char * argv[])
{
@autoreleasepool {
struct node *list = NULL,*l;
for(int i=0;i<4;++i)
{
l = malloc(sizeof(struct node));
l->val = i;
l->next = list;
list = l;
}
__block int i = 0;
l=list;
foreach(list, ^(int n){printf("node(%d) = %d\n",i++,l->val);});
}
return 0;
node(1) = 3
node(2) = 3
五 总结
void foreach( struct node *list, func_t func)
{
while (list != NULL ) {
func(list-> val );
list = list-> next ;
}
}
void f( int n)
{
printf ( "node(?) = %d\n" ,n);
}
int main( int argc, const char * argv[])
{
@autoreleasepool {
struct node *list = NULL ,*l;
for ( int i= 0 ;i< 4 ;++i)
{
l = malloc ( sizeof ( struct node ));
l-> val = i;
l-> next = list;
list = l;
}
int i = 0 ;
l=list;
while (l != NULL)
{
printf("node(%d) = %d\n",i++,l->val);
l = l->next ;
}
foreach(list, f);
}
return 0 ;
}
输出结构:
node(1) = 2
node(2) = 1
node(3) = 0
node(?) = 3
node(?) = 2
node(?) = 1
分析:函数指针的局限性主要体现在下面的两个方面:
1.foreach函数的第二个参数所指向的函数必须在远离循环体的地方单独进行定义,这是C语言函数指针的第一个缺点,在大多数语言中,函数都可以在需要调用的地方当场定义,这个是C语言所固有的缺点。
2.从运行结果看在,与while语句不同,foreach的循环实际是在另一个函数中执行的,因此无法从函数中访问位于外部的局部变量i.
函数对象与函数指针相比较有两个方面的优点:首先如果被重载的调用操作符是inline函数则编译器能够执行内联编译,提供可能的性能好处;其次函数对象可以拥有任意数目的额外数据,用这些数据可以缓冲结果,也可以缓冲有助于当前操作的数据。
四 闭包
首先看下面的代码:
#include <stdlib.h>
struct node {
struct node *next;
int val;
};
typedef void(^func_t) (int);
void foreach(struct node *list,func_t func)
{
while (list != NULL) {
func(list->val);
list = list->next;
}
}
void f(int n)
{
printf("node(?) = %d\n",n);
}
int main(int argc, const char * argv[])
{
@autoreleasepool {
struct node *list = NULL,*l;
for(int i=0;i<4;++i)
{
l = malloc(sizeof(struct node));
l->val = i;
l->next = list;
list = l;
}
__block int i = 0;
l=list;
foreach(list, ^(int n){printf("node(%d) = %d\n",i++,l->val);});
}
return 0;
}
输出结果:
node(0) = 3node(1) = 3
node(2) = 3
node(3) = 3
分析:
1.重点看加粗的那行代码,我们可以看到使用block可以在调用foreach的时候直接将代码块作为foreach的第二个参数传入进去,而不像C语言中的函数调用那种远离foreach。
2.在block中可以直接访问代码块外的局部变量i。(注:为了在代码块中改变i的值需要加__block修饰)
五 闭包的特性
1.作用域
作用域即变量的可见范围,也就是某个变量可以被访问的范围,在C语言版的foreach函数无法实现的索引显示功能,而将block函数对象作为foreach第二个参数的时候,从block内可以访问外部的变量i,因此,从函数对象中能够访问外部变量进行访问(引用、更新),是闭包的构成要件之一。
2.生存周期
生存周期就是变量的寿命。看下面的例子:
typedef void (^myBlock)(void );
myBlock extend()
{
__block int n = 0;
void (^myFunc)( void) = ^( void)
{
n++;
NSLog(@"count=%d",count);
};
return [[myFunc copy] autorelease];
}
int main(int argc, const char * argv[])
{
@autoreleasepool {
myBlock block = extend();
block();
block();
}
return 0;
}
局部变量
n
是在extent函数中声明的,而extent函数已经执行完毕了。变量脱离作用域之后不是应该就消失了吗?不过就这个运行结果来看,即便函数执行完毕了,局部变量n貌似还在某个地方继续存货着。(注:上面的实例没有使用ARC,为了使返回的Block临时变量不被销毁延长其生命周期必须要要使用[myFunc copy],而为了最后的回收要使用autorelease)
这个就是生命周期,也就是说,这个从属于外部(代码块外部)作用域中的局部变量,被函数对象给“封闭”在里面了。闭包这个词原来就是封闭的意思。被封闭起来的变量的寿命,与封闭它的对象寿命相等,也就是说,当封闭这个变量的函数对象不再被访问,被垃圾回收器回收掉时,这个变量的寿命也就终结了。
现在大家明白了,将局部变量这一个环境封闭起来的结构被称为闭包。
五 总结
由上面的例子知道,当函数每次被执行时,作为隐藏上下文的局部变量n就会被引用和更新,也就是说,这意味着函数(过程)与数据结合起来了。
“过程与数据的结合”是形容面向对象中“对象”时经常使用的表达。对象在数据中以方法的形式内含了过程,而闭包则是在过程中以环境的形式内含了数据。即对象和闭包是同一个事物的正反两个方面。
补充
C++中函数对象
1.什么是函数对象
函数对象就是重载了“()”操作符的对象,也就是说如果一个类重载了“()”操作符,由它创建的对象就是函数对象。 因为函数对象本身是一个类的实例,因此它可以有自己的成员,这样,可以用这些成员保存一些普通函数不能轻易保存的(但可以通过静态局部变量和全局变量保存)的信息。同时,通过这个类的其他方法,可以对它的成员变量进行初始化和检查。 函数对象是比函数更加通用的概念,因为函数对象可以定义跨越多次调用的可持久的部分(类似静态局部变量),同时又能够从对象的外面进行初始化和检查(和静态局部变量不同)。
例如:
int sum(int i, int j) { return i+j;
很可能它是一个函数对象:
class Sum {
public:
int operator()(int i, int j) { return i+j; }
};
Sum sum;
而大多数情况是
template <class T>
class Sum {
public:
T operator()(T i, T j) { return i+j; }
};
Sum <int> sum;
在标准库中,函数对象被广泛地使用以获得弹性”,标准库中的很多算法都可以
使用函数对象或者函数来作为自定的回调行为。