rust十三.1、匿名函数(闭包)

在编译后,所谓的闭包是编译为单独的函数,所以原文的作者也把closure称为函数。

因此,本文也称为函数。这个更好理解的一个概念。

一、概念

在某个程序体内定义的一段代码,具有参数(可选)和程序体,但不具有名称,实现函数作用,这样的代码称为匿名函数(closure)。

匿名函数这个东西,现在各个语言大行其道,核心的原因是更加方便,某些习惯这样思维的工程师能从此受益。在没有匿名函数之前,程序也运行得好好的。

这种思维方式也有负面的地方,其中一个是可能导致严谨性缺失,培养乱丢垃圾的习惯。

所以,所谓的匿名函数,本质上是编译器的把戏!

二、定义方式

主要有两种方式:

  1. 指定持有者变量方式
  2. 无持有者变量方式-这一种相当常见

但无论哪一种方式,在编码上,不会给它们添加函数名称,难以名之

2.1、指定持有者变量方式

let f = |x| { println!("不改变捕获的x={}", x) };

2.2、无持有者变量方式

fn  giveaway(&mut self, user_prefer:Option<ShirtColor>) -> ShirtColor {
        user_prefer.unwrap_or_else(|| self.most_stocked())
}
函数unwrap_or_else中内容就是一个匿名函数.
 

2.3、和其它语言比较

前文说过,现在很多语言都有这种图方便的写法。
java有匿名函数和朗达表达式,javascript也有类似的匿名函数和朗达表达式。
和rust比起来,个人觉得还是java,js的书写方式更加地人性化。例如java可以这样写:
Isort sort2 = (a, b) -> a + b;
Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello from anonymous Runnable!");
            }
};

 

js可以这样写

// 带有对象字面量的箭头函数(注意:需要括号)
const createPerson = (name, age) => ({ name: name, age: age });
const person = createPerson("Alice", 30);
console.log(person); // 输出: { name: 'Alice', age: 30 }
// 箭头函数中的 this 绑定

function Person() {
    this.age = 0;
    setInterval(() => {
        this.age++; // `this` 绑定到 Person 实例
        console.log(this.age);
    }, 1000);
}

rust使用的是比较怪异的 ||替代(),并且参数定义区域和函数体之间只是用空格分隔,这种方式无疑会让初学者误会,也不太符合大部分语言的约定。

然而,刻意地与众不同,应该是rust发明人追求目标。

只要这样这个目标没有影响到另外一个目标(安全、高效),那么也还是可以忍受的!

三、变量捕获和引用问题

如果有学过其它语言,这个其实很容易理解,其实么有特别好理解的。

rust的麻烦主要是所有权和引用所导致的。

这些问题其实可以归结为三个:

  1. 如何捕获
  2. 影响之-如何使用:能不能修改
  3. 影响之-所有权

3.1、捕获方式

一句话:自动捕获

编译器通过匿名函数的编写和执行时候所使用的参数来确定捕获了什么变量。

直接在函数体捕获外部变量

let mut z = 20;
let mut closure2 = || {
  z += 1;
};

通过参数捕获(实为传参)

let  fx=|y| y+1;
let v=20;
let r=fx(v);
println!("{}+1={}",v,r);
 
处于图方便的缘故,第一种形式比较多,这一点在js中也是类似的。

3.2、可变捕获

即不但捕获,还要在匿名函数体内改变被捕获变量的值。

如果采用”指定持有者变量“方式来定义此类匿名函数,那么必须把为这个变量指定mut关键字,例如:

let mut z = 20;
let mut closure2 = || {
  z += 1;
};
如果不是,则不需要。
 
可变捕获后,有一个很特别的事情需要记住:一旦你使用了可变捕获捕获一个变量,那么在最后一次匿名函数被调用之前,你不能在父级作用域使用被捕获的变量
否则,编译器会提示:immutable borrow occurs here.
或者提示 :cannot borrow `xxx` as mutable more than once at a time
 

3.3、所有权问题

在默认情况下,捕获变量,不会导致所有权变化,只是纯粹的引用:不可变引用,或者是可变引用。

大部分时候,我们只是希望借用下,就像大部分语言,函数那样使用匿名函数。匿名还是在这种情况下,仅仅只是作为一个黑盒,有借有还!

如果你希望所有权转移到匿名函数体内,那么需要借助关键字 move,例如:

let mut move_fn= move || {
  vec.push(4);
  println!("{:?}", vec);
};

以上一段代码中,匿名函数closure3拥有了vec的所有权。

当然奇特的不仅仅是在于这,而是你如果执行了类似move_fn一次后,还可以持续多次调用move_fn,而且vec的值会一直变化。

基于其它语言的概念和习惯,工程师们需要一段时间来适应这种有别于传统的方式。

然而难度也不是那么大,只是有一点而已。习惯它,只要知道编译器就是这么规定的,困难的事情是编译器在做。

四、示例

fn main() {
    let x = 10;

    // 1. 没有捕获
    let closure1 = |y| x + y;
    println!("1.0执行前x={}", x); //x是不可变引用,所以x可以在父级作用域不停使用
    let x1=closure1(10);
    println!("1.0现在{}+{}={},x还是{}", x,10,x1,x); //x是不可变引用,所以x可以在父级作用域不停使用

    // 1.5 捕获,但是不变
    let f = |x| { println!("不改变捕获的x={}", x) };
    println!("1.5执行前x={}", x); //x是不可变引用,所以x可以在父级作用域不停使用
    f(x);
    println!("1.5现在x={}", x); //x是不可变引用,所以x可以在父级作用域不停使用

    //2.0 可变捕获
    let mut z = 20;

    println!("2.0执行前,z={}", z);
    // 可变捕获,实现FnMut
    let mut closure2 = || {
        z += 1;
    };
    closure2();
    //println!("第一次调用后,z={}", z);   //编译错误 immutable borrow occurs here
    closure2();
    println!("2.0第二次调用后,z={}", z);

    // 3.0 所有权转移的可变捕获
    let mut vec = vec![1, 2, 3];

    println!("3.0执行前,vec={:?}", vec);
    // 所有权转移,只实现FnOnce
    let mut closure3 = move || {
        vec.push(4);
        println!("{:?}", vec);
    };

    closure3();
    //println!("现在vec={:?}", vec);  //编译错误,因为被closure3借用后,vec已经不在范围之内(消失了);
    closure3();

    let  fx=|y| y+1;
    let v=20;
    let r=fx(v);
    println!("{}+1={}",v,r);
}

 

 

五、小结

  1. 匿名函数的确方便了新一代的工程师。但匿名函数在rust还是有大用的
  2. rust的所有权问题让它的匿名函数和其它语言存在较大的不同
  3. 如果是可变捕获,那么会存在一个糟糕的,比较难于理解的现象:一旦你使用了可变捕获捕获一个变量,那么在最后一次匿名函数被调用之前,你不能在父级作用域使用被捕获的变量

           而在其它语言中,不会有这个问题:因为我们都认为:定义是定义,都还有使用怎么就捕获了?

       

posted @ 2024-12-08 13:11  正在战斗中  阅读(45)  评论(0编辑  收藏  举报