面试官,你为什么老是问我“闭包“

1.前言

写这篇博文的背景是前段时间在参加深圳鹏城实验室后台研发工程师一职时被问及闭包是什么,之前对闭包的理解只是停留在使用层面,并未做深层的了解。

我的回答是闭包可以让内部函数访问其所在函数的局部变量。这个回到好像并非是面试官想要的答案,然后又问到闭包的学术性定义是什么,我懵了,戳中了我的知识盲区。鉴于闭包是面试求职过程中被高频问到的一个知识点,且闭包这个术语因难以定义而臭名昭著,我们有必要对其有个全面透彻的了解。

2.定义

了解一个事物,最直接的方式是看其定义。

给一个事物下定义是一件非常困难的事情。下面看一下闭包在编程语言中的定义。

在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,指定义在函数内部引用了函数内部变量的函数。

如果第一次接触闭包,那么光看上面的定义绝对无法正真理解闭包,但是当我们了解了闭包实际的作用和用法后,回过头来再看上面的定义,闭包就不再那么晦涩难懂了。

3.作用

不同的编程语言如 Scheme、PHP、Scala、Common Lisp、Smalltalk、Groovy、JavaScript、Ruby、Python、Go、Lua、Objective C、Swift、Java(>=Java8)及 C++(>=C++11)等都对闭包有不同程度的支持。

闭包有什么作用呢,让众多语言拥抱闭包。

概括地说,闭包有两个作用:
(1)在函数外部访问函数内部变量成为可能。
(2)函数内部变量离开其作用域后始终保持在内存中而不被销毁。

4.示例

为了便于理解闭包的作用,需要结合具体实例。下面以 Go 为例,给出闭包的使用示例。注意 Go 里的闭包函数必须是匿名函数。

(1)闭包与逃逸分析。

package main

import "fmt"

func closure() func(int) int {
	var x int
	return func(a int) int {
		x++
		return a + x
	}
}

func main() {
	cl := closure()
	fmt.Println("x=", cl(1)) // x=2
	fmt.Println("x=", cl(2)) // x=4
}

Go 提供了相关的命令,可以查看变量是否发生逃逸。使用如下命令编译上述代码。

go build -gcflags "-l -m" main.go
# command-line-arguments
./main.go:6:6: moved to heap: x
./main.go:7:9: func literal escapes to heap
./main.go:15:13: main ... argument does not escape
./main.go:15:14: "x=" escapes to heap
./main.go:15:22: cl(1) escapes to heap
./main.go:16:13: main ... argument does not escape
./main.go:16:14: "x=" escapes to heap
./main.go:16:22: cl(2) escapes to heap

从输出结果可以看到函数 closure() 的内部变量 x 发生了逃逸,成为了堆变量,以此延长了内部变量 x 的生命周期,保证了其离开其作用域后始终保持在内存中而不被销毁。

(2)利用闭包实现一个简单的计数器。

package main

import (
    "fmt"
)

// Counter 创建计数器,返回一个匿名函数 func() int
func Counter(begin, step int) func() int {
	begin -= step
	return func() int {
		begin += step
		return begin
	}
}

func main() {
	counter0 := Counter(0, 1)
	fmt.Println(counter0())
	fmt.Println(counter0())
	fmt.Println(counter0())
	
	counter10 := Counter(10, 1)
	fmt.Println(counter10())
	fmt.Println(counter10())
}

运行输出:

0
1
2
10
11

通过上面的例子可以看到,将返回的匿名函数赋给 counter0,通过 counter0 完成了对函数 Counter 内部变量 begin 的访问,并使 begin 离开其作用域后始终保持在内存中而不被销毁,只要闭包还被使用,那么被闭包引用的变量会一直存在。新创建的匿名函数对象 counter10 所引用的内部变量 begin 将重新被赋予指定的初始值。

(3)通过闭包可以比较优雅地实现一些功能,比如斐波那契数列。

package main

import (
    "fmt"
)

// FibGen 斐波那契数列生成器。
func FibGen() func() int {
	f1, f2 := 0, 1
	return func() int {
		f1, f2 = f2, f1+f2
		return f1
	}
}

func main() {
	fibGen := FibGen()
	for i := 0; i < 10; i++ {
		fmt.Print(fibGen(), " ")
	}
}

运行输出:

1 1 2 3 5 8 13 21 34 55

(4)延迟调用与闭包。
defer 调用会在当前函数执行结束前才被执行,这些调用被称为延迟调用 。defer 中使用的匿名函数也是一个闭包。

package main

import "fmt"

func main() {
	x, y := 1, 2

	defer func(a int) {
		fmt.Printf("x=%v y=%v\n", a, y) // y 为闭包引用
	}(x) // 复制 x 的值

	x += 100
	y += 100
	fmt.Println(x, y)
}

运行输出:

101 102
x=1 y=102

