Skip to content

闭包 Closure

闭包时一种匿名函数它可以赋值给一个变量,也可以作为参数传递给其他函数

不同的是闭包可以捕获调用者作用域中的值

1.基本使用

闭包的基本语法如下

rust
|param1, param2,...|{
    // 函数体
    返回值
}

这么一看,闭包函数和普通函数大差不差,参数列表不是用小括号(),而是用||

函数内部也是和普通函数一样,正常书写函数体,最后一行若没有;,就视为返回值

当闭包只有一个返回值时,可以省略{}

rust
|param1, param2,...| 返回值

闭包可以赋值给一个变量,然后可以通过这个变量来调用闭包函数

rust
let print = |str| {
    println!("{}", str);
};

print("你好"); // 打印你好

// 使用返回值
let get_value = |value| value;

let val = get_value(123);
println!("{}", val)     // 123

2.闭包的类型推导

在书写普通函数时,我们需要给每一个参数的类型显示声明,因为普通函数通常需要作为Api可供外部使用

但是闭包函数不会作为Api最外提供,因此闭包函数享受编译器的类型推导能力

所以闭包函数的参数类型和返回值类型无需显式添加

一般我们写个函数可以这样

rust
let sum = |x: i32, y: i32| -> i32 {
    x + y
}

为了方便,可以简化为这样

rust
let sum  = |x, y| x + y;

但是要注意,类型推导不是泛型,当编译器推导出一种类型后,它就会一直使用该类型

rust
let example_closure = |x| x;

let s = example_closure(String::from("hello"));
let n = example_closure(5);  // 此处会报错

上面代码中,第一次调用example_closure后,rust已经固定参数类型为String了,所以再次调用时传入一个整型,就会报错.

3.捕获作用域中的值

闭包可以捕获作用域中的值,例如

rust
fn main() {
    let x = 4;
    let capture = || {
        println!("{}", x);
    };

    capture()
}

上面代码中,x不是属于capture函数中的值,但是在函数中也是可以使用,

这对于普通函数来说是不可以的

rust
fn main() {
    let x = 4;

    fn capture() {
        println!("{}", x);          // 这里会报错
    }

    capture()
}

当闭包函数中使用了一个变量,会现在闭包函数的当前作用域去寻找,若找不到就会往上层作用域找,如果找到顶层作用域都没找到,则会给你报错

rust
const PI: f32 = 3.14;

fn main() {
    let x = 1;
    {
        let y = 2;
        {
            let z = 3;
            let capture = || {
                println!("{},{},{},{}", PI, x, y, z); // 3.14,1,2,3
                // println!("{}", a);      // 这里会报错,因为a不存在
            };

            capture()
        }
    }
}

4.闭包对内存的影响

当闭包从环境中捕获一个值时,会分配内存去存储这些值。对于有些场景来说,这种额外的内存分配会成为一种负担。与之相比,函数就不会去捕获这些环境值,因此定义和使用函数不会拥有这种内存负担。

5.闭包特性

闭包一共有三种trait特性可以实现,区别在于对作用与变量的捕获方式,恰好对应函数参数的三种传入方式:转移所有权、可变借用、不可变借用

  • FnOnce: 只需要捕获所有权,闭包只能调用一次。
  • FnMut: 需要修改捕获的变量,闭包可以多次调用。
  • Fn: 不需要修改捕获的变量,闭包可以多次调用。

5.1 FnOnce

FnOnce的核心特征是只能被调用一次,并且在该闭包内会拿走被捕获变量的所有权

捕获所有权意味着变量从原始作用域移动(move)到了闭包内部

rust
fn fn_once<F>(func: F)
where
    F: FnOnce(usize) -> bool,
{
    println!("{}", func(3));
    // println!("{}", func(4));			// 报错,func只能被使用一次
}

fn main() {
    let x = vec![1, 2, 3];
    fn_once(|z| {
        let v = x;		// x被移动,所有权给了v
        let len = v.len();
        z == len
    });

    // println!("{:?}", x)   // 报错,x已被移动
}

5.2 FnMut

FnMut以可变借用的方式捕获了环境中的值,因此可以修改该值:

rust
fn main() {
    let mut s = String::new();

    let update_string =  |str| s.push_str(str);
    update_string("hello");

    println!("{:?}",s);
}

这么写编译器会报错,因为闭包默认是实现的Fn

想要在闭包内部捕获可变借用,需要把该闭包声明为可变类型,这样闭包会自动推导为FnMut

rust
fn main() {
    let mut s = String::new();

    let mut update_string =  |str| s.push_str(str);
    update_string("hello");

    println!("{:?}",s);
}

FnMut的闭包作为函数的参数时,需要给参数添加mut关键字

rust
fn fn_once<F>(mut func: F)
where
    F: FnMut(),
{
    func();
}

fn main() {
    let mut s = String::from("hello");
    fn_once(|| {
        s.push_str("world");
    });

    println!("{}", s); // helloworld
}

TIP

只要闭包捕获的类型都实现了Copy特征的话,这个闭包就会默认实现Copy特征。

我们来看一个例子:

rust
let s = String::new();
let update_string =  || println!("{}",s);

这里取得的是s的不可变引用,所以是能Copy的。而如果拿到的是s的所有权或可变引用,都是不能Copy的。我们刚刚的代码就属于第二类,取得的是s的可变引用,没有实现Copy

