返回介绍

5.2 闭包

发布于 2024-10-13 11:25:29 字数 5492 浏览 0 评论 0 收藏 0

闭包(closure, lambda)是 匿名函数 和其持有的 环境变量 组合。

  • 参数和返回值类型可推导。
  • 必须指定参数名。
  • 参数以双竖线包含。
  • 函数体大括号可选。
fn main() {
  // let f = |x| x + 1;
  // let f = |x: i32| x + 1;
  let f = |x: i32| -> i32 { x + 1 };

  assert_eq!(f(2), 3);
}
fn main() {
  let f = |x| x;

  f(2);     // 通过此次调用推断类型。
  f("abc");
    ^^^^^ expected integer, found `&str`
}

可直接执行。

fn main() {
  let x = (|n| {      // 注意添加括号。
    println!("hello");
    n + 1
  })(100);

  assert_eq!(x, 101);
}

实现

环境变量是匿名函数定义所在上下文,非调用处。

struct closure-<id> {
  &var1, 
  ...
}

每个闭包变量都有唯一且匿名的类型,这导致无法在代码中声明。从实现上看,闭包结构仅保存环境变量引用,不包括函数。得益于编译器强大分析能力,直接对匿名函数静态调用。

fn main() {
  let v1 = 100;
  let v2 = 100;

  let a = |x: i32| x;       // struct main::closure-0
  let b = |x: i32| x + v1;    // struct main::closure-1
  let c = |x: i32| x + v1 + v2;   // struct main::closure-2

  assert_eq!(size_of(&a), 0);
  assert_eq!(size_of(&b), 8);   // v1_ptr
  assert_eq!(size_of(&c), 16);  // v1_ptr + v2_ptr
}

反汇编查看具体实现和调用方式。

该实现不代表所有情形,作为返回值的闭包可能会复制栈帧内分配的局部环境变量。

fn main() {
  let a: i64 = 0x11;
  let b: i64 = 0x22;

  let f = |x: i64| x + a + b;
  f(0x33);
}
(gdb) b 6
(gdb) r

(gdb) ptype f     # 查看闭包类型。
type = struct demo::main::closure-0 (
  *mut i64,
  *mut i64,
)

(gdb) p f       # 查看闭包内容。
$1 = demo::main::closure-0 (
  0x7fffffffe1a0,
  0x7fffffffe1a8
)

(gdb) p &a      # 环境变量地址。
$2 = (*mut i64) 0x7fffffffe1a0

(gdb) p &b
$3 = (*mut i64) 0x7fffffffe1a8
(gdb) disass
Dump of assembler code for function demo::main:
   0x0000555555559160 <+0>:    sub  rsp,0x28
   
   ; 环境变量 a、b。
   0x0000555555559164 <+4>:    mov  QWORD PTR [rsp],0x11
   0x000055555555916c <+12>:  mov  QWORD PTR [rsp+0x8],0x22
   
   ; closure { &a, &b }
   0x0000555555559175 <+21>:  mov  rax,rsp
   0x0000555555559178 <+24>:  mov  QWORD PTR [rsp+0x10],rax
   0x000055555555917d <+29>:  lea  rax,[rsp+0x8]
   0x0000555555559182 <+34>:  mov  QWORD PTR [rsp+0x18],rax
   
   ; call (rdi: &closure, rsi: 0x33)
=> 0x0000555555559187 <+39>:  mov  QWORD PTR [rsp+0x20],0x33
   0x0000555555559190 <+48>:  mov  rsi,QWORD PTR [rsp+0x20]
   0x0000555555559195 <+53>:  lea  rdi,[rsp+0x10]
   0x000055555555919a <+58>:  call   0x555555559270 <main::{{closure}}>
   
   0x000055555555919f <+63>:  add  rsp,0x28
   0x00005555555591a3 <+67>:  ret  
End of assembler dump.


(gdb) disass 0x555555559270
Dump of assembler code for function demo::main::{{closure}}:
   0x0000555555559270 <+0>:    sub  rsp,0x28
   
   ; 从 rdi、rsi 获取闭包内容和参数。
   0x0000555555559274 <+4>:    mov  QWORD PTR [rsp+0x18],rdi
   0x0000555555559279 <+9>:    mov  QWORD PTR [rsp+0x20],rsi

捕获

编译器尽可能按 &T -> &mut T -> move 顺序捕获环境变量,最大限度减少影响。

fn main() {
  let s = "abc".to_string();

  // 所有权转移。
  // fn test(s: String) { println!("{:?}", s); }
  // test(s);

  // 最小捕获,&T。
  let c = || println!("{:?}", s);
  c();

  assert_eq!(s.as_str(), "abc");
}

如要修改环境变量,则要求闭包也是可变的。

fn main() {
  let mut s = "abc".to_string();

  // &mut T,闭包变量必须是 mut。
  let mut c = || s.push('d');
  c();

  assert_eq!(s.as_str(), "abcd");
}

当需要所有权时,转移(move)所有权给闭包对象(非匿名函数)。

fn main() {
  let s = "abc".to_string();

  // 匿名函数内需要所有权转移。
  // 闭包创建时,所有权属于闭包对象,而非匿名函数。
  
  let c = || { let _s = s; };
      --      - variable moved due to use in closure
      |
      value moved into closure here

  println!("{:?}", s);
           ^ value borrowed here after move
  
  c();
}

所有权转移,须注意多次调用引发的释放(drop)问题。

fn main() {
  let s = "abc".to_string();

  // 第一次执行,闭包将所有权转移给 _s,本次调用结束时释放。
  let c = || { let _s = s; };
  
  c();
  // --- `c` moved due to this call

  // c();
  // ^ value used here after move
}
(gdb) disass
Dump of assembler code for function demo::main:
   0x0000555555559208 <+24>:  call   0x55555555aac0 <alloc...to_string>
   ...
   0x0000555555559249 <+89>:  lea  rdi,[rsp+0x38]
   0x000055555555924e <+94>:  call   0x5555555599b0 <main::{{closure}}>
   ...
End of assembler dump.


(gdb) disass 0x5555555599b0
Dump of assembler code for function demo::main::{{closure}}:
   0x00005555555599cd <+29>:  mov  rdi,rsp
   0x00005555555599d0 <+32>:  call   0x55555555ac80 <drop_in_place<String>>
   
   0x00005555555599d5 <+37>:  add  rsp,0x18
   0x00005555555599d9 <+41>:  ret  
End of assembler dump.

还可以用 move 关键字强制转移所有权,无论闭包是否需要。

fn main() {
  let s = "abc".to_string();

  // 强制转移所有权。
  let c = move || println!("{:?}", s);

  // 注意!所有权被闭包持有,而非匿名函数。
  // 匿名函数内部仅引用(&T),故不会释放。
  c();
  c();
}
(gdb) b 9
(gdb) r

(gdb) disass/s
1  fn main() {
2    let s = "abc".to_string();
5    let c = move || println!("{:?}", s);
6  
7    // 注意!所有权被闭包持有,而非匿名函数。
8    // 匿名函数内部仅引用(&T),故不会释放。
9    c();
=> 0x0000555555568a73 <+51>:  call 0x555555568af0 <closure#0>

10    c();
   0x0000555555568a9b <+91>:  call 0x555555568af0 <closure#0>
11  }
   0x0000555555568aa7 <+103>:  call 0x555555569870 <drop_in_place<closure_env#0>>
   0x0000555555568ab0 <+112>:  ret  

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文