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

Rust function pointer seems to be treated as stateful by borrow checker

提问人:google2 提问时间:4/2/2023 最后编辑:google2 更新时间:4/2/2023 访问量:70

问:

以下示例代码不编译:

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);
}

编译器返回以下错误:

   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

如果我将赋值移动到循环中,代码将编译:ffor

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);
}

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

rustc 版本:稳定版 v1.68.2

Rust Closures Lifetime 借用检查器 mutable-reference

评论

0赞 Chayim Friedman 4/2/2023
“变量也是不可变的,所以即使它的类型是有状态的,它也不能存储闭包”——这是不正确的:它可以使用内部可变性。但它仍然无法存储闭包。f
0赞 cafce25 4/2/2023
“变量 if 的类型,这意味着它是无状态的” Rust 不是纯粹的,总是可以有全局状态,所以这是错误的。ffn
1赞 google2 4/2/2023
@cafce25你是对的,但这种说法不是 100% 正确的,对不起。然而,这不是问题所在。在后一个示例中也可以修改全局状态,但该示例会编译。fn

答:

5赞 cdhowie 4/2/2023 #1

我相信这个问题是由于参数中存在的隐含寿命造成的。就好像你写了这个:f

fn invoke<'a>(i: i32, mut f: impl FnMut(i32) + 'a) {
    f(i)
}

将函数存储在循环外部时,编译器必须选择适用于整个函数中所有调用的单个生存期。

(另一种看待它的方式是,这个参数的具体类型将是一个匿名结构,如 实现 .重要的是,无论你如何看待它,推导出来满足的具体类型都将包含一个生命周期,因为在闭包中通过引用捕获。struct AnonymousType<'a>FnMut(i32)impl FnMut(i32)sum

生存期不能限制为循环的单个迭代,因为该生存期不适用于所有其他迭代。因此,编译器必须选择更长的生存期,但这会导致独占借用重叠的问题,这就是您正在观察到的问题。

将行移动到循环中允许编译器为每次迭代选择不同的生存期,因为每次迭代也会存在不同的生存期。let ff

特别要注意的是,Rust 中的函数指针和闭包目前不允许是泛型的,因此不能包含在隐藏生命周期内泛型的函数指针。该功能可以在以后添加,如果是这样,那么这将允许编译此代码。f