Rust 函数指针似乎被借用检查器视为有状态

goo*_*le2 4 closures lifetime rust borrow-checker mutable-reference

以下示例代码无法编译:

fn invoke(i: i32, mut f: impl FnMut(i32)) {
    f(i)
}

fn main() {
    let f: fn(i32, _) = invoke;

    let mut sum: i32 = 0;
    for i in 0..10 {
        _ = f(i, |x| sum += x);
    }

    println!("{:?}", sum);
}
Run Code Online (Sandbox Code Playgroud)

编译器返回以下错误:

   Compiling playground v0.0.1 (/playground)
error[E0499]: cannot borrow `sum` as mutable more than once at a time
  --> src/main.rs:10:18
   |
10 |         _ = f(i, |x| sum += x);
   |             -    ^^^ --- borrows occur due to use of `sum` in closure
   |             |    |
   |             |    `sum` was mutably borrowed here in the previous iteration of the loop
   |             first borrow used here, in later iteration of loop

For more information about this error, try `rustc --explain E0499`.
error: could not compile `playground` due to previous error
Run Code Online (Sandbox Code Playgroud)

如果我将f赋值移至for循环,代码将编译:

fn invoke(i: i32, mut f: impl FnMut(i32)) {
    f(i)
}

fn main() {
    let mut sum: i32 = 0;
    for i in 0..10 {
        let f: fn(i32, _) = invoke;
        _ = f(i, |x| sum += x);
    }

    println!("{:?}", sum);
}
Run Code Online (Sandbox Code Playgroud)

我很困惑为什么第一个代码不能编译。该变量f的类型为fn,这意味着它是无状态的。变量f也是不可变的,所以即使它的类型是有状态的,它也不能存储闭包。因此,编译器应该能够得出结论,在循环的下一次迭代之前将删除闭包for。然而编译器的行为就好像f是可变的并且它可以存储闭包。您能否解释一下为什么编译器会这样做。

rustc 版本:稳定版 v1.68.2

cdh*_*wie 5

I believe the issue is due to the implied lifetime present in the f parameter. It's as though you'd written this:

fn invoke<'a>(i: i32, mut f: impl FnMut(i32) + 'a) {
    f(i)
}
Run Code Online (Sandbox Code Playgroud)

When you store the function outside of the loop, the compiler must choose a single lifetime that applies for all invocations in the whole function.

(Another way of looking at it is that the concrete type of this parameter will be an anonymous struct, like struct AnonymousType<'a>, implementing FnMut(i32). The important thing is that, no matter how you look at it, the concrete type deduced to satisfy impl FnMut(i32) will contain a lifetime because sum is captured by reference in the closure.)

The lifetime cannot be restricted to a single iteration of the loop, because that lifetime would not apply to all other iterations. Therefore, the compiler must choose a longer lifetime -- but then this causes problems with the exclusive borrows overlapping, which is what you are observing.

Moving the let f line into the loop allows the compiler to select a different lifetime for each iteration, because a different f comes into existence each iteration as well.

Note in particular that function pointers and closures in Rust are not currently permitted to be generic, so f can't contain a function pointer that's generic over the hidden lifetime. That feature could be added later, and if it is then that would allow this code to compile.