【编程语言】什么是闭包?你可能经常在用它,但不知道它叫闭包!

news/2024/7/7 21:24:06

文章目录

  • 什么是闭包
  • 用例
  • 不具有一等函数的语言中的闭包
  • 闭包可能造成内存泄漏
  • ps:数学中的闭包


wiki-closure
百度百科-闭包
在 JS 中闭包为什么叫「闭包」,而不用其它名称命名?
阮一峰-学习Javascript闭包(Closure)
什么是闭包?闭包的作用? 闭包会导致内存泄漏吗?


什么是闭包

闭包(closure)的概念是在20世纪60年代为λ-微积分中表达式的机械评估而提出的,并在1970年首次作为PAL编程语言的一个语言特征被完全实现,以支持词义范围内的一等函数

现在如果搜索“闭包”,出现的结果中很多都是关于 JavaScript 的,文章中都是以 JavaScript 讲解闭包的特性,以及在 JS 中的应用场景,但是我们应该清楚,闭包最初的出现是在 JS 之前 30 年,那时解决的场景是当时语言 lambda 演算的不足。

闭包的使用与函数是一等对象的语言有关。在此类语言中,函数可以作为参数传递、从函数调用返回、绑定到变量名等,就像字符串和整数等更简单的类型一样。这种是一等对象的函数,也叫一等函数

意思是,如果想使用闭包,需要使用那些能把函数作为参数、返回值、赋值给变量的语言。如js、golang

如果这个(被用作参数或返回值的)函数带有(内部使用了)自由变量,那就会产生一个闭包(就是这个函数)

自由变量:自由变量是指函数中使用的变量,既不是局部变量也不是该函数的参数。在这种情况下,术语非局部变量通常是同义词。
定义它的环境的变量
非局部变量:在编程语言理论中,非局部变量是指不在局部范围内定义的变量。虽然这个术语可以指全局变量,但它主要用于嵌套函数和匿名函数,在这些函数中,有些变量既不在局部范围,也不在全局范围。
结合闭包理解:自由变量,不是定义在闭包函数中的局部变量,也不是任何全局变量。而是指闭包所在函数的局部变量、所在函数的上级函数的局部变量(如果嵌套了多层函数的话)…

所以,闭包就是能让外部读取函数内部变量的函数。在本质上,闭包是将函数内部和函数外部连接起来的桥梁

闭包一词可以理解为:在一个封的作用域中,将某些自由变量在定义它的函数中。


用例

在具有一等函数的语言中才能够使用闭包。如果你从其他类型的语言转过来,会觉得一等函数用起来非常爽

“闭包的作用是让外部读取函数内部的变量”,简单的一句话不能让你领悟闭包的魅力,我们可以从以下例子来循序渐进地感受
网上都是用js举例,这里我用go来举例

eg1:n是f1的局部变量,是f2的自由变量。f2捕获了变量n,并被作为返回值传递了出去,外部(相对于n所在的域f1来说)就可以通过f2来访问f1的变量

func f1() func() {
	var n = 999
	f2 := func() {
		fmt.Println(n) // f2包含了自由变量n,是一个闭包函数。闭包了n
	}
	n++
	return f2 // return一个闭包
}
func main() {
	result := f1()
	result() // 1000 // 在f1外部,通过闭包间接访问到了f1内部的变量
}

eg2:匿名函数捕获自由变量n,并作为f2的参数传递出去,外部(相对于n所在的域main来说)就可以通过f2来访问f1的变量n

func main() {
	var n = 999
	f2(func() { // 传递了一个闭包
		fmt.Println(n) // f2包含了自由变量n,是一个闭包函数。闭包了n
	})
}
func f2(fun func()) {
	if ... { // fun可以被按需调用
		fun()
	}
	// f2执行完毕后,fun被回收
}

eg3:闭包经常与回调一起使用,特别是用于事件驱动的程序。这个例子比较贴近于使用场景 (这个例子只描述大体意思,不要深究细节)

