C/C++语言中闭包的探究及比较
(感谢投稿人 @思禽饮霜 )
这里主要讨论的是C语言的扩展特性block。该特性是Apple为C、C++、Objective-C增加的扩展,让这些语言可以用类Lambda表达式的语法来创建闭包。前段时间,在对CoreData存取进行封装时(让开发人员可以更简洁快速地写相关代码),我对block机制有了进一步了解,觉得可以和C++ 11中的Lambda表达式相互印证,所以最近重新做了下整理,分享给大家。
目录
0. 简单创建匿名函数
下面两段代码的作用都是创建匿名函数并调用,输出Hello, World语句。分别使用Objective-C和C++ 11:
^{ printf("Hello, World!\n"); } ();
[] { cout << "Hello, World" << endl; } ();
Lambda表达式的一个好处就是让开发人员可以在需要的时候临时创建函数,便捷。
在创建闭包(或者说Lambda函数)的语法上,Objective-C采用的是上尖号^,而C++ 11采用的是配对的方括号[]。
不过“匿名函数”一词是针对程序员而言的,编译器还是采取了一定的命名规则。
比如下面Objective-C代码中的3个block,
#import <Foundation/Foundation.h> int (^maxBlk)(int , int) = ^(int m, int n){ return m > n ? m : n; }; int main(int argc, const char * argv[]) { ^{ printf("Hello, World!\n"); } (); int i = 1024; void (^blk)(void) = ^{ printf("%d\n", i); }; blk(); return 0; }
会产生对应的3个函数:
__maxBlk_block_func_0 __main_block_func_0 __main_block_func_1
可见函数的命名规则为:__{$Scope}_block_func_{$index}。其中{$Scope}为block所在函数,如果{$Scope}为全局就取block本身的名称;{$index}表示该block在{$Scope}作用域内出现的顺序(第几个block)。
1. 从语法上看如何捕获外部变量
在上面的代码中,已经看到“匿名函数”可以直接访问外围作用域的变量i:
int i = 1024; void (^blk)(void) = ^{ printf("%d\n", i); }; blk();
当匿名函数和non-local变量结合起来,就形成了闭包(个人看法)。
这一段代码可以成功输出i的值。
我们把一样的逻辑搬到C++上:
int i = 1024; auto func = [] { printf("%d\n", i); }; func();
GCC会输出:错误:‘i’未被捕获。可见在C++中无法直接捕获外围作用域的变量。
以BNF来表示Lambda表达式的上下文无关文法,存在:
lambda-expression : lambda-introducer lambda-parameter-declarationopt compound-statement lambda-introducer : [ lambda-captureopt ]
因此,方括号中还可以加入一些选项:
[] Capture nothing (or, a scorched earth strategy?) [&] Capture any referenced variable by reference [=] Capture any referenced variable by making a copy [=, &foo] Capture any referenced variable by making a copy, but capture variable foo by reference [bar] Capture bar by making a copy; don't copy anything else [this] Capture the this pointer of the enclosing class
根据文法,对代码加以修改,使其能够成功运行:
bash-3.2# vi testLambda.cpp bash-3.2# g++-4.7 -std=c++11 testLambda.cpp -o testLambda bash-3.2# ./testLambda 1024 bash-3.2# cat testLambda.cpp #include <iostream> using namespace std; int main() { int i = 1024; auto func = [=] { printf("%d\n", i); }; func(); return 0; } bash-3.2#
2. 从语法上看如何修改外部变量
上面代码中使用了符号=,通过拷贝方式捕获了外部变量i。
但是如果尝试在Lambda表达式中修改变量i:
auto func = [=] { i = 0; printf("%d\n", i); };
会得到错误:
testLambda.cpp: 在 lambda 函数中: testLambda.cpp:9:24: 错误:向只读变量‘i’赋值
可见通过拷贝方式捕获的外部变量是只读的。Python中也有一个类似的经典case,个人觉得有相通之处:
x = 10 def foo(): print(x) x += 1 foo()
这段代码会抛出UnboundLocalError错误,原因可以参见FAQ。
在C++的闭包语法中,如果需要对外部变量的写权限,可以使用符号&,通过引用方式捕获:
int i = 1024; auto func = [&] { i = 0; printf("%d\n", i); }; func();
反过来,将修改外部变量的逻辑放到Objective-C代码中:
int i = 1024; void (^blk)(void) = ^{ i = 0; printf("%d\n", i); }; blk();
会得到如下错误:
main.m:14:29: error: variable is not assignable (missing __block type specifier) void (^blk)(void) = ^{ i++; printf("%d\n", i); }; ~^ 1 error generated.
可见在block的语法中,默认捕获的外部变量也是只读的,如果要修改外部变量,需要使用__block类型指示符进行修饰。
为什么呢?请继续往下看 :)
3. 从实现上看如何捕获外部变量
闭包对于编程语言来说是一种语法糖,包括Block和Lambda,是为了方便程序员开发而引入的。因此,对Block特性的支持会落地在编译器前端,中间代码将会是C语言。
先看如下代码会产生怎样的中间代码。
int main(int argc, const char * argv[]) { int i = 1024; void (^blk)(void) = ^{ printf("%d\n", i); }; blk(); return 0; }
首先是block结构体的实现:
#ifndef BLOCK_IMPL #define BLOCK_IMPL struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; // 省略部分代码 #endif
第一个成员isa指针用来表示该结构体的类型,使其仍然处于Cocoa的对象体系中,类似Python对象系统中的PyObject。
第二、三个成员是标志位和保留位。
第四个成员是对应的“匿名函数”,在这个例子中对应函数:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int i = __cself->i; // bound by copy printf("%d\n", i); }
函数__main_block_func_0引入了参数__cself,为struct __main_block_impl_0 *类型,从参数名称就可以看出它的功能类似于C++中的this指针或者Objective-C的self。
而struct __main_block_impl_0的结构如下:
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int i; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _i, int flags=0) : i(_i) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
从__main_block_impl_0这个名称可以看出该结构体是为main函数中第零个block服务的,即示例代码中的blk;也可以猜到不同场景下的block对应的结构体不同,但本质上第一个成员一定是struct __block_impl impl,因为这个成员是block实现的基石。
结构体__main_block_impl_0又引入了一个新的结构体,也是中间代码里最后一个结构体:
static struct __main_block_desc_0 { unsigned long reserved; unsigned long Block_size; } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
可以看出,这个描述性质的结构体包含的价值信息就是struct __main_block_impl_0的大小。
最后剩下main函数对应的中间代码:
int main(int argc, const char * argv[]) { int i = 1024; void (*blk)(void) = (void (*)(void))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, i); ((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk); return 0; }
从main函数对应的中间代码可以看出执行block的本质就是以block结构体自身作为__cself参数,这里对应__main_block_impl_0,通过结构体成员FuncPtr函数指针调用对应的函数,这里对应__main_block_func_0。
其中,局部变量i是以值传递的方式拷贝一份,作为__main_block_impl_0的构造函数的参数,并以初始化列表的形式赋值给其成员变量i。所以,基于这样的实现,不允许直接修改外部变量是合理的——因为按值传递根本改不到外部变量。
4. 从实现上看如何修改外部变量(__block类型指示符)
如果想要修改外部变量,则需要用__block来修饰:
int main(int argc, const char * argv[]) { __block int i = 1024; void (^blk)(void) = ^{ i = 0; printf("%d\n", i); }; blk(); return 0; }
此时再看中间代码,发现多了一个结构体:
struct __Block_byref_i_0 { void *__isa; __Block_byref_i_0 *__forwarding; int __flags; int __size; int i; };
于是,用__block修饰的int变量i化身为__Block_byref_i_0结构体的最后一个成员变量。
代码中blk对应的结构体也发生了变化:
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __Block_byref_i_0 *i; // by ref __main_block_impl_0(void *fp, struct__main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
__main_block_impl_0发生的变化就是int类型的成员变量i换成了__Block_byref_i_0 *类型,从名称可以看出现在要通过引用方式来捕获了。
对应的函数也不同了:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { __Block_byref_i_0 *i = __cself->i; // bound by ref (i->__forwarding->i) = 0; // 看起来很厉害的样子 printf("%d\n", (i->__forwarding->i)); }
main函数也有了变动:
int main(int argc, const char * argv[]) { __block __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 1024}; void (*blk)(void) = (void (*)(void))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (struct __Block_byref_i_0 *)&i, 570425344); ((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk); return 0; }
前两行代码创建了两个关键结构体,特地高亮显示。
这里没有看__main_block_desc_0发生的变化,放到后面讨论。
使用__block类型指示符的本质就是引入了__Block_byref_{$var_name}_{$index}结构体,而被__block关键字修饰的变量就被放到这个结构体中。另外,block结构体通过引入__Block_byref_{$var_name}_{$index}指针类型的成员,得以间接访问到外部变量。
通过这样的设计,我们就可以修改外部作用域的变量了,再一次应了那句话:
There is no problem in computer science that can’t be solved by adding another level of indirection.
指针是我们最经常使用的间接手段,而这里的本质也是通过指针来间接访问,为什么要特地引入__Block_byref_{$var_name}_{$index}结构体,而不是直接使用int *来访问外部变量i呢?
另外,__Block_byref_{$var_name}_{$index}结构体中的__forwarding指针成员有何作用?
请继续往下看 :)
5. 背后的内存管理动作
在Objective-C中,block特性的引入是为了让程序员可以更简洁优雅地编写并发代码(配合看起来像敏感词的GCD)。比较常见的就是将block作为函数参数传递,以供后续回调执行。
先看一段完整的、可执行的代码:
#import <Foundation/Foundation.h> #include <pthread.h> typedef void (^DemoBlock)(void); void test(); void *testBlock(void *blk); int main(int argc, const char * argv[]) { printf("Before test()\n"); test(); printf("After test()\n"); sleep(5); return 0; } void test() { __block int i = 1024; void (^blk)(void) = ^{ i = 2048; printf("%d\n", i); }; pthread_t thread; int ret = pthread_create(&thread, NULL, testBlock, (void *)blk); printf("thread returns : %d\n", ret); sleep(3); // 这里睡眠1s的话,程序会崩溃 } void *testBlock(void *blk) { sleep(2); printf("testBlock : Begin to exec blk.\n"); DemoBlock demoBlk = (DemoBlock)blk; demoBlk(); return NULL; }
在这个示例中,位于test()函数的block类型的变量blk就作为函数参数传递给testBlock。
正常情况下,这段代码可以成功运行,输出:
Before test() thread returns : 0 testBlock : Begin to exec blk. 2048 After test()
如果按照注释,将test()函数最后一行改为休眠1s的话,正常情况下程序会在输出如下结果后崩溃:
Before test() thread returns : 0 After test() testBlock : Begin to exec blk.
从输出可以看出,当要执行blk的时候,test()已经执行完毕回到main函数中,对应的函数栈也已经展开,此时栈上的变量已经不存在了,继续访问导致崩溃——这也是不用int *直接访问外部变量i的原因。
5.1 拷贝block结构体
上文提到block结构体__block_impl的第一个成员是isa指针,使其成为NSObject的子类,所以我们可以通过相应的内存管理机制将其拷贝到堆上:
void test() { __block int i = 1024; void (^blk)(void) = ^{ i = 2048; printf("%d\n", i); }; pthread_t thread; int ret = pthread_create(&thread, NULL, testBlock, (void *)[blk copy]); printf("thread returns : %d\n", ret); sleep(1); } void *testBlock(void *blk) { sleep(2); printf("testBlock : Begin to exec blk.\n"); DemoBlock demoBlk = (DemoBlock)blk; demoBlk(); [demoBlk release]; returnNULL; }
再次执行,得到输出:
Before test() thread returns : 0 After test() testBlock : Begin to exec blk. 2048
可以看出,在test()函数栈展开后,demoBlk仍然可以成功执行,这是由于blk对应的block结构体__main_block_impl_0已经在堆上了。不过这还不够——
5.2 拷贝捕获的变量(__block变量)
在拷贝block结构体的同时,还会将捕获的__block变量,即结构体__Block_byref_i_0,复制到堆上。这个任务落在前面没有讨论的__main_block_desc_0结构体身上:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);} static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);} static struct __main_block_desc_0 { unsigned long reserved; unsigned long Block_size; void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*); void (*dispose)(struct __main_block_impl_0*); } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
栈上的__main_block_impl_0结构体为src,堆上的__main_block_impl_0结构体为dst,当发生复制动作时,__main_block_copy_0函数会得到调用,将src的成员变量i,即__Block_byref_i_0结构体,也复制到堆上。
5.3 __forwarding指针的作用
当复制动作完成后,栈上和堆上都存在着__main_block_impl_0结构体。如果栈上、堆上的block结构体都对捕获的外部变量进行操作,会如何?
下面是一段示例代码:
void test() { __block int i = 1024; void (^blk)(void) = ^{ i++; printf("%d\n", i); }; pthread_t thread; int ret = pthread_create(&thread, NULL, testBlock, (void *)[blk copy]); printf("thread returns : %d\n", ret); sleep(1); blk(); } void *testBlock(void *blk) { sleep(2); printf("testBlock : Begin to exec blk.\n"); DemoBlock demoBlk = (DemoBlock)blk; demoBlk(); [demoBlk release]; returnNULL; }
- 在test()函数中调用pthread_create创建线程时,blk被复制了一份到堆上作为testBlock函数的参数。
- test()函数中的blk结构体位于栈中,在休眠1s后被执行,对i进行自增动作。
- testBlock函数在休眠2s后,执行位于堆上的block结构体,这里为demoBlk。
上述代码执行后输出:
Before test() thread returns : 0 1025 After test() testBlock : Begin to exec blk. 1026
可见无论是栈上的还是堆上的block结构体,修改的都是同一个__block变量。
这就是前面提到的__forwarding指针成员的作用了:
起初,栈上的__block变量的成员指针__forwarding指向__block变量本身,即栈上的__Block_byref_i_0结构体。
当__block变量被复制到堆上后,栈上的__block变量的__forwarding成员会指向堆上的那一份拷贝,从而保持一致。
参考资料:
- http://msdn.microsoft.com/en-us/library/dd293603.aspx
- http://www.cprogramming.com/c++11/c++11-lambda-closures.html
- http://developer.apple.com/library/ios/#documentation/cocoa/Conceptual/Blocks/Articles/00_Introduction.html
- http://en.wikipedia.org/wiki/Closure_(computer_science)
(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)
《C/C++语言中闭包的探究及比较》的相关评论
讲的十分深入,鄙人第一次接触这样的概念。
写得很条理,太棒了
如果作者认为闭包只是语法糖的话,真该好好补习下Lisp(or Ruby)
ObjectC中的必包,与Ruby中的block,Proc等特性比较起来,还是不好用。
@YangZX
在C++中,lamda表达式能做到的,不用lamda表达式也能做到。对于C++来说,不是语法糖是什么?看文章中的介绍,这个Block对C也是语法糖的作用。
也不要太较真成『语法糖主义』… Java 5 之前没 Generics 的时候代码不还是照样写、Assembler 那时没有 C 不还是可以编程,C 从 89 到 17 加了多少特性,vararg、complex、block、pack,多少也是『可有可无』的,甚至因为有内联汇编和底层访问性、低层次的规范,你都可以内联汇编给它“加特性”,你甚至可以说,C 就是汇编的语法糖,哪怕它让代码机器平台无关、提供了类型检查
C++ 的 block 的确是没啥革命性的变更,但是一切皆语法糖这个观点太表面了…
楼主没理解C++的closure
int i = 1024;
auto func = [] { printf(“%d\n”, i); }; ==》 应该是auto func = [=] { printf(“%d\n”, i); };
func();
C++对闭包的控制比block严谨和灵活多了,能够明确外面的变量是如何在lamda表达式中引入,是按值方式引入还是按引用方式引入。
如果连外层变量都不能访问或者修改的话,根本谈不上什么闭包了。
数学上的闭包概念也是表达的这层意思。比如对于实数域而言,开区间(a,b) 的闭包就是[a,b],它的直观理解就是一个集合的闭包可以延伸至预期紧密关联的部分(聚点)
理解block推荐阅读
《Pro Multithreading and Memory Management for iOS and OS X》
@dourgulf
未看完全部文章就评论了,不好意思。
这个文章是《Pro Multithreading and Memory Management for iOS and OS X》中关于block这部分很好的一个翻译,只是不知道LZ是否真正动手验证过其中的东西。。。
@dourgulf
:) 是的,我几个月前了解这部分知识是通过《Pro Multithreading and Memory Management for iOS and OS X》这书的,其中最关键的是得知clang -rewrite-objc 这个输出中间代码的选项。
如果你看完文章,你会发现书籍只是参考,文章内容是经过思考和总结的。而且文中的一些例子和遇到的问题是我在实际项目中遇到的,然后抽象成简单的代码示例。
https://godbolt.org/ 这个 Compiler Explorer 也很好
@dourgulf
:) 这位网友,是不是只看到这部分就评论了,没有看到整篇文章是存在一条由浅到深的主线吗? 这是我特地整理出来的结构,为了更好点的阅读性 泪
@YangZX
我也一直想好好补习下Lisp的 :)
:) 这位网友,是不是只看到这部分就评论了,没有看到整篇文章是存在一条由浅到深的主线吗? 这是我特地整理出来的结构,为了更好点的阅读性 泪
看着好头疼啊。
请问,forwarding指向的数据的互斥访问时怎么实现的?
表示不理解
@Celebi
好吧,我修正为“C++中的‘闭包’是语法糖,”。
就像宏一样,C中的宏在Lisp的宏面前,还好意思叫自己宏吗?
由于没有使用环境模型,C++中的lambda不能这样工作:
#include
#include
using namespace std;
function counter(int n)
{
return [&] {return ++n;};
}
int main()
{
auto f = counter(5);
cout<<f()<<endl;
cout<<f()<<endl;
return 0;
}
而这种方式恰恰是闭包的威力所在,scheme就可以通过这种方式实现消息传递模型
counter(Int n) 是 by-copy by-value 的,每次去 increment 都是新的 value ++5(准确的说是 frame variable int)
你可以再加层引用,然后把引用 by-value 进 lambda ? C++ 虽然复杂但是特性还是丰富
copy 是 “pass by copy”, value 是 “call by value” 和 call-by-need 惰性参数求值 传名调用 并列的那个
注意那个 5 实际上是拷贝的局部变量左值 left-hand-side value
int n=5
另外你所说的 environment 模型是指 Scheme 系基于 Lambda 演算程序设计语言的 Lexical scoping 变量解析吗?
Lisp 里这样的… 我还是用 Racket 吧(我不会 R*RS,但多范式融合的也就是那个样子),怎么说,Lisp 首先这个语言本身已经很老了并且也对很多人不那么可获取、其次它的名称被滥用的很严重,有一大堆 『Lisp 方言』 和 『C-like 语言』了…. 我…..
[code lang=racket]
(define counter
(lambda [n] (+ n 1))) ; your definition above
(define effect-couter
(lambda [n] [lambda [] (set! n (+ n 1)) n])) ; using side-effects upvalue assignment like what we does in JavaScipt
(define time-a (effect-counter 0))
(display (a)) ; 1
(display (a)) ; 2
[/code]
可是要用到副作用啊… 什么是消息传递模型,大概就是类似面向对象 Builder pattern 那个数据依赖构建吧,一般都是高阶函数引用 UpValue… 并且传递的消息,一般都是 immutable 的数据模型对象啊?
不得不说,这标题的误导性太强了…Apple一个私家扩展,竟然上升到了C++语言特性的地步
这么说吧,C++的Lambda是一种闭包,但是C++并不完整的支持闭包…
@Jason Lee
是的是的,非常抱歉!
来个C语言版的准lambda
#include
int main(int argc, char **argv) {
int j=1024;
int (*f)() = ({ int __fn__ () {
printf(“%d\n”,j++);return j;} __fn__; });
printf(“%d\n”,f());
return 0;
}
看了就有一种
#define function(x) [x]
的冲动……
(好吧,我承认我JavaScript用太多了。)
c++这门语言的问题是:提供一个说起来爽的特性的同时,加入一堆屎
这是gcc的扩展,不是标准C的@pingf
@lixinqi
囧,偶又木有说是标准C.
不过就学习C语言来说,与其学习标准C还不如学习GCC(GCC的通用性和性能都可以了,我现在用它在linux下,win下写程序,还用它开发avr和arm,就上述的这个特性来说,各个平台的gcc都是支持的).
同样,比如照着C++11标准,还不如照着G++来学习.
有些狗屁特性,连支持的编译器都木有,学习了有个屁用.
而有些高级的特性,好多编译器都支持了,干嘛非要死扣标准.
有了这个东西 C 或 C++ 语言可以写出以前写不出来的程序吗 ???
其实我就是想知道如何评论文章
参考资料中的http://www.cprogramming.com/c++11/c++11-lambda-closures.html 讲的非常好,推荐一下
很不错的文字。学习了。谢谢!
#include
#include
using namespace std;
function counter(int n)
{
return [n]() mutable {return ++n;};
}
int main()
{
auto f = counter(5);
cout<<f()<<endl;
cout<<f()<<endl;
return 0;
}
看作者ID是武侠迷啊,想起来高中时候省钱买杂志的时光,饮霜是思禽他爹吧
博主帮我看看这个靠谱不?
http://pastebin.com/8S7CjdSb
@gg
除开特定协议,比如http, ftp, smtp, 在tcp基础上实现的通用网络库,如果没有提供rpc, 跨平台,AMQP中特性之一,基本没什么价值。
请问在其他语言,比如C#
Action func() {
int x = 0;
return () => ++x;
}
这里到底发生了什么?x应该是以引用的方式被捕获到闭包里的,但是func返回后局部变量x已经失效了,那么返回得到的闭包中的x到底是什么东西?
C# 对『lambda』的支持我不知道,不过 Java 8 的了解一点,Java 8 的是真·匿名内部类、SingleAbstractMethod
@FunctionalInterface
接口语法糖,编译器理应在脱糖转化的时候就可以,可是呢… 实际上这个东西也被放在运行时由 JRE 的 LambdaMetaFactory 动态为目标接口生成子类实现,编译器用javap -c
看看就知道是桥接;这也就是 Android 还存在什么 RuntimeDesugar 的问题的原因了。[code lang=java]
invokedynamic #4, 0 // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
invokeinterface #5, 2 // InterfaceMethod java/util/List.forEach:(Ljava/util/function/Consumer;)V
return
private static void lambda$simple$0(java.lang.String);
getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
aload_0 ; String name$arg0
invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
return
[/code]
Java 8 的『capture』(注意不是泛型 wildcard 的 type capture) 就是 by-value capture,记得酷壳应该有一篇文章引到了『Java 是只有值类型的语言(引用不也是 GC 堆上的… 你又不能修改 GC 堆的『a』(reference)“字节码类型”)』这种的文章
如果你想 capture 一个非 final 的变量,然后在 Lambda 里给它赋值,Javac 会拦住你 — 因为 capture 到的 Upvalue,它是一个右值(right-hand-side value),
当然如果你捕获的是 effective final 的也可以,就是说它被『使用得是』 final 的样子(不准赋值),类型可以宽限一点
当然如果你一定要引用,自己定义一个引用类就可以了
[code lang=kotlin]
final class Reference<T>(var x: T)
[/code]
问题的回答是:
(1) x 不是以引用的方式被捕获的,你拿到的是一个被分配好了的对象,它是『值』
(2) 的确是失效了,不过对于 C# 和 Java 我们首先应该把绝大部分存储状态的分配视为由 GC 管理的,GC 堆可不是一个『subroutine local scoped』的东西
(3) 是一个对象啊,并且它的值等于外包(encapsulating function)函数的 x (Java 的 reference
==
)只要合理使用用多线程,网络库一般是带宽先达到瓶颈,我自己用C++实现的一个特定用途的网络库,不断的增加连接,流量达到100M时,家用机双核cup才占用了50%,所以说单纯的比较性能没啥用处,一般都足够用了, 重要的是提供的接口容易使用,比如直接提供rpc, 带有mq特性的接口。
@飞翔的天地 请参考著作SICP的第三章,描述了Lisp中的情况,而ruby和perl应该也是类似的机制。
Perl 我几乎没用过、Ruby 现在的情况不是很清楚,在 Ruby 1.9 YARV 的时候这个情况和 Lisp/Java 8/C# 的情况应该还是蛮不同的。
因为 Ruby 1.9 左右的时候 YARV 对 block capture 到的外部环境的确还是使用比较清奇的方式实现的。
不谈我不知道的 Perl/C#/Lisp,
Java: 弄一个 lambda$scope$count,然后让 runtime LambdaMetaFactory 来生成这个匿名内部类,这个对内部类 Functional SAM(SingleAbstracMethod) Interface 的实现是
invokedynamic
了目标接口的目标 SAM 方法(它的参数类型什么的可能是不定的),然后运行时层面动态生成实际代理实现(直接转 lambda$scope$count)它的捕获就是在参数列表前头弄个注入点,让编译器自己把需要的 captured 值放进去。值要是被修改就不能反映程序员预期的行为了(只是改变了本地的版本,外部函数的那个引用就不会被修改)
Ruby 1.9: 这个非常极端,因为据说,它创建个闭包不管捕获与否都要把整个栈拷贝一份存住,目的是保存执行环境方便重新运行…
而父层若是创建闭包之后改变这个变量,怎么样呢?
Ruby 的方法很独特,某个外部函数闭包之后,Ruby 栈指针就被替换到了闭包 shadow copy 出来的那个… 所以外部函数对 UpValue 的改动能够同步到 inner 内部函数
而若是内部闭包的函数修改了这个变量,父层也自然是可以访问到新版本的,因为此时父层在使用的栈环境就是闭包的环境
若是再创建新的闭包也一样,一方面所有创造的环境闭包都有自己随意的环境,另一方面外部函数总是能够修改最后一次闭包的环境的存储(不确定…)
这方面推荐 Pat Shaughnessy 写的《Ruby Under a Microscope》
@飞翔的天地
至于C#, 去问m$
分析的很透彻,很欣赏你的看法,学习了。
不一定,但如果你以此为标准衡量一个语言特性,那实在没什么意义@sincoder
c++?没研究过,先把java学好吧
好的,挺喜欢这个分析
@飞翔的天地
闭包对捕获的对象也有隐式的依赖,这个依赖告诉gc不要急着去回收这片内存
博主最近更新好慢啊
期待更新
久久星座约会
以星座为主题的网络约会,爱情交友,目前只同意女生加群。
Q群:235893866
以后『Q 群』『约会』『星座』『女生』这几个广告关键字真应该屏蔽了… 加权重… 这是技术博客,为什么会有这种广告党 ?
@thynson
托管堆我理解,但是栈上的数据不由gc管理……
楼主怎么还不更新?
匿名函数不等于闭包,而且文章全在讲obj-c,却用个C/C++的标题。内容完全是在讲具体实现,属于编译器的实现,太标题党了。
请问你的博客中,代码高用的是哪一种啊
@YangZX
语法糖难道不是一段更加易读的代码吗?
我觉得你所说的”闭包”仅仅是一种封装, 跟可读性没什么关系, 有时可能让可读性更差.
nb