前言
c#闭包是一个很强大的功能,在平时项目中会经常用到,我们大多数人在体验其便利性的同时,可能不会去考虑其背后的原理。这篇文章简要讲述闭包的工作原理,已帮助大家正确的使用闭包。
一、什么是闭包?
闭包的概念应该源于离散数学这门计算机基础课程,离散数学中,一个关系R的闭包,是指加上最小数目的有序偶而形成的具有自反性,对称性或传递性的新的有序偶集,此集就是关系R的闭包。1
通俗理解就是一在一个不完全的关系集上加上某些特定的序偶对使其满足了关系的某个性质。编程上闭包的概念可以类比的方式理解。
闭包(Closure)在编程中的定义是一个函数或方法与其引用环境(通常是指函数体外部的变量)的组合。闭包使得函数可以访问其定义时的环境,即使在函数被执行时,这些变量已经超出了它们的原始作用域。(来自chatGPT的解释)
具体的我们平时使用的lambda表达式,委托,本地方法等都可以视为闭包。
二、闭包的工作原理
1.代码示例:
public Action CreateCounter()
{
int count = 0;
return () =>
{
count++;
Console.WriteLine(count);
};
}
public void Test()
{
var counter = CreateCounter();
counter(); // 输出 1
counter(); // 输出 2
}
上述的代码是一个很简单的闭包示例,其中count是方法体中定义的局部变量,但却可以在另一个方法中被使用,为什么count 能够拥有超出其作用域的生命周期,其背后的原理是什么,count在内存的布局中究竟是怎样的存在形式,这些问题都深深困扰着我。
我最初的猜想是:在运行时被闭包捕获的变量分配在堆上,而非栈上,使得变量拥有超过其作用域的生命周期,而栈上存储的是指向该变量的地址,经过向chatGPT的求证得出了大差不差的结论,这个结论也确实能够解除我的这些疑惑。以下是chatGPT给出的解释。
当一个委托捕获了一个方法体内的局部变量时,编译器会自动将这些局部变量提升到一个类的字段中。这个类通常被称为“闭包类”或“显示捕获类”(display class)。这样就确保了这些变量在方法体执行结束后仍然存在于堆上。
2.编译器背后的工作
经过Chatgpt的解释,我猜测编译器背后的处理工作如下:
- 根据闭包所捕获的局部变量,编译器会自动创建一个形如以下代码的闭包类
class Closure { public int count; public void func() { Console.WriteLine(count); } }
- 将上述方法转换为使用此闭包的等效实现
public Closure CreateCounter() { Closure closure = new Closure(); closure.count = 0; return closure; } public void Test() { var counter = CreateCounter(); counter.func(); counter.func(); }
总结
以上是我对于闭包背后工作原理的理解,希望可以帮助到跟我有相同困惑的同学,如有不正确之处欢迎各位大神指正。