闭包--C# lamda VS Rust闭包

Rust中闭包捕获环境变量(上下文)有三种形式,捕获变量的不可变借用,捕获变量的可变借用,捕获变量所有权。
捕获变量的不可变借用就是Fn trait,该闭包可以多次执行。
捕获变量的可变借用就是FnMut trait,该闭包可以多次执行,但需将该闭包对象声明为mut,即表明该trait对象会修改环境变量。
捕获变量所有权就是FnOnce trait,由于该闭包捕获了变量所有权,所以只能执行一次,且使用move明确的将变量的所有权移动到闭包中。
以上这些都是老生常谈的东西了。

不过我发现了另一个有意思的问题,变量捕获是发生在什么时候,是在闭包执行时?还是在闭包定义时?
“凭借”(其实是想当然)我多年的C#开发经验,一开始想当然的是发生在执行时,C#就是发生在执行时(其实不对),代码证明如下:

C#Lamda表达式对环境变量的捕获

internal class LamdaFun1
{
    public void Run()
    {
   	 //此处特意使用了引用类型的变量和值类型变量,但在lamda中调用时,他俩的表现是一致的
        StringBuilder builder =new StringBuilder("a");
        int x = 1;
        var lamda = () =>
        {
            Console.WriteLine(builder);
            Console.WriteLine(x);
        };

        for (int i = 0; i < 5; i++)
        {
            x++;
            lamda();
            builder.Append(i);
        }
   	 Console.WriteLine(builder.ToString());
   	 Console.WriteLine(x);
    }
}

上面代码打印结果如下:

a0
2
a01
3
a012
4
a0123
5
a01234
6
a01234
6

从结果来分析,在lamda执行时,每次执行都会重新读取所引用变量(builder和x)的值,尽管x是值类型,但C#并不会进行值拷贝,反而引用(或类似引用类型方式调用),最终的打印也说明,两个外部的变量确实进行了修改。

Rust闭包对环境变量的捕获

    let mut str="Hello ".to_string();
    let mut closure_fn=move ||{
        str.push_str("world! ");
        println!("{str}");
    };
    closure_fn();
    closure_fn();

在执行这段代码之前,我完全没想到输出结果。
甚至我一开始都怀疑这段代码能否通过编译,因为已经使用move将str的所有权移动到闭包中,然后因为closure_fn持有str的所有权,在执行一次后就会释放,当然事实证明我想错了。

上面代码的执行结果:

Hello world! 
Hello world! world! 

上面的代码看似简单,但其实有几个关键点

  1. closure_fn 是FnMut,不是FnOnce,不能想当然的看到move就认为是FnOnce,关键要看在闭包中如何使用环境变量。move关键字只是表明该变量移动到闭包内,归闭包所有,不是有move就是FnOnce,在闭包内,push_str只需要str变量的&mut Self,因此该闭包为FnMut。
  2. 理解闭包为FnMut后,第二条输出的结果就好理解了,move将变量移动到闭包中,变量的生命周期与闭包相同,然后执行两次而已。

闭包捕获变量的时机

再然后就有了最上面的疑问,Rust有严格的变量所有权和生命周期控制,闭包是在什么时候捕获环境变量的,经过思考不难发现,是在闭包声明(定义)的时候。例如以下代码就无法执行

    let mut str="Hello ".to_string();
    let mut closure_fn=move ||{
        str.push_str("world! ");
        println!("{str}");
    };
    //报错:borrow of moved value: `str`
    println!("{}",str);

报错告诉你,str已经移动到闭包中了,你无法在此处使用,尽管没有执行闭包。

但,总想找找有没有例外的情况,假如一个场景,需要在闭包内读取变量,在闭包外还要修改变量该怎么办呢?
自然,想到了使用Cell和RefCell,代码如下:

    //使用RefCell的
	let str = "a".to_string();
    let cell = RefCell::new(str);
    let print = || println!("{}", cell.borrow());
    cell.borrow_mut().push_str(" hello world");
    print();

	//使用Cell的
	let i = Cell::new(0);
    let f = || println!("{}", i.get());
    i.set(i.get() + 1);
    f();

以上代码的逻辑很简单

  1. 先让闭包捕获环境变量
  2. 在闭包外修改变量
  3. 执行闭包打印变量的值
    结果也没问题,都可以打印出修改后的值。
    经过之前的也可以得出原因,两个闭包因为只使用了&self,因此都是Fn(),所以...,而且也能推断出,即便将修改值的逻辑放在闭包内也没问题,因为不管是RefCell的borrow_mut方法还是Cell的set方法,都是&self,因此闭包也还是Fn()。

结论

经过分析,明确了以下几点
1.闭包获取变量是在声明时,而不是在运行时。
2. 判断闭包是FnMut还是FnOnce,不是看变量是否move到闭包内,而是看闭包内使用变量的方式,使用的&mut Self就是FnMut,使用的Self就是FnOnce。
3. 如果要在闭包内外同时访问变量,且保持可修改,单线程下使用RefCell或Cell,多线程下使用Arc或Arc等支持多线程的就行。

以下是错误的,保留下来仅做为思考路径展示
另外,反过来再想想C# 中Lamda表达式捕获环境变量的逻辑,应该跟Rust是一样的,必须在闭包声明时就明确捕获哪些变量,而且应该是记录了该变量的内存位置(比如说值类型的变量),不然无法获取到变量更新后的值。但还有一点,如果变量的地址发生了改变(数组扩容,导致移动了内存位置),是如何通知已经捕获该变量的lamda表达式的?或者说如何避免这种情况?

以下为更正内容
在C#中,lamda表达式捕获变量是靠编译器将被捕获变量提升到一个隐藏类中,之后不管在lamda内部或外部对该变量的修改都修改的是那个隐藏类中的变量,打个比方就是,编译器将被捕获的变量偷偷的放到一个小盒子中,变量就是小盒子中的一个物件,之后不管是lamda还是其他对该变量的修改,都是修改的小盒子内的那一个,因此不管是变量地址发生改变,还是值发生改变,在任何地方都可以读取到正确的值。

posted @ 2026-04-13 22:21  伊森-w  阅读(7)  评论(0)    收藏  举报