rust
// 拿所有权
let s = String::new();
let update_string = move || println!("{}", s);

exec(update_string);
// exec2(update_string); // 不能再用了

// 可变引用
let mut s = String::new();
let mut update_string = || s.push_str("hello");
exec(update_string);
// exec1(update_string); // 不能再用了

5.3 Fn

Fn 特征以不可变借用的方式捕获环境中的值,一个闭包默认实现Fn特征

rust
let s = String::from("hello");

let update_string = || {
    println!("{}", s);      // hello 不可变借用
};

update_string();
println!("{}", s);  // hello   仍可以使用

5.4 三种Fn的关系

实际上,一个闭包并不仅仅实现某一种 Fn 特征,规则如下:

  • 所有的闭包都自动实现了 FnOnce 特征,因此任何一个闭包都至少可以被调用一次
  • 没有移出所捕获变量的所有权的闭包自动实现了 FnMut 特征
  • 不需要对捕获变量进行改变的闭包自动实现了 Fn 特征
rust
fn main() {
    let s = String::new();

    let update_string =  || println!("{}",s);

    exec(update_string);
    exec1(update_string);
    exec2(update_string);
}

fn exec<F: FnOnce()>(f: F)  {
    f()
}

fn exec1<F: FnMut()>(mut f: F)  {
    f()
}

fn exec2<F: Fn()>(f: F)  {
    f()
}

6.闭包在函数中的应用

6.1 作为参数

闭包作为参数,这种写法是经常见到的,通常作为回调函数使用

注意闭包作为参数时要使用泛型,以下代码中,有一个F泛型,泛型约束为FnOnce(String),也就是说我们的闭包函数是FnOnce的,并且有一个String类型参数,然后在参数列表中给闭包变量设置这个F泛型

rust
fn get_hello<F: FnOnce(String)>(f: F) {
    f("hello".to_string());
}
fn main() {
    get_hello(|x| println!("{}", x))
}

6.2 作为返回值

闭包还可以作为函数的返回值。由于闭包是匿名的,我们需要使用 impl TraitBox 来描述其类型。

使用 impl Fn 返回闭包

rust
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
    move |y| x + y
}

fn main() {
    let add_five = make_adder(5);
    println!("5 + 3 = {}", add_five(3)); // 输出: 5 + 3 = 8
}

使用 Box<dyn Fn> 返回闭包

rust
fn make_adder(x: i32) -> Box<dyn Fn(i32) -> i32> {
  Box::new(move |y| x + y)
}

fn main() {
  let add_ten = make_adder(10);
  println!("10 + 2 = {}", add_ten(2)); // 输出: 10 + 2 = 12
}

6.move关键字

move关键字可以让一个闭包捕获变量时

  • 对于非Copy类型,强制获取所有权,如StringVec
  • 对于实现Copy的类型,进行复制,如i32bool

我们在Fn类型的闭包中试试

rust
fn main() {
    let x = 42; // i32,实现了 Copy
    let s = String::from("hello"); // String,未实现 Copy

    let closure = || {
        println!("数字: {}, 字符串: {}", x, s);
    };

    closure();

    println!("{}", x); // x   ,仍可以被使用
    println!("{}", s);          // hello  ,仍可以被使用
}
rust
fn main() {
    let x = 42; // i32,实现了 Copy
    let s = String::from("hello"); // String,未实现 Copy

    let closure = move || {
        println!("数字: {}, 字符串: {}", x, s);
    };

    closure();

    println!("{}", x); // x   ,仍可以被使用
    // println!("{}", s); // 错误,s的所有权已被转移
}

以上代码中的闭包默认是Fn类型的,我们再试试FnOnce

rust
fn do_somthing<T>(f: T)
where
    T: FnOnce(),
{
    f();
}

fn main() {
    let x = 42; // i32,实现了 Copy
    let s = String::from("hello"); // String,未实现 Copy

    do_somthing(|| {
        println!("x:{},s:{}", x, s);
    });

    println!("x:{}", x); // 42
    println!("s:{}", s); // hello
}
rust
fn do_somthing<T>(f: T)
where
    T: FnOnce(),
{
    f();
}

fn main() {
    let x = 42; // i32,实现了 Copy
    let s = String::from("hello"); // String,未实现 Copy

    do_somthing(move || {
        println!("x:{},s:{}", x, s);
    });

    println!("x:{}", x); // 42
    // println!("s:{}", s); // 错误,s所有权已移动
}

FnMut中也是一样

rust
fn do_somthing<T>(mut f: T)
where
    T: FnMut(),
{
    f();
}

fn main() {
    let x = 42; // i32,实现了 Copy
    let s = String::from("hello"); // String,未实现 Copy

    do_somthing(|| {
        println!("x:{},s:{}", x, s);
    });

    println!("x:{}", x); // 42
    println!("s:{}", s); // hello
}
rust
fn do_somthing<T>(mut f: T)
where
    T: FnMut(),
{
    f();
}

fn main() {
    let x = 42; // i32,实现了 Copy
    let s = String::from("hello"); // String,未实现 Copy

    do_somthing(move || {
        println!("x:{},s:{}", x, s);
    });

    println!("x:{}", x); // 42
    // println!("s:{}", s); // 错误,s所有权已移动
}

MIT Licensed