// 以下是释放某个持续性技能的一段伪代码
// 有一个技能,是持续性技能,目标死亡后停止释放,并可以获得击杀奖励
// 做法是玩家释放技能时监听目标的死亡事件,如果死亡就停止技能的释放,并获得击杀奖励
func SkillCast(doPlayer, bePlayer *Player, skillId int32, ...) {
	// ...
	buffCom := doPlayer.GetBuffCom() // 获取buff组件
	// 利用 buffCom,判断doPlayer是否死亡、是否被定身等,如果是,则不满足技能释放条件
	if buffCom... {
		return
	}
	attrCom := doPlayer.GetAttrCom() // 获取属性组件
	// 利用 attrCom,判断doPlayer的魔法值,是否能够抵扣本次技能需要的魔法值,如果不能,则不满足技能释放条件
	if attrCom... {
		return
	}
	// ...
	bePlayer.SetOnDead(func() { // 注册死亡监听,bePlayer死亡时会回调注册的函数
		// 停止技能释放
		doPlayer.StopSkillCast(skillId) // 闭包了doPlayer和skillId
		// 击杀奖励:添加增益buff
		buffCom.Add(...) // 闭包了buffCom
		// 击杀奖励:回复100滴血
		attrCom.AddHp(...) // 闭包了attrCom
	})
	// ...
}

// 以下是玩家死亡时的一段代码。当玩家死亡时,会回调注册过来的监听函数
func Dead(player *Player, ...) {
	// ...
	for _, onDead := range player.GetOnDeads() { // 回调所有注册的死亡监听
		onDead()
	}
	// ...
}

从这个例子更加贴近使用场景,但可能还不足以让你感受到闭包的"爽点"
现在我修改一下上边的代码,取消所有的闭包

func SkillCast(doPlayer, bePlayer *Player, skillId int32, ...) {
	// ...
	buffCom := doPlayer.GetBuffCom()
	if buffCom... {
		return
	}
	attrCom := doPlayer.GetAttrCom()
	if attrCom... {
		return
	}
	// ...
	bePlayer.SetOnDead(func(doPlayer *Player, skillId int32) {
		// 停止技能释放
		doPlayer.StopSkillCast(skillId)
		// 获取buff组件
		buffCom := doPlayer.GetBuffCom()
		// 击杀奖励:添加增益buff
		buffCom.Add(...)
		// 获取属性组件
		attrCom := doPlayer.GetAttrCom()
		// 击杀奖励:回复100滴血
		attrCom.AddHp(...)
	})
	// ...
}
func Dead(player *Player, ...) {
	// ...
	// 通过一些方式获取到doPlayer和skillId
	doPlayer := ...
	skillId := ...
	for _, onDead := range player.GetOnDeads() { // 回调所有注册的死亡监听
		onDead(doPlayer, skillId)
	}
	// ...
}

如上修改的代码中,doPlayer、skillId、buffCom、attrCom都不再被闭包,回调函数中使用的doPlayer和skillId是Dead传递过来的,buffCom和attrCom是通过doPlayer获取的。
但为了不闭包,需要多写一些代码用来获取和传递doPlayer和skillId、获取buffCom和attrCom,虽然这样做避免了闭包带来的内存占用,但编码效率变慢了、代码可读性也降低了(代码也不再简洁)。并且因为访问内存的次数变多了,所以执行时间也会变长,也就是时间换空间。

其实很多情况下,被闭包的变量都能通过其它方式访问到,而不是必须使用闭包。

使用闭包的优点是

  • 提升编码效率:变量被保存起来没有被销毁,不用通过其它方式获取变量(比如把原本闭包的自由变量定义为全局变量)
  • 提高代码可读性:代码简洁
  • 提升程序执行效率:空间换时间,所以要做好取舍,如果造成大量的内存占用也是不值得的,但大多数情况不会。

不具有一等函数的语言中的闭包

以Java为例。Java不具有一等函数,所以我们不能使用闭包。

严格来说,我们只是不能写出上述典型的闭包代码,但闭包是存在的。

最典型的莫过于 匿名内部类。java可以将类的实例作为参数进行传递,而匿名内部类可以在创建时编写函数。这样就间接实现了传递函数的作用。以下是通过匿名内部类实现的一个回调

// doPlayer为攻击方,bePlayer为受击方。当受击方血量变化时,为攻击方回复魔法值 
Player doPlayer = ... // 
Player bePlayer = ...
bePlayer.register(new CombatAttributeListener() { // 注册一个战斗属性监听
    @Override
    public void onChangeHp(int changeNum) {
        // ...
        doPlayer.ChangeMp(changeNum); // 闭包了doPlayer
        // ...
    }
});

再进阶一下,Java普通的类其实就是闭包,类方法中捕获类的成员变量,外部只要实例化这个类,就可以调用方法,从而使用类的内部变量
并且不止Java,任何面向对象的语言的类都是闭包,不过它们一般不把类称为闭包,没为什么,就是种习惯

class Add {
   private int x = 2;
   public void add(){
       int y=3;
       x = x+y;
       System.out.println(x);
   }
}
public static void main(String[] args) {
   Add add = new Add();
   add.add();
}

