参数类型 `T` 可能无法存活足够长的时间。

5

我正在尝试用Rust编写一个小程序,但是它无法正常工作。

我已经在一个更小的脚本中复制了这个错误:

fn main() {
    let name = String::from("World");
    let test = simple(name);
    println!("Hello {}!", test())
}

fn simple<T>(a: T) -> Box<Fn() -> T> {
    Box::new(move || -> T {
        a
    })
}

当我编译它时,会出现以下错误:
error[E0310]: the parameter type `T` may not live long enough
  --> test.rs:8:9
   |
7  |       fn simple<T>(a: T) -> Box<Fn() -> T> {
   |                 - help: consider adding an explicit lifetime bound `T: 'static`...
8  | /         Box::new(move || -> T {
9  | |             a
10 | |         })
   | |__________^
   |
note: ...so that the type `[closure@test.rs:8:18: 10:10 a:T]` will meet its required lifetime bounds
  --> test.rs:8:9
   |
8  | /         Box::new(move || -> T {
9  | |             a
10 | |         })
   | |__________^

我尝试添加一个显式的生命周期限制T: 'static,如错误建议所示,但是我得到了一个新的错误:

error[E0507]: cannot move out of captured outer variable in an `Fn` closure
 --> test.rs:9:13
  |
7 |     fn simple<T: 'static>(a: T) -> Box<Fn() -> T> {
  |                           - captured outer variable
8 |         Box::new(move || -> T {
9 |             a
  |             ^ cannot move out of captured outer variable in an `Fn` closure

你尝试过按照编译器的建议去做吗?(显式生命周期限制 T: 'static - MB-F
3
请注意,您无法将变量(a)移出Fn闭包 - 在第一次调用之后,它不再存在。 - MB-F
3
你的目标是什么? - ljedrz
当我使用 'static 生命周期时,会出现以下错误:error[E0507]: cannot move out of captured outer variable in an `Fn` closure - b1zzu
有没有办法将变量 a 移出闭包? - b1zzu
5
有时候人们会提出很宽泛的问题,这会导致会有各种不同的解决方案。我认为现在是您告诉我们更多关于您实际目标的时候了 :) - MB-F
2个回答

9
这里发生了几件事情,它们都与移动语义和闭包的微妙笨拙有关。首先,`simple` 函数确实需要为其 `T` 参数指定生命周期。从函数的角度来看,`T` 可以是任何类型,这意味着它可能是一个引用,因此它需要有生命周期。生命周期省略不适用于这种情况,因此您需要显式地编写它。编译器建议使用 `'static`,对于 hello world 来说这是可以的。如果您有更复杂的生命周期,您需要使用生命周期参数;请参见下面我的示例。您的闭包不能是 `Fn`,因为您无法调用它超过一次。正如您得到的新错误所说,当调用它时,闭包会将其捕获的值 (`a`) 移出闭包。这与说它是一个使用 `self` 而不是 `&self` 的方法相同。如果函数调用是一个普通方法而不是具有特殊语法的方法,那么它将类似于这样:
trait FnOnce {
    type Output
    fn call(self) -> Output
}

trait Fn : FnOnce {
    fn call(&self) -> Output
}

// generated type
struct MyClosure<T> {
    a: T
}

impl<T> FnOnce for MyClosure<T> {
    fn call(self) -> T { self.a }
}

简单来说,一个消耗其捕获值的闭包不实现Fn,只实现FnOnce。调用它会消耗闭包。还有一个FnMut,但这与本文无关。

这还有另一个含义,涉及到移动值时的消耗。您可能已经注意到,在任何特质对象(Box<T>其中T是一个特质)上不可以调用带有self参数的方法。为了移动对象,正在移动它的代码需要知道被移动对象的大小。这在特质对象中没有发生,因为它们是无尺寸的。这也适用于Box<FnOnce>。由于调用闭包会将其移动(因为调用是一个self方法),因此无法调用该闭包。

那么如何解决这个问题呢?这使得Box<FnOnce>有点无用。有两个选择。

如果您可以使用不稳定的 Rust,您可以使用 FnBox 类型:它是 FnOnce 的替代品,可在 Box 中使用。它被隐藏在一个功能门后,因为文档警告你:“请注意,如果 Box<FnOnce()> 闭包直接可用,FnBox 可能会在将来被弃用。” 这里有一个使用此解决方案并添加生命周期参数以修复原始问题的 playground。 另一个可能更广泛适用的工程解决方案是避免移出闭包。
  • 如果您总是将静态对象放入闭包中,可以返回一个引用&'static T。这样,您可以多次调用闭包,并且所有调用者都会获得对同一对象的引用。

  • 如果对象不是静态的,则可以返回Rc<T>。在这种情况下,所有调用者仍然获得对同一对象的引用,并且该对象的生命周期是动态管理的,因此它将保持活动状态直到不再需要。这里有另一个实现此选项的playground。

  • 您可以让闭包将其参数复制给每个调用者。这样,它可以被多次调用,每个调用者都会获得自己的副本。不需要进行进一步的生命周期管理。如果以这种方式实现,仍然可以将参数设置为Rc<T>而不是T,以与上面的选项使用函数。


12
T可以是任何类型,这意味着它可能是引用” - 这解答了我一直很好奇但从未问过的问题。顺便问一下,有没有办法指定某些T不能是引用? - MutantOctopus
有一个替代 FnBox 的方法可以在稳定版上运行,但会产生一些运行时成本(我认为 FnBox 不会)。Playground 链接 - MB-F

0

simple 函数返回一个闭包,该闭包对返回类型 T 是通用的。

这意味着返回的类型可以是任何类型,例如引用或包含引用的类型,因此编译器建议在类型上指定 'static

fn simple<T: 'static>(a: T) -> Box<Fn() -> T> {
    Box::new(move || -> T { a })
}

但现在你有问题:

error[E0507]: cannot move out of captured outer variable in an `Fn` closure
 --> src/main.rs:2:29
  |
1 | fn simple<T: 'static>(a: T) -> Box<Fn() -> T> {
  |                       - captured outer variable
2 |     Box::new(move || -> T { a })
  |                             ^ cannot move out of captured outer variable in an `Fn` closure

error[E0597]: `name` does not live long enough
 --> src/main.rs:7:24
  |
7 |     let test = simple(&name);
  |                        ^^^^ borrowed value does not live long enough
8 |     println!("Hello {}!", test())
9 | }
  | - borrowed value only lives until here
  |
  = note: borrowed value must be valid for the static lifetime...

由于捕获的变量name是由外部“main”上下文拥有的,因此它不能被其他人“窃取”。

接下来要尝试的是通过引用传递参数,注意为装箱的Fn特征定义生命周期。

实现Fn特征的装箱闭包存在于堆上,必须明确分配正确的生命周期:Fn() -> &'a T` + 'a

fn main() {
    let name = String::from("World");
    let test = simple(&name);
    println!("Hello {}!", test())
}

fn simple<'a, T: 'a>(val: &'a T) -> Box<Fn() -> &'a T + 'a> {
    Box::new(move || -> &'a T { val })
}

另一种解决方案是使用impl trait,从Rust 1.26开始可用:

fn main() {
    let name = String::from("World");
    let test = simple(&name);
    println!("Hello {}!", test())
}

fn simple<'a, T: 'a>(val: &'a T) -> impl Fn() -> &'a T {
    move || -> &'a T { val }
}

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