关于rust中的闭包(一)

闭包

在计算机中,闭包 Closure, 又称词法闭包 Lexical Closure 或函数闭包 function closures, 是引用了自由变量的函数

被引用的自由变量将和函数一同存在,即使已经离开了创造它的环境也不例外。换句话说,闭包是由函数和与其相关的引用环境组合而成的实体

闭包

结合实例介绍“闭包”

let f = |x: i32| -> i32 { x + 1 };

说明: 闭包 || 代表传入参数,-> 后面代表返加值,{} 大括号里代表函数体

let f = |x: i32| x + 1;

同时如果函数体只有一行,可以省略 {}

let f = |x| x+1;

甚至可以省去 i32 类型, 因为 rust 很智能,默认 x+1 会推导出闭包 f 返回类型是 i32

let f = |x| x;

如果这种情况,就需要根据第一次使用时推导出类型

fn main() {
    let f = |x| x;
    f(1);
    f('a');
}
$ cargo run
   省略部分内容
error[E0308]: mismatched types
 --> src/main.rs:4:7
  |
4 |     f('a');
  |       ^^^ expected integer, found `char`

error: aborting due to previous error` 

上面的报错,告诉我们类型是 i32, 而后来传入的是 char

闭包底层实现

fn main() {
    let v1 = 100;
    let v2 = 100;
    let fa = |x: i32| x;
    let fb = |x: i32| x + v1;
    let fc = |x: i32| x + v1 + v2;

    assert_eq!(size_of(&fa), 0);
    assert_eq!(size_of(&fb), 8);
    assert_eq!(size_of(&fc), 16);
}

fn size_of(_: &T) -> usize {
    std::mem::size_of::()
}

定义了三个闭包,a 普通的匿名函数,b 引用外部变量 v1, c 引用两个外部变量 v1, v2

(lldb) var
# 输出相关变量
(i32) v1 = 100
(i32) v2 = 100
# 闭包
(common_trait_demos::test_closure_code_demo::{closure_env#0}) fa = {}
(common_trait_demos::test_closure_code_demo::{closure_env#1}) fb = {
  _ref__v1 = 0x000070000cd99788
}
(common_trait_demos::test_closure_code_demo::{closure_env#2}) fc = {
  _ref__v1 = 0x000070000cd99788
  _ref__v2 = 0x000070000cd9978c
}
# 分别查看不同闭包的地址
(lldb) print &fa
(*mut common_trait_demos::test_closure_code_demo::{closure_env#0}) &fa = 0x000070000cd99790
(lldb) print &fb
(*mut common_trait_demos::test_closure_code_demo::{closure_env#1}) &fb = 0x000070000cd99798
(lldb) print &fc
(*mut common_trait_demos::test_closure_code_demo::{closure_env#2}) &fc = 0x000070000cd997a0
# 看一看v1和v2变量在闭包中的引用
(lldb) print &v1
(*mut i32) &v1 = 0x000070000cd99788
(lldb) print &v2
(*mut i32) &v2 = 0x000070000cd9978c

通过返汇编,我们可以看到,rust 里匿名函数其实和闭包是一样的结构,底层实现一样的

闭包引用变量情况

闭包 fa 是空结构体,所以大小 0 字节,而 fb 拥有一个指针字段,64位平台上当然是 8 字节,fc 就是 16 字节长度。打印地址,可以看到捕获的就是对应 v1, v2

同时要注意到,这个例子里,闭包在二进制包 text 代码段中用 hello_cargo::main::closure-N 结构体来表示,编号依次递增的,同时在该例子中结构体捕获的变量,其实是引用形式

闭包与所有权

fn main(){
    let mut a = 1;
    let mut inc = || {a+=1;a};
    inc();
    inc();
    println!("now a is {}", a);
}

闭包 inc 捕获自由变量 a, 然后自增

# cargo run  省略部分内容
Finished dev [unoptimized + debuginfo] target(s) in 2.35s
     Running `target/debug/hello_cargo`
now a is 3

可以看到,当 inc 执行两次后,a 的结果是 3, 由上面可以知道此时 inc 以引用的方式捕获变量 a

引用闭包汇编

fn main(){
    let s = String::from("test");
    let f = || {let _s = s;println!("{}", _s)};
    f(); // s所有权发生转移
    //如下代码会在编译期间抛出异常,s所有权在上面已发生转译,再次执行,s处于未被初始化状态
    f();
}

这是转移所有权的例子,堆上的字符串变量 s, 所有权转给了闭包中的临时变量 _s

 use of moved value: `f`
 --> src/main.rs:5:5
  |
4 |     f();
  |     --- `f` moved due to this call
5 |     f();
  |     ^ value used here after move` 

函数只能执行一次,因为当第一次执行时,_s 随后析构释放了内存,所以编译器报错; 尝试将第二个f()注释掉,汇编情况如下:

注释掉第二个f()汇编情况

fn main(){
    let mut s = String::from("test");
    let mut f = || {s.push('a');println!("{}", s)};
    f();
    f();
}

例子中闭包 f 修改字符串 s, 并打印

$ cargo run
   省略部分代码
    Finished dev [unoptimized + debuginfo] target(s) in 0.55s
     Running `target/debug/hello_cargo`
testa
testaa
汇编情况
fn main(){
    let s = String::from("test");
    let f = move || {println!("{}", s)};
    f();
    f();
}

如果想把变量转移给闭包,就需要显示使用 move 关键字,此时字符串 test 所有权转给了闭包 f, 当然可以多次执行,直到 f 离开作用域后一起析构;

move汇编情况

可以得出结论:闭包捕获变量优先只读引用,然后可变引用,最后 move 所有权

唯一不可变借入捕获

通过唯一不可变借用捕获: 当捕获对可变变量的不可变引用并使用它来修改引用的值时,就会出现这种情况。例如,让我们考虑以下示例:

fn main() {
    let mut s = String::from("hello");
    let x = &mut s;

    let mut mut_closure = || {
        (*x).push_str(" world");
    };
}

在这里,闭包捕获了不可变变量x,即对可变变量的引用String。闭包不会修改引用值,因此闭包应该x通过不可变借用来捕获。因此,我们应该能够同时对该变量进行其他引用。但是,这是不正确的,例如,在以下编译错误的示例中:

fn main() {
    let mut s = String::from("hello");
    let x = &mut s;

    let mut mut_closure = || {
        (*x).push_str(" world");
    };

    // cannot borrow `x` as immutable because previous closure requires unique access
    println!("{:?}", x); // error happens here
    mut_closure();
}

原因是在这种情况下,变量被唯一的不可变借用捕获。

捕获变量模式

关于Rust 2021中闭包的更新

对于闭包 || a.x + 1, 2018 的实现是捕获整个结构体 a, 而在Rust2021中只捕获所需要用的 x

这个特性会导致一些对像在不同时间点被释放 dropped, 或是影响了闭包是否实现 Send 或 Clone trait, 所以** cargo 会插入语句 let _ = &a 引用完整结构体来修复这个问题。

引用

rust中闭包

你可能感兴趣的:(关于rust中的闭包(一))