00:09:09
闭包(Closure)是编程中一个强大却常被误解的概念。本文将以 C# 为例,解析闭包的本质、应用场景及底层实现,帮助你写出更高效、更优雅的代码。
假设有一个函数 Puzzle(int a, int b)
,当某个条件为真时,它会完全忽略第二个参数 b
。在 C# 这类严格求值(eager evaluation)的语言中,无论是否需要,所有参数都会在调用前被计算。这意味着,即使 b
是一个耗时操作(例如需要循环一百万次),它的计算成本也无法避免。
相比之下,Haskell 这类惰性求值(lazy evaluation)语言则只在需要时才计算参数,从而避免不必要的性能损耗。
在 C# 中,我们可以通过委托(Delegate)模拟惰性求值。将函数签名改为 Puzzle(int a, Func<int> b)
,其中第二个参数是一个返回整数的委托。这样,只有在确实需要 b
的值时,才会执行 b()
。
这种模式有两种常见应用场景:
为了兼顾便利性,可以为该函数创建重载版本,允许传入直接的值,由重载方法自动包装成委托。
接收或返回委托的函数被称为高阶函数(Higher-Order Function)。当 lambda 表达式或匿名函数捕获(Capture)了其外部作用域的变量时,就形成了闭包。
闭包的特殊之处在于,它捕获的不是变量某个时刻的值,而是变量本身。这意味着,闭包内外操作的是同一个变量,任何一方对变量的修改都会被另一方看到。
闭包并非魔法。C# 编译器在背后自动生成了一个“助手类”,将被捕获的变量(如 n
和 m
)提升(Hoist)为该类的公共字段。而 lambda 表达式则被编译为该类的一个实例方法。
因此,原先的局部变量变成了对象的字段,lambda 表达式和外部代码实际上是通过操作同一个对象的字段来共享和修改“变量”的。这完美解释了为何闭包能持续访问和修改已“离开”其原始作用域的变量。
闭包是一个将代码(委托)和数据(被捕获的变量环境)捆绑在一起的实体。它的两个核心价值是:
从本质上讲,任何一个能够访问和操作其所在类字段的实例方法,其行为模式就是一个闭包。理解了这一点,你就会发现,闭包并非什么神秘的新概念,而是你早已熟悉的对象导向编程思想的另一种表现形式。