注意 defer 中的 x 是 1 而不是 101,其实是原因是在 defer 定义时已经将 x 的当前值 1 复制给了 defer,defer 执行时使用的是当时 defer 定义时 x 的拷贝,而不是当前环境中 x 的值。

(5)涉及 Goroutine 的情况,多个闭包函数访问同一个内部变量。

package main

import "fmt"

func main() {
	for i := 0; i < 5; i++ {
		go func() {
			fmt.Print(i, " ") 
		}()
	}
	time.Sleep(time.Second * 1)
}

多次运行输出结果可能不一致:

2 2 5 5 5

// 或
3 3 5 5 5

// 或
5 5 5 5 5

多个闭包函数作为单独的 Go 程执行,因 Go 程调度时机的不确定性,当闭包函数被执行时,i 的值可能是 0~5 中任意某一个值。

如果想让闭包函数被执行时 i 的值是确定的,那么可以利用信道控制 Go 程同步执行,但这也失去了并发带来的性能提升。

func main() {
	ch := make(chan int, 1)
	for i := 0; i < 5; i++ {
		go func() {
			fmt.Print(i, " ")
			ch <- 1
		}()
		<-ch
	}
}

运行输出结果一致:

0 1 2 3 4

如果想让闭包函数被执行时 i 的值是确定的,还有一种方法:只需要每次将变量 i 的拷贝传进函数即可,但此时就不是使用的上下文环境中的变量了。

func main() {
	for i := 0; i < 5; i++ {
		go func(i int) {
			fmt.Print(i, " ")
		}(i)
	}
	time.Sleep(time.Second * 1)
}

可能的结果:

3 2 1 4 0

每次运行输出结果可能不同,因为 Go 程的执行顺序是不确定的,但是有一点可以肯定的是输出的结果是 0~4 五个数字,只是顺序不确定而已。

5.注意事项

闭包会导致变量逃逸到堆上来延长变量的生命周期,增加内存消耗,给 GC 带来压力,所以不能滥用闭包。

6.小结

本文虽然并未详尽列出闭包的用例,但我希望这里讨论的内容能够让你清楚地了解它的作用。闭包是函数式编程语言中一个非常强大和有用的工具,每个开发人员都应该习惯使用它。


参考文献

百度百科.闭包
Wikipedia.Closure (computer programming)
阮一峰.学习Javascript闭包(Closure)
简书.go 闭包与匿名函数这一篇就够了

深度学习是机器学习的一个子领域,它基于人工神经网络的研究,特别是利用多层次的神经网络来进行学习和模式识别。深度学习模型能够学习数据的高层次特征,这些特征对于图像和语音识别、自然语言处理、医学图像分析等应用至关重要。以下是深度学习的一些关键概念和组成部分: 1. **神经网络(Neural Networks)**:深度学习的基础是人工神经网络,它是由多个层组成的网络结构,包括输入层、隐藏层和输出层。每个层由多个神经元组成,神经元之间通过权重连接。 2. **前馈神经网络(Feedforward Neural Networks)**:这是最常见的神经网络类型,信息从输入层流向隐藏层,最终到达输出层。 3. **卷积神经网络(Convolutional Neural Networks, CNNs)**:这种网络特别适合处理具有网格结构的数据,如图像。它们使用卷积层来提取图像的特征。 4. **循环神经网络(Recurrent Neural Networks, RNNs)**:这种网络能够处理序列数据,如时间序列或自然语言,因为它们具有记忆功能,能够捕捉数据中的时间依赖性。 5. **长短期记忆网络(Long Short-Term Memory, LSTM)**:LSTM 是一种特殊的 RNN,它能够学习长期依赖关系,非常适合复杂的序列预测任务。 6. **生成对抗网络(Generative Adversarial Networks, GANs)**:由两个网络组成,一个生成器和一个判别器,它们相互竞争,生成器生成数据,判别器评估数据的真实性。 7. **深度学习框架**:如 TensorFlow、Keras、PyTorch 等,这些框架提供了构建、训练和部署深度学习模型的工具和库。 8. **激活函数(Activation Functions)**:如 ReLU、Sigmoid、Tanh 等,它们在神经网络中用于添加非线性,使得网络能够学习复杂的函数。 9. **损失函数(Loss Functions)**:用于评估模型的预测与真实值之间的差异,常见的损失函数包括均方误差(MSE)、交叉熵(Cross-Entropy)等。 10. **优化算法(Optimization Algorithms)**:如梯度下降(Gradient Descent)、随机梯度下降(SGD)、Adam 等,用于更新网络权重,以最小化损失函数。 11. **正则化(Regularization)**:技术如 Dropout、L1/L2 正则化等,用于防止模型过拟合。 12. **迁移学习(Transfer Learning)**:利用在一个任务上训练好的模型来提高另一个相关任务的性能。 深度学习在许多领域都取得了显著的成就,但它也面临着一些挑战,如对大量数据的依赖、模型的解释性差、计算资源消耗大等。研究人员正在不断探索新的方法来解决这些题。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值