为什么在 Rust 中,闭包中调用函数和直接调用函数的生命周期不同?

11
在下面的代码示例中:
fn default_values() -> &'static [u32] {
    static VALUES: [u32; 3] = [1, 2, 3];
    &VALUES
}

fn main() {
    let values: [u32; 3] = [4, 5, 6];
    let optional_values: Option<&[u32]> = Some(&values);

    // this compiles and runs fine 
    let _v = optional_values.unwrap_or_else(|| default_values());

    // this fails to compile
    let _v = optional_values.unwrap_or_else(default_values);
}

最后一个语句无法编译,报错为:

error[E0597]: `values` does not live long enough
  --> src/main.rs:8:49
   |
8  |     let optional_values: Option<&[u32]> = Some(&values);
   |                                                 ^^^^^^ borrowed value does not live long enough
...
12 | }
   | - borrowed value only lives until here
   |
   = note: borrowed value must be valid for the static lifetime...

我想知道:

  1. 是什么导致了最后两个语句之间的行为差异
  2. 第一个 unwrap_or_else(|| default_values()) 是否是处理此问题的正确方式,还是有更好的方式

2
我不明白为什么,但是我把它改成 default_values as fn() -> &'static _ 然后它就可以工作了。 - DanSnow
2
Nightly + feature(nll) 提供了额外的信息:*'borrowed value does not live long enough, cast requires that values is borrowed for 'static'* - hellow
看起来编译器是基于函数的返回类型而不是可选值的类型推断 _v,实际上只需要 as fn() -> _ 就足够了。这只是一个“你需要帮助编译器”的情况。 - Stargateur
2个回答

5
这是因为default_values实现了Fn() -> &'static [u32],但没有实现for<'a> Fn() -> &'a [u32]。特质是不变的,因此您无法将“实现Fn() -> &'static [u32]的内容”强制转换为“实现Fn() -> &'a [u32]的内容”(对于一些小于'static'a),尽管从逻辑上讲,default_values可以满足两者。

在闭包中调用default_values()时,它返回一个&'static [u32],但可以立即强制转换为&'a u32,使闭包本身能够实现Fn() -> &'a [u32](其中的&'a由编译器确定)。

至于为什么添加as fn() -> &'static [u32]可以解决问题,我认为编译器可以识别函数指针类型fn() -> &'static [u32]能够实现Fn() -> &'a [u32],其中'a是任意的。我不确定为什么它不会对普通函数和闭包也这样做;也许未来的编译器版本可以足够聪明以允许原始代码。

另一种解决方案是使default_values的类型能够实现您需要的Fn特质:

fn default_values<'a>() -> &'a [u32] {
    static VALUES: [u32; 3] = [1, 2, 3];
    &VALUES
}

在这里,函数签名的意思是“这是一个可以返回任何生命期引用的函数”,而不是说“这是一个返回'static引用的函数”。我们知道“任何生命期引用”必须是一个'static引用,但编译器将这两个签名视为不同,因为这一个具有额外的自由度。这个变化足以使你的原始示例成功编译。


1
这仍然感觉像一个 bug。可能这是 HRTB 的未经打磨的边缘。 - Peter Hall
1
我同意,这确实感觉像一个 bug。 - trent

2

闭包和直接函数调用之间没有区别:只是类型推断的问题。

编译的闭包:

let _v = optional_values.unwrap_or_else(|| default_values());
let _v = optional_values.unwrap_or_else(|| -> & [u32] {default_values()});

无法编译的闭包:

let _v = unwrap_or_else(optional_values, || -> &'static [u32] {default_values()});

编译函数:

let _v = unwrap_or_else(optional_values, default_values as fn() -> &'static _);

无法编译的函数:

let _v = unwrap_or_else(optional_values, default_values);

一点解释

考虑下面等效的代码:

fn default_values() -> &'static [u32] {
    static VALUES: [u32; 3] = [1, 2, 3];
    &VALUES
}

fn unwrap_or_else<T, F>(slf: Option<T>, f: F) -> T where
    F: FnOnce() -> T, {
        match slf {
            Some(t) => t,
            None => f()
        }
    }

以下代码片段:
fn main() {
    let values: [u32; 3] = [4, 5, 6];
    let optional_values: Option<&[u32]> = Some(&values);

    let _v = unwrap_or_else(optional_values, || -> &'static [u32] {default_values});

    // the above throws the same error of:
    //let _v = unwrap_or_else(optional_values, default_values);
}

失败:

error[E0597]: `values` does not live long enough
  --> src/main.rs:18:48
   |
18 |     let optional_values: Option<&[u32]> = Some(&values);
   |                                                ^^^^^^^
   |                                                |
   |                                                borrowed value does not live long enough
   |                                                cast requires that `values` is borrowed for `'static`
...
27 | }
   | - `values` dropped here while still borrowed

从单态化的角度来看:假设编译器推断T解析为具体类型&'static [u32],并且假设生成的代码类似于:

fn unwrap_or_else_u32_sl_fn_u32_sl(slf: Option<&'static [u32]>,
                                   f: fn() -> &'static [u32]) -> &'static [u32] {
    ...
}

以上的单态化解释了错误:

slf 的值是 optional_values:一个 Option<&'a [u32]>,它不够长久,显然不能转换,因为它不满足 'static 生命周期要求。

如果你写成:

let _v = unwrap_or_else(optional_values, || default_values());

// the same, expliciting the return type:
let _v = unwrap_or_else(optional_values, || -> & [u32] {default_values()});

编译成功:现在返回类型的生命周期与optional_values的生命周期兼容。

最后,我无法解释为什么,但证据表明,as fn() -> &'static _ 转换可以帮助编译器确保解除绑定于 optional_valuesdefault_values 的生命周期是安全的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接