这是我参与「第三届青训营 -后端场」笔记创作活动的的第 4 篇笔记。
最近学习中处处碰到闭包(closure)这个概念,鉴于博主基础不好,于是打算整理一下。
本文将粗略解释闭包的概念和存在意义。
什么是闭包
定义
闭包是一个函数值,它引用了其函数体之外的变量。该函数可以访问并赋予其引用的变量的值,换句话说,该函数被这些变量 “绑定” 在一起。
引用 wiki
a closure is a record storing a function together with an environment.
闭包是由函数和与其相关的引用环境组合而成的实体 。
这段话中有几个关键点:
- 闭包需要存在在函数外部定义,并在函数内部引用的变量。
- 脱离闭包形成的上下文,闭包也能自由使用该变量,该变量称为捕获变量。
- 内层函数作为外层函数的返回值。
函数,指的是在闭包实际实现的时候,往往通过调用一个外部函数返回其内部函数来实现的。内部函数可能是内部实名函数、匿名函数或者一段lambda表达式。用户得到一个闭包,也等同于得到了这个内部函数,每次执行这个闭包就等同于执行内部函数。
go 中的一个例子
package main
import "fmt"
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
pos, neg := adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(
pos(i),
neg(-2*i),
)
}
}
输出:
0 0
1 -2
3 -6
6 -12
10 -20
15 -30
21 -42
28 -56
36 -72
45 -90
这里面函数 adder 返回一个函数,而这个返回的函数会引用其外部的变量 sum (捕获变量)。当函数 adder 执行结束后,函数变量 pos 和 neg 仍然可以使用 adder 中的变量 sum 。
底层剖析
以上述 go 的代码为例,pos 和 neg 在接受返回函数时 go 会在堆中创建两个不同的 funcval 结构体,而这两个 funcval 结构体中存放的都是相同的地址和捕获变量的列表,即 adder 在编译时形成的代码段地址和变量各自捕获列表中的 sum ,之后这两个 funcval 会作为返回值返回给 栈 中的 pos 和 neg。
闭包的作用
闭包的价值 闭包的价值在于可以作为函数对象或者匿名函数,对于类型系统而言,这意味着不仅要表示 数据还要表示代码。支持闭包的多数语言都将函数作为第一级对象,就是说这些函数可以存储到 变量中作为参数传递给其他函数,最重要的是能够被函数动态创建和返回。
Go语言中的闭包同样也会引用到函数外的变量。闭包的实现确保只要闭包还被使用,那么被闭包引用的变量会一直存在。
以下是闭包的常见用法:
- 读取函数的内部变量,并且让变量保存在内存中。
- 闭包既能重复使用局部变量,又不污染全局。
- 可以实现变量私有化,实现数据寄存。
闭包缺点
- 使用不当会造成内存泄漏
- 大量使用会耗费内存空间
用闭包实现的斐波纳契函数
package main
import "fmt"
// 返回一个“返回int的函数”
func fibonacci() func() int {
a, b := 0, 1
return func()int{
t := a
a, b = b, a + b
return t
}
}
func main() {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}
0
1
1
2
3
5
8
13
21
34
另一个🌰
package main
import "fmt"
func main(){
for i := 0; i < 3; i++ {
defer func(){
fmt.Println(i)
}()
}
}
输出
3
3
3
defer 会在 return 之前执行,并且遵循先进后出的规则。而这个 defer 匿名函数捕获了 for 循环中的变量 i ,形成了闭包。当最后一次 for 循环完成时 i++
i 值为 3 。因此输出三次 3。