彻底理解闭包实现原理

news/2024/5/20 13:01:19/文章来源:https://www.cnblogs.com/crossoverJie/p/16827024.html

前言

闭包对于一个长期写 Java 的开发者来说估计鲜有耳闻,我在写 PythonGo 之前也是没怎么了解,光这名字感觉就有点"神秘莫测",这篇文章的主要目的就是从编译器的角度来分析闭包,彻底搞懂闭包的实现原理。

函数一等公民

一门语言在实现闭包之前首先要具有的特性就是:First class function 函数是第一公民。

简单来说就是函数可以像一个普通的值一样在函数中传递,也能对变量赋值。

先来看看在 Go 里是如何编写的:

package mainimport "fmt"var varExternal intfunc f1() func(int) int {varInner := 20innerFun := func(a int) int {fmt.Println(a)varExternal++varInner++return varInner}return innerFun
}func main() {varExternal = 10f2 := f1()for i := 0; i < 2; i++ {fmt.Printf("varInner=%d, varExternal=%d \n", f2(i), varExternal)}fmt.Println("======")f3 := f1()for i := 0; i < 2; i++ {fmt.Printf("varInner=%d, varExternal=%d \n", f3(i), varExternal)}
}// Output:
0
varInner=21, varExternal=11 
1
varInner=22, varExternal=12 
======
0
varInner=21, varExternal=13 
1
varInner=22, varExternal=14 

这里体现了闭包的两个重要特性,第一个自然就是函数可以作为值返回,同时也能赋值给变量。

第二个就是在闭包函数 f1() 对闭包变量 varInner 的访问,每个闭包函数的引用都会在自己的函数内部保存一份闭包变量 varInner,这样在调用过程中就不会互相影响。

从打印的结果中也能看出这个特性。

作用域

闭包之所以不太好理解的主要原因是它不太符合自觉。

本质上就是作用域的关系,当我们调用 f1() 函数的时候,会在栈中分配变量 varInner,正常情况下调用完毕后 f1 的栈会弹出,里面的变量 varInner 自然也会销毁才对。

但在后续的 f2()f3() 调用的时,却依然能访问到 varInner,就这点不符合我们对函数调用的直觉。

但其实换个角度来看,对 innerFun 来说,他能访问到 varExternalvarInner 变量,最外层的 varExternal 就不用说了,一定是可以访问的。

但对于 varInner 来说就不一定了,这里得分为两种情况;重点得看该语言是静态/动态作用域。

就静态作用域来说,每个符号在编译器就确定好了树状关系,运行时不会发生变化;也就是说 varInner 对于 innerFun 这个函数来说在编译期已经确定可以访问了,在运行时自然也是可以访问的。

但对于动态作用域来说,完全是在运行时才确定访问的变量是哪一个。

恰好 Go 就是一个静态作用域的语言,所以返回的 innerFun 函数可以一直访问到 varInner 变量。

实现闭包

但 Go 是如何做到在 f1() 函数退出之后依然能访问到 f1() 中的变量呢?

这里我们不妨大胆假设一下:

首先在编译期扫描出哪些是闭包变量,也就是这里的 varInner,需要将他保存到函数 innerFun() 中。

f2 := f1()
f2()

运行时需要判断出 f2 是一个函数,而不是一个变量,同时得知道它所包含的函数体是 innerFun() 所定义的。

接着便是执行函数体的 statement 即可。

而当 f3 := f1() 重新赋值给 f3 时,在 f2 中累加的 varInner 变量将不会影响到 f3,这就得需要在给 f3 赋值的重新赋值一份闭包变量到 f3 中,这样便能达到互不影响的效果。

闭包扫描

GScript 本身也是支持闭包的,所以把 Go 的代码翻译过来便长这样:

int varExternal =10;
func int(int) f1(){int varInner = 20;int innerFun(int a){println(a);int c=100;varExternal++;varInner++;return varInner;}return innerFun;
}func int(int) f2 = f1();
for(int i=0;i<2;i++){println("varInner=" + f2(i) + ", varExternal=" + varExternal);
}
println("=======");
func int(int) f3 = f1();
for(int i=0;i<2;i++){println("varInner=" + f3(i) + ", varExternal=" + varExternal);
}// Output:
0
varInner=21, varExternal=11
1
varInner=22, varExternal=12
=======
0
varInner=21, varExternal=13
1
varInner=22, varExternal=14

可以看到运行结果和 Go 的一样,所以我们来看看 GScript 是如何实现的便也能理解 Go 的原理了。


先来看看第一步扫描闭包变量:

allVariable := c.allVariable(function)
查询所有的变量,包括父 scope 的变量。

