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

在编译后,所谓的闭包是编译为单独的函数,所以原文的作者也把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!");
            }
};
 

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变为后面所说FnMut。

注:move关键字其实不是必须,因为用不用move和一个被捕获变量是否可以修改没有关系! move仅仅是显式告诉编译器,要把变量的所有权转移到匿名函数之内。

如果你写上了,只是更便于阅读和维护。

 

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

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

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

四、Fn特质

2025/04/09 补充

特质(trait)按照顺序,本不是本章节能接触到的。 不过在原文中,已经提到了特质这个东西。

这里假定读者已经知道特质是什么东西。

这里直接应用译文内容:

  • FnOnce 适用于只能被调用一次的闭包。所有闭包至少都实现了这个 trait,因为所有闭包都能被调用。一个会将捕获的值从闭包体中移出的闭包只会实现 FnOnce trait,而不会实现其他 Fn 相关的 trait,因为它只能被调用一次。
  • FnMut 适用于不会将捕获的值移出闭包体,但可能会修改捕获值的闭包。这类闭包可以被调用多次。
  • Fn 适用于既不将捕获的值移出闭包体,也不修改捕获值的闭包,同时也包括不从环境中捕获任何值的闭包。这类闭包可以被多次调用而不会改变其环境,这在会多次并发调用闭包的场景中十分重要

注:原文含糊不清,而译文也没有传达真正的意思。 建议参考 

rust进阶-基础.1.匿名函数和FnXXX特质 

 

比较

a.使用次数

  • FnOnce -1次
  • FnMut - 多次
  • Fn-多次

b.是否可以修改

  • FnOnce 看情况
  • FnMut - 通常可以修改
  • Fn-不可修改

总结起来就是

  • FnOnce - 执行1次,是否可以修改,看情况
  • FnMut  - 可执行多次,可修改
  • Fn        -可执行多次,不修改

三者从次数和是否可修改角度出发,递进层级为FnOnce->FnMut->Fn

在大部分情况下,工程师都不需要关心rustc是符合实现,何时实现。

那么了解这个又有什么用了?

主要是可以在定义函数/方法参数的时候可以作为特质绑定使用,约束参数的某些特性。

例如原书的例子:

impl<T> Option<T> {
    pub fn unwrap_or_else<F>(self, f: F) -> T
    where
        F: FnOnce() -> T
    {
        match self {
            Some(x) => x,
            None => f(),
        }
    }
}

这就要求传入的匿名函数只能执行一次,且会把捕获的值移出(不夺取被捕获变量的所有权)。

 

另外,在本书的高级章节中,有谈到函数指针,函数指针实现了FnOnce,FnMut,Fn.

相关内容参考:    rust学习二十.13、RUST的函数指针fn和匿名函数

五、示例

5.1 简单示例

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);
}

 

5.2、FnOnce、FnMut,Fn测试

2025/04/25 补充

#[derive(Debug)]
struct Book{
    title: String,
    author: String,
    age: u32,
}

impl Book {
    fn new(title: &str, author: &str,age:u32) -> Self {
        Book { title: title.to_string(), author: author.to_string() ,
        age: age,
        }
    }

    fn print(&self) {
        println!("{} 作者 {}(发行时年龄{})", self.title, self.author,self.age);
    }
}

#[allow(non_snake_case)]
fn test_FnOnce<T:FnOnce()>(f:T) {
    println!("调用FnOnce,只能一次");
    f();
}
 
#[allow(non_snake_case)] 
fn test_FnMut<T:FnMut()>(mut f: T) {
    println!("调用FnMut,多次执行");
    f();
    f();
}
#[allow(non_snake_case)]
fn test_Fn<T:Fn()>(f: T) {
    println!("调用Fn,多次执行");
    f();
    f();
}

fn main() {

    //1.0 测试FnOnce特质
    let book1=Book::new("唐诗三百首", "孙洙(蘅塘退士)",54);
    let f1 = || {
        book1.print();
    };
    test_FnOnce(f1);    
    //这个ts.print还可以继续使用,说明它被FnOnce归还了。
    book1.print();
    

    //2.0 测试FnMut特质
    println!("-----------------------------------------");
    let mut book2 = Book::new("Rust程序设计语言", "Steve Klabnik, Carol Nichols",45);
    println!("book2地址: {:p}", &book2);
    let mut f2 = move || {
        book2.age+=1;
        book2.print();
        //这里可以明显看出变量地址发生了变化,因为所有权转移了
        println!("book2地址: {:p}", &book2);
    };
    test_FnMut(f2);
    //println!("{}",book2.age);  //book1不可用是因为move转移了所有权,且FnMut需要可变借用

    println!("-----------------------------------------");
    let book3= Book::new("认识儿童绘画的特定作用", "卢ml",13);
    println!("book3地址: {:p}", &book3);
    let f3 = || {
        println!("闭包内book3地址: {:p}", &book3);
        book3.print();
    };
    test_Fn(f3);
    println!("{}",book3.age);  //book2仍然可用,因为Fn只捕获了不可变引用
    println!("外部book3地址: {:p}", &book3);  //验证地址是否相同
}

输出结果:

注意:rustc有时候会给出错误的提示,例如f2不要加mut,但是根据rust的规定FnMut函数必须添加mut修饰符。

在本例中,test_FnMut和test_Fn都添加了打印所捕获变量的地址的语句,通过这个可以判断是否被转移所有权(如果地址变了那么所有权就转移了)。

从测试可以看出,对于test_FnMut,其相关的外部变量book2的地址一开始是:0x97d5eff548,转移后则是0x97d5eff4d0。

而FnMut没有变化。

 

六、小结

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

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

       

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