关于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` </pre>

上面的报错,告诉我们类型是 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>(_: &T) -> usize {
    std::mem::size_of::<T>()
}

定义了三个闭包,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` </pre>

函数只能执行一次,因为当第一次执行时,_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中闭包

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容