scopeVariable := c.currentScopeVariable(function)
查询当前 scope 包含下级所有 scope 中的变量,这样一减之后就能知道闭包变量了,然后将所有的闭包变量存放进闭包函数中。

闭包赋值


之后在 return innerFun 处,将闭包变量的数据赋值到变量中。

闭包函数调用

func int(int) f2 = f1();func int(int) f3 = f1();

在这里每一次赋值时,都会把 f1() 返回函数复制到变量 f2/f3 中,这样两者所包含的闭包变量就不会互相影响。



在调用函数变量时,判断到该变量是一个函数,则直接返回函数。

之后直接调用该函数即可。

函数式编程

接下来便可以利用 First class function 来试试函数式编程:


class Test{int value=0;Test(int v){value=v;}int map(func int(int) f){return f(value);}
}
int square(int v){return v*v; 
}
int add(int v){return v++; 
}
int add2(int v){v=v+2;return v; 
}
Test t =Test(100);
func int(int) s= square;
func int(int) a= add;
func int(int) a2= add2;
println(t.map(s));
assertEqual(t.map(s),10000);println(t.map(a));
assertEqual(t.map(a),101);println(t.map(a2));
assertEqual(t.map(a2),102);

这个有点类似于 Java 中流的 map 函数,将函数作为值传递进去,后续支持匿名函数后会更像是函数式编程,现在必须得先定义一个函数变量再进行传递。


除此之外在 GScript 中的 http 标准库也利用了函数是一等公民的特性:

// 标准库:Bind route
httpHandle(string method, string path, func (HttpContext) handle){HttpContext ctx = HttpContext();handle(ctx);
}

在绑定路由时,handle 便是一个函数,使用的时候直接传递业务逻辑的 handle 即可:

func (HttpContext) handle (HttpContext ctx){Person p = Person();p.name = "abc";println("p.name=" + p.name);println("ctx=" + ctx);ctx.JSON(200, p);
}
httpHandle("get", "/p", handle);

总结

总的来说闭包具有以下特性:

  • 函数需要作为一等公民。
  • 编译期扫描出所有的闭包变量。
  • 在返回闭包函数时,为闭包变量赋值。
  • 每次创建新的函数变量时,需要将闭包数据复制进去,这样闭包变量才不会互相影响。
  • 调用函数变量时,需要判断为函数,而不是变量。


可以在 Playground 中体验闭包函数打印裴波那切数列的运用。

本文相关资源链接

  • GScript 源码:https://github.com/crossoverJie/gscript

  • Playground 源码:https://github.com/crossoverJie/gscript-homepage

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_218373.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

工程项目部质量管理体系的控制要点分析

质量管理是施工企业风险控制的重要组成部分。本文从有序的生产过程控制&#xff0c;提高企业质量意识出发&#xff0c;结合贯彻ISO9001标准及50430规范的企业贯标工作&#xff0c;分阶段研究和分析施工企业工程项目部质量管理体系的控制要点。 质量是企业的生命线&#xff0c;…

Android实战——单元测试从吹水到实践

目录1.单元测试到底需要不需要了&#xff1f;开发时间紧张&#xff0c;不需要做单元测试了吧&#xff1f;开发经验丰富&#xff0c;不需要做单元测试了吧&#xff1f;或许存在一种”自动化“的测试&#xff0c;就不需要做单元测试了吧&#xff1f;2.单元测试的好处单元测试可以…

【附源码】计算机毕业设计SSM校园拍卖平台

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

React 状态管理器,我是这样选的

前言 我们的前端团队在一直深度使用 React &#xff0c;从最早的 CRA &#xff0c;到后来切换到 umijs &#xff0c;从 1.x、2.x、3.x 再到现在的 4.x&#xff0c;其中有一点不变的&#xff0c;就是我们一直在使用基于 react-redux 思想的 dva 作为状态管理工具。 在状态共享这…

(附源码)计算机毕业设计SSM跨移动平台的新闻阅读应用

&#xff08;附源码&#xff09;计算机毕业设计SSM跨移动平台的新闻阅读应用 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目…

DM-DM DBLINK使用配置

简单介绍 DM-DM DBLINK支持3种连接方式创建&#xff0c;分别是&#xff1a;dmmal、dpi、odbc。 其中dpi、odbc属于第三方接口&#xff0c;dmmal属于原生接口。dpi类型dblink为新版本新添加支持&#xff0c;以前版本中不支持。 环境说明 &#xff08;1&#xff09;数据库版本…

2023届C/C++软件开发工程师校招面试常问知识点复盘Part 7

目录46、C类的成员变量初始化顺序及拓展47、强制转换类型操作符号48、const 成员函数–常成员函数与常量对象49、volatile关键字50、赫夫曼树51、前缀树46、C类的成员变量初始化顺序及拓展 注意&#xff1a; 1、const成员或者引用必须在成员变量初始化列表中初始化&#xff0c;…

