Rust Closure

Table of Contents

1. Rust Closure

1.1. function

function 是 first class object, 当函数作为参数, 返回值, 变量等情况下, 实际上使用的是函数指针, 其类型为 fn(args)->(return).

fn test() {}


fn call(f: fn()) {
    println!("call:");
    println!("f is at {:?}", &f as *const _);
    println!("which point to {:?}", f as *const ());
    unsafe {
        println!("also 0x{:x}", *(&f as *const _ as *const i64));
    }

    f();
}

fn main() {
    println!("main:");
    println!("test is at {:?}", test as *const ());
    call(test);
}
main:
test is at 0x55e7143f37f0
call:
f is at 0x7ffc23659470
which point to 0x55e7143f37f0
also 0x55e7143f37f0

1.2. closure

1.2.1. memory layout

closure 分为两种情况:

  1. 当 closure 不需要 capture 时, 和普通函数相同.
  2. 当 closure 需要 capture 时, 需要占用额外的空间保存 capture 的数据
fn main() {
    println!("\nmove ---------------");
    let x = 0xffi64;
    println!("x is at {:?}", &x as *const _);
    // move
    let foo_move = move || {
        println!("{:?}", x);
    };
    println!("size of foo: {}", std::mem::size_of_val(&foo_move));
    println!("foo_move is at {:?}", &foo_move as *const _);
    unsafe {
        println!(
            "foo_move val is 0x{:x}",
            *(&foo_move as *const _ as *const i64)
        );
    }
    // borrow
    println!("\nborrow ---------------");
    let foo_borrow = || {
        println!("{:?}", x);
    };
    println!("size of foo_borrow: {}", std::mem::size_of_val(&foo_borrow));
    println!("foo_borrow is at {:?}", &foo_borrow as *const _);
    unsafe {
        println!(
            "foo_borrow val is {:x}, whic is &x",
            *(&foo_borrow as *const _ as *const i64)
        );
    }
}
move ---------------
x is at 0x7ffc6e4c7d08
size of foo: 8
foo_move is at 0x7ffc6e4c7d68
foo_move val is 0xff

borrow ---------------
size of foo_borrow: 8
foo_borrow is at 0x7ffc6e4c7ea0
foo_borrow val is 7ffc6e4c7d08, whic is &x

closure 对应的 code 并不需要包含在 closure 中, 因为每个 closure 都是不同的类型, 编译器知道每个 closure 对应的 code 在哪里.

1.2.2. closure 的类型

#![feature(core_intrinsics)]
fn type_of<T>(_: &T) -> &str {
    unsafe { std::intrinsics::type_name::<T>() }
}

fn test() -> () {}

fn main() {
    let foo = || {};
    let bar = || {};
    println!("type of foo: {:?}", type_of(&foo));
    println!("type of bar: {:?}", type_of(&bar));
    println!("type of test: {:?}", type_of(&test));
}
type of foo: "d41d8cd98f::main::{{closure}}"
type of bar: "d41d8cd98f::main::{{closure}}"
type of test: "d41d8cd98f::test"

1.3. Fn trait

closure 无法做为函数的参数和返回值等, 因为:

  1. closure 都是动态生成的类型, 无法写在函数的 signature 里, 只能依靠编译器的类型推导 (c++ 的 auto 关键字解决类似的问题)
  2. closure 的大小是不确定的, 同样 signature 的 closure, 会因为 capture 的不同导致不同的大小, 所以无法通过类似 `fn()->()` 来指示 closure 的类型
fn call(f: fn()) {
    f();
}

fn test() {}

fn main() {
    // OK, 没有 capture 的 closure 可以 coerce 为 fn()
    call(|| {
        println!("hello");
    });
    // OK
    call(test);

    // WRONG, 需要 capture 的 closure 无法 coerce 为 fn()
    // error[E0308]: mismatched types
    // --> src/main.rs:19:10
    // |
    // 19 |       call(|| {
    // |  __________^
    // 20 | |         println!("{:?}", x);
    // 21 | |     })
    // | |_____^ expected fn pointer, found closure
    // |
    // = note: expected type `fn()`
    let x = 1;
    call(|| {
        println!("{:?}", x);
    })
}

解决方法是使用 Fn trait, 因为 closure, fn 都会 impl Fn trait

fn call(f: &dyn Fn()) {
    f();
}

fn test() {
    println!("normal fn");
}

fn main() {
    call(&test);

    call(&|| {
        println!("uncaptured closure");
    });

    let x = 1;
    call(&|| {
        println!("captured closure: {:?}", x);
    })
}
normal fn
uncaptured closure
captured closure: 1

1.4. move

fn call(f: impl Fn()) {
    f()
}

fn main() {
    let f = {
        let x = "hello".to_owned();
        let ret = move || {
            println!("{:?}", x);
        };
        // error[E0382]: borrow of moved value: `x`
        // println!("{:?}", x);
        ret
    };

    call(f)
}
hello

默认情况下会使用 borrow 的方式 capture 环境变量, 若像上面的例子一样无法 borrow, 则需要显式的指定 move.

另外, move 是定义 closure 时 (而非执行时) 把环境变量一次性的 move 到 closure 内的. 后面执行 closure 时不会再发生 move

另外, 若 closure 代码会 consume 环境变量, 编译器会自动的 move

fn call(f: impl FnOnce()) {
    f()
}

fn main() {
    let x = "hello".to_owned();
    let f = || {
        let y = x;
        println!("{:?}", y);
    };
    println!("{:?}", x);
    call(f)
}

// error[E0382]: borrow of moved value: `x`
// --> quickrun:11:22
// |
// 6  |     let x = "hello".to_owned();
// |         - move occurs because `x` has type `std::string::String`, which does not implement the `Copy` trait
// 7  |     let f = || {
// |             -- value moved into closure here
// 8  |         let y = x;
// |                 - variable moved due to use in closure
// ...
// 11 |     println!("{:?}", x);
// |                      ^ value borrowed here after move
//
// error: aborting due to previous error
//
// For more information about this error, try `rustc --explain E0382`.

1.5. Fn, FnMut, FnOnce

所有的 function 都 impl Fn.

但对于 closure 来说, 针对 capture 的不同, 编译器会自动 impl Fn, FnMut 或 FnOnce 中的一个.

1.5.1. FnOnce

move || {
    let z = x;
};

这个 closure impl FnOnce, 因为 closure 每次执行时都会 move capture 到的变量, 但这个 move 只能进行一次. 所以 FnOnce 表示 closure 只能执行一次.

只有 move closure 才可能是 FnOnce

1.5.2. FnMut

若 closure 会修改 capture variable, 则它会 impl FnMut

let x = 1;
|| {
    x += 1;
}

上面的 closure 实际会持有 `&mut x`

1.5.3. Fn

Fn 表示 closure 不会修改 capture variable 且可以执行多次.

1.5.4. trait bound

Fn 等作为 trait bound 时有不同的意义:

  1. Fn trait bound 表示需要能执行多次且不修改 capture variable
  2. FnMut trait bound 表示需要能执行多次且`可以`修改 (而不是一定要修改)
  3. FnOnce trait bound 表示不需要能执行多次

所以, Fn -> FnMut -> FnOnce 的要求是依次降低的:

  1. Fn trait bound 无法接受 FnMut 和 FnOnce, 因为它要求能执行多次且不修改
  2. FnMut trait bound 无法接受 FnOnce
  3. FnOnce trait bound 可以接受任何 Fn, FnMut 或 FnOnce

Author: [email protected]
Date: 2019-02-27 Wed 00:00
Last updated: 2024-09-26 Thu 22:31

知识共享许可协议