自用|C#知识点:闭包
指的是闭包(Lambda 表达式 / 匿名方法)被创建时所处的代码范围。在例子中:闭包是在Main方法的代码块中定义的,因此它的 “定义时所在作用域” 就是Main方法的作用域。这个作用域内的变量(如count)就是闭包可以 “捕获” 的外部变量。闭包的本质其实是函数+函数定义时其作用域的变量或者环境的组合。“定义时所在作用域” 是闭包 “诞生” 的环境(比如Main方法内),决定了它能捕获哪些变
闭包
指:一个函数(通常是匿名函数或者Lambda表达式)能够捕获并且访问定义其时的作用域上的变量,即使该函数在原作用域之外也可以访问。
例子:
using System;
class Program
{
static void Main()
{
int count=0;//该count所在Main()函数的作用域
// 定义一个Lambda表达式(闭包),捕获了count变量
//且该Lambda表达式的定义时的作用域是Main()函数的作用域
Action incrementAndPrint = () =>
{
count++;
Console.WriteLine($"当前计数:{count}");
};
//在原作用域调用闭包
incrementAndPrint(); // 输出:当前计数:1
incrementAndPrint(); // 输出:当前计数:2
incrementAndPrint(); // 输出:当前计数:3
//在其他方法中调用闭包(原作用域之外)
ExecuteClosure(incrementAndPrint);
}
}
static void ExecuteClosure(Action action)
{
action(); // 在其他作用域中执行闭包,修改count。 输出:当前计数:4
}
1. 定义时所在作用域
指的是闭包(Lambda 表达式 / 匿名方法)被创建时所处的代码范围。
在例子中:
闭包incrementAndPrint
是在Main
方法的代码块中定义的,因此它的 “定义时所在作用域” 就是Main
方法的作用域。
这个作用域内的变量(如count
)就是闭包可以 “捕获” 的外部变量。
2. 原作用域之外被执行
指的是闭包被调用时,所处的代码范围已经不在它定义时的那个作用域内。
在例子中:
最直观的是ExecuteClosure
方法:闭包incrementAndPrint
被传递到ExecuteClosure
中执行,而ExecuteClosure
是一个独立的方法(与Main
是不同的作用域)。此时闭包的执行位置已经完全脱离了它定义时的Main
方法作用域。
3.特殊点
在不加入闭包的情况下,count属于Main()方法作用域里,当该方法结束,count也会随之销毁。
Q:那为什么count也会销毁呢?
A:
正常情况下,函数内的局部变量(如Main
方法中的count
)是存储在栈内存中的。栈内存的特点是:
随着函数的执行而 “创建”(为变量分配空间);
当函数执行结束时,函数对应的 “栈帧”(Stack Frame)会被立即销毁,栈上的局部变量也会随之消失。
Q:栈上的局部变量也会随之消失,那为什么有了闭包却可以了呢?
A:当局部变量被闭包捕获时,C# 编译器会自动进行一系列 “暗箱操作”,改变变量的存储方式,大致流程如下:
1. 生成一个 “隐藏的辅助类”
编译器会自动创建一个匿名的辅助类(你在代码中看不到,但可以通过反编译工具观察到),这个类的作用是 “托管”被闭包捕获的变量 。
//注意该生成的隐藏类下,Main
方法中的count
和闭包中访问的count
是同一个变量(编译器通过生成隐藏类,让两者指向同一个内存地址)。则不管改变哪个count,修改的都是同一个值.
比如之前的count
变量,会被变成这个类的一个字段,而Lambda表达式会变成类的方法:
// 编译器自动生成的辅助类(伪代码)
private sealed class ClosureHelper
{
// 被闭包捕获的变量
//严格意义来说,该辅助类的count其实是原count的引用,他们指向共同的count本体
public int count;
// 闭包的逻辑会被编译成这个类的方法
public void IncrementAndPrint()
{
count++;
Console.WriteLine($"当前计数:{count}");
}
}
2. 把局部变量 “迁移” 到堆上
原本存储在栈上的count
,会被改写成辅助类的实例字段。也就是说:
- 变量的存储位置从 “栈内存” 转移到了 “堆内存”;
- 堆内存的生命周期不由函数栈帧决定,而是由垃圾回收器(GC) 管理 —— 只有当变量不再被任何对象引用时,才会被回收。
3.闭包(Lambda / 匿名方法)被编译成 “指向辅助类实例方法的委托”
在 C# 中,Lambda 表达式或匿名方法最终会被编译为委托类型(如Action
、Func<T>
等)。而这里的关键是:这个委托不会是 “静态方法委托”,而是 “实例方法委托”—— 它会关联到一个具体的ClosureHelper
实例。
例如,原代码中的闭包定义:
Action incrementAndPrint = () =>
{
count++;
Console.WriteLine($"当前计数:{count}");
};
会被编译器转换为:(以下代码都在Main()函数内)
// 1. 创建辅助类实例(在堆上)
ClosureHelper helper = new ClosureHelper();
helper.count = 0; // 初始化被捕获的变量
// 2. 创建委托,指向helper实例的IncrementAndPrint方法
// 这个委托会“持有”helper实例的引用
Action incrementAndPrint = helper.IncrementAndPrint;
//委托简单来说就是实例的引用和对应函数的引用的包装。(所以有跨界传递的能力)
//所以这里的Action
委托本质上是一个 “包含实例引用 + 方法指针” 的结构 —— 它不仅知道要执行IncrementAndPrint
方法,还知道要在哪个ClosureHelper
实例上执行这个方法。
4.通过实例引用执行方法
当我们调用闭包(即调用incrementAndPrint()
)时,本质上是通过委托中持有的helper
实例引用,执行该实例的IncrementAndPrint
方法:
// 则最上面的函数调用:incrementAndPrint()等价于:
helper.IncrementAndPrint();
4.总结
闭包的本质其实是函数+函数定义时其作用域的变量或者环境的组合。
- “定义时所在作用域” 是闭包 “诞生” 的环境(比如
Main
方法内),决定了它能捕获哪些变量。 - “原作用域之外被执行” 是闭包 “工作” 的环境(比如
ExecuteClosure
方法内),体现了闭包的核心能力:即使离开诞生的环境,依然能带着捕获的变量 “工作”。
更多推荐
所有评论(0)