git的基础指令操作

git的下载地址&#xff1a;https://git-scm.com/download 安装好git后 在桌面上右键即可以看到两个git的快捷方式。 需要先对git进行基本的配置&#xff0c;即需要配置用户名和用户邮箱 1. 打开Git Bash 2. 设置用户信息 git confifig --global user.name “zqy” git confi…

权限项目 1_搭建环境

硅谷通用权限系统&#xff1a;搭建环境 一、项目介绍 1、介绍 权限管理是所有后台系统都会涉及的一个重要组成部分&#xff0c;而权限管理的核心流程是相似的&#xff0c;如果每个后台单独开发一套权限管理系统&#xff0c;就是重复造轮子&#xff0c;是人力的极大浪费&…

第 1 章之:二叉树特性

声明&#xff1a;文章为博主原创&#xff0c;转载请联系博主。文章若有错误和疏漏之处&#xff0c;还望大家不吝赐教&#xff01; 第一章&#xff1a;数据结构与算法基础--------------------------- 本章重点内容为&#xff1…

基于麻雀算法二维oust图像分割算法研究附matlab代码

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法 …

期刊|认知科学领域期刊《Trends in Cognitive Sciences》

Hello&#xff0c;大家好&#xff01; 这里是壹脑云科研圈&#xff0c;我是Ns&#xff5e; 今天我们介绍的是爱思维尔(Elsevier)旗下细胞出版社&#xff08;cell press&#xff09;发行的关于认知科学领域的期刊&#xff1a;Trends in Cognitive Sciences。 1 期刊简介 基本…

mysql之给字符串加索引

文章目录前言长字段加索引前缀索引对覆盖索引的影响合理的使用前缀索引总结前言 之前的文章介绍了主键索引和唯一索引的区别&#xff0c;也介绍了主键索引和唯一索引在不同业务场景下的区别。今天我们继续介绍&#xff0c;普通索引怎么合理的使用。 长字段加索引 这里我们就…

Spring6.0全新发布,快来看看

Spring6.0全新发布&#xff0c;快来看看 Spring Framework 6.0 发布了首个 RC 版本。 翻译后页面(有点好笑)&#xff1a; On behalf of the team and everyone who has contributed, I am pleased to announce that Spring Framework is available now.6.0.0-RC2 Spring Frame…

零信任如何给为企业的数字资源保驾护航?

零信任安全最早由著名研究机构Forrester的首席分析师约翰.金德维格在2010年提出。 零信任安全针对传统边界安全架构思想进行了重新评估和审视&#xff0c;并对安全架构思路给出了新的建议。 零信任模型是什么 零信任是一种基于严格身份验证的网络安全架构。、 在该架构下&am…

【SpringBoot笔记12】SpringBoot框架实现文件上传和文件下载

这篇文章&#xff0c;主要介绍如何使用SpringBoot框架实现文件上传和文件下载。 目录 一、SpringBoot文件上传 1.1、引入依赖 1.2、编写文件上传页面 1.3、编写文件上传代码 &#xff08;1&#xff09;MultipartFile对象 &#xff08;2&#xff09;ResourceUtils工具类 …

音频拼接在一起怎么做?这篇文章来告诉你

随着互联网的发展&#xff0c;很多优质歌曲都纷纷地呈现在大家眼前&#xff0c;而将不同的音乐合并在一起&#xff0c;并且放入视频里&#xff0c;也是别有一番风味&#xff0c;那么许多人会好奇音频如何拼接在一起呢?下面就为大家分享两个好用的方法&#xff0c;只要一点时间…

【C++】使用对象自动管理指针(用到运算符重载)

文章目录1. 首先设计整型类&#xff1a;class Int普通指针2. 设计一个Object类&#xff0c;并设计Int类型的指针。那如何获取Int类型的值呢&#xff1f;1. 首先设计整型类&#xff1a;class Int class Int { private:int value; public:Int(int x 0) :value(x){cout <<…

Springbootg整合validation整合

坚持年年写博客&#xff0c;不能断了&#xff0c;所以粘贴平时写的一份笔记吧 一、简介 校验参数在以前基本都是使用大量的if/else&#xff0c;稍微方便一点的可以使用反射自定义注解的形式&#xff0c;但是复用性不是很好&#xff0c;并且每个人对于的自定义注解有着自己的使…

Java基础-任务执行服务

今天小编带领大家一起来探索Java中的任务执行服务 关于任务执行服务&#xff0c;我们介绍了&#xff1a; 任务执行服务的基本概念。 主要实现方式&#xff1a;线程池。 定时任务。 &#xff08;1&#xff09;基本概念 任务执行服务大大简化了执行异步任务所需的开发&…