闭包可能造成内存泄漏

每个语言都有自己的一套GC(垃圾回收)机制,当分配出去的内存不再被引用时便会回收;内存泄露的根本原因就是你的代码中分配了一些‘顽固的’内存,GC无法进行回收,如果这些’顽固的’内存不停地出现,就会导致后面需要的内存不足,造成泄露。

没使用闭包时,函数执行完,局部变量(假设有个局部变量n)就会被销毁。但如果n被闭包,也就多了个外部引用,只能等这个引用销毁,n才能被销毁,n的内存才能被释放

// f1执行完,n就会被回收
function f1(){
	var n=999;
}
----------------------------
function f1(){
	var n=999;
	function f2(){
		n++; // n被闭包
		console.log(n)
	}
	return f2;
}
var result=f1();
result();
// result 所在的代码块执行完,n才会被销毁。或者手动给result赋值为空

所以闭包会延迟变量的存在时间,如果a的外部引用一直不被销毁,a的内存就会始终存在。如果这段程序被调用无数次,内存就会泄漏

为了避免内存泄漏,一定要保证a的外部引用会被销毁。不能出现 闭包变量循环引用 等问题导致无法回收

ps:数学中的闭包

数学中也有 闭包 一词,和计算机科学中的 闭包 并不是一个概念

参见:离散数学中的闭包和计算机语言中的闭包有联系吗?、闭包(数学)


http://lihuaxi.xjx100.cn/news/55064.html

相关文章

蛇形矩阵蒸滴好玩

先来看看题目: 描述 康托尔三角是由著名数学家康托尔设计的一个整数三角,可以用来证明所有有理数与自然数一一对应,亦即有理数集是一个可数集。康托尔三角的构造如下: 01 02 06 07 15 16 28 29 45 46 03 05 08 14 17 27 30 44 4…

基于python+django框架+Mysql数据库的企业公司网站系统设计与实现

项目背景和意义 目的:本课题主要目标是设计并能够实现一个基于python的公司企业网站,整体基于B/S架构,技术上使用基于python的Django框架来实现;通过后台添加公司资讯、公司产品、公司产品案例、公司资讯、查看注册用户、查看留言…

Python 导入模块

Python 导入模块1.import 模块名2.import 模块名 as 名称缩写3.import 模块名.子模块名 as 名称缩写4.from 模块名 import 函数5.from 模块名.子模块名 import 函数模块 是第三方专门为了解决某些特定问题而编写的工具。Python 本身自带了一些常用的模块,例如&#…

图像处理之空间滤波

1 原理 1.1 空间滤波简介 滤波器即只让一部分频率的波形通过来达到波形过滤目的的器件。空间域指一张图像像素平面一定范围内的像素域,相对的是时间域,即多帧图像之间的关系,主要在处理视频帧时描述。在图像处理中,滤波分为两种&…

PTA - 数据库合集6(10题)

目录 10-1 查询选修‘C语言’课程的学生 10-2 查询平均分高于80分的学生 10-3 查询平均成绩最高的前3名同学 10-4 批量插入学生记录 10-5 修改女生成绩 10-7 spj-查询供应工程 j1 的供应商 10-8 spj-查询至少使用s1供应商所供应的全部零件的工程 10-9 查询年龄18-20之间…

Linux--权限管理

学习目标1. Linux权限管理1.1 用户分类2. 用户类型和访问权限2.1 理解什么是权限3 文件类型和权限操作3.1 修改权限3.2 关于root3.3 更改文件拥有者3.4 修改组权限3.5 目录权限3.5.1 粘滞位3.6 关于目录权限的总结3.7 默认权限3.7.1 自定义默认权限1. Linux权限管理 1.1 用户分…

Kubernetes(k8s)基础之三:K8s常用命令

目录 kubectl的基本命令 k8s常用操作命令 k8s常用命令操作示例 资源管理 命令式对象管理 kubectl命令 资源类型 查看k8s对象状态 k8s对象配置 k8s容器编排配置文件模板 1. deployment 相关使用 创建deployment 更新deployment 回退deployment 暂停和恢复deploym…

基于PHP+小程序(MINA框架)+Mysql数据库的积分商城小程序系统设计与实现

项目背景和意义 目的:本课题主要目标是设计并能够实现一个基于微信小程序商城系统,前台用户使用小程序,小程序使用微信开发者工具开发;后台管理使用基PPMySql的B/S架构,开发工具使用phpstorm;通过后台添加商…