浅谈闭包|青训营笔记

83 阅读3分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第 4 篇笔记。

最近学习中处处碰到闭包(closure)这个概念,鉴于博主基础不好,于是打算整理一下。
本文将粗略解释闭包的概念和存在意义。

什么是闭包

定义

闭包是一个函数值,它引用了其函数体之外的变量。该函数可以访问并赋予其引用的变量的值,换句话说,该函数被这些变量 “绑定” 在一起。

引用 wiki
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 执行结束后,函数变量 posneg 仍然可以使用 adder 中的变量 sum

底层剖析

以上述 go 的代码为例,posneg 在接受返回函数时 go 会在中创建两个不同的 funcval 结构体,而这两个 funcval 结构体中存放的都是相同的地址和捕获变量的列表,即 adder 在编译时形成的代码段地址和变量各自捕获列表中的 sum ,之后这两个 funcval 会作为返回值返回给 中的 posneg

闭包的作用

闭包的价值 闭包的价值在于可以作为函数对象或者匿名函数,对于类型系统而言,这意味着不仅要表示 数据还要表示代码。支持闭包的多数语言都将函数作为第一级对象,就是说这些函数可以存储到 变量中作为参数传递给其他函数,最重要的是能够被函数动态创建和返回。

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。