闭包

指:一个函数(通常是匿名函数或者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 表达式或匿名方法最终会被编译为委托类型(如ActionFunc<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方法内),体现了闭包的核心能力:即使离开诞生的环境,依然能带着捕获的变量 “工作”。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