Rust函数返回闭包时:需要明确生命周期限定

4
以下代码无法编译。
fn main() {
    let foo = bar(8);

    println!("Trying `foo` with 4: {:d}", foo(4));
    println!("Trying `foo` with 8: {:d}", foo(8));
    println!("Trying `foo` with 13: {:d}", foo(13));
}

//

fn bar(x: int) -> (|int| -> int) {
    |n: int| -> int {
        if n < x { return n }

        x
    }
}

错误信息如下:
11:32 error: explicit lifetime bound required
.../hello/src/main.rs:11 fn bar(x: int) -> (|int| -> int) {
                                            ^~~~~~~~~~~~

我通过值传递将整数参数传递给bar。为什么Rust关心通过值传递的整数的生命周期?编写返回闭包的这种函数的正确方法是什么?谢谢。
编辑
我在手册中发现了以下内容:在最简单和最便宜的形式(类似于a || {}表达式),lambda表达式通过引用捕获其环境,有效地借用了在函数内部提到的所有外部变量的指针。或者,编译器可能会推断lambda表达式应该从环境中复制或移动值(取决于它们的类型)。 编译器如何推断是否通过引用、复制还是移动来捕获外部变量?评估标准是什么,应用顺序是什么?是否有记录(除了阅读编译器代码)?
2个回答

10

编辑: 我用i32代替了int,因为int现在已被弃用。它已被isize替换,但这可能不是正确的类型。

编译器没有抱怨闭包的参数,而是抱怨闭包本身。你需要为闭包指定生命周期。

fn bar<'a>(x: i32) -> (|i32|:'a -> i32) {
    |n: i32| -> i32 {
        if n < x { return n }

        x
    }
}

但是它不起作用:

<anon>:13:16: 13:17 error: captured variable `x` does not outlive the enclosing closure
<anon>:13         if n < x { return n }
                         ^
<anon>:11:41: 17:2 note: captured variable is valid for the block at 11:40
<anon>:11 fn bar<'a>(x: i32) -> (|i32|:'a -> i32) {
<anon>:12     |n: i32| -> i32 {
<anon>:13         if n < x { return n }
<anon>:14 
<anon>:15         x
<anon>:16     }
          ...
<anon>:11:41: 17:2 note: closure is valid for the lifetime 'a as defined on the block at 11:40
<anon>:11 fn bar<'a>(x: i32) -> (|i32|:'a -> i32) {
<anon>:12     |n: i32| -> i32 {
<anon>:13         if n < x { return n }
<anon>:14 
<anon>:15         x
<anon>:16     }
          ...

这是因为闭包尝试通过引用来捕获 x,但是闭包的生命周期超出了 x 的有效期,这是不合法的(就像返回对 x 的引用一样不合法)。

让我们尝试使用一个 procproc 通过移动值来捕获。

编辑:自从撰写本答案以来,proc 已从该语言中删除。

fn main() {
    let foo = bar(8);

    println!("Trying `foo` with 4: {:d}", foo(4));
    println!("Trying `foo` with 8: {:d}", foo(8));
    println!("Trying `foo` with 13: {:d}", foo(13));
}

//

fn bar<'a>(x: i32) -> (proc(i32):'a -> i32) {
    proc(n: i32) -> i32 {
        if n < x { return n }

        x
    }
}

很遗憾,这也不起作用。

<anon>:5:43: 5:46 error: use of moved value: `foo`
<anon>:5     println!("Trying `foo` with 8: {:d}", foo(8));
                                                   ^~~
note: in expansion of format_args!
<std macros>:2:23: 2:77 note: expansion site
<std macros>:1:1: 3:2 note: in expansion of println!
<anon>:5:5: 5:51 note: expansion site
<anon>:4:43: 4:46 note: `foo` moved here because it has type `proc(i32) -> i32`, which is non-copyable (perhaps you meant to use clone()?)
<anon>:4     println!("Trying `foo` with 4: {:d}", foo(4));
                                                   ^~~
您只能调用 proc 一次。调用将消耗该闭包。
现在的正确解决方案是使用“非装箱”闭包:
fn main() {
    let foo = bar(8);

    println!("Trying `foo` with 4: {}", foo(4));
    println!("Trying `foo` with 8: {}", foo(8));
    println!("Trying `foo` with 13: {}", foo(13));
}

//

fn bar(x: i32) -> Box<Fn(i32) -> i32 + 'static> {
    Box::new(move |&: n: i32| -> i32 {
        if n < x { return n }

        x
    })
}

输出:

Trying `foo` with 4: 4
Trying `foo` with 8: 8
Trying `foo` with 13: 8

感谢您提供详细的答案。我的Rust版本是rustc 0.12.0-nightly (af3889f69 2014-09-18 21:20:38 +0000)。它无法编译最终版本。我收到以下错误:error: internal compiler error: get_unique_type_id_of_type() - unexpected type: closure, ty_unboxed_closure(syntax::ast::DefId{krate: 0u32, node: 170u32}, ReScope(169u32)) note: the compiler hit an unexpected failure path. this is a bug. - JONNALAGADDA Srinivas
@sigma.ml:你可能需要再次更新rustc。最终版本对我来说运行良好(rustc 0.12.0-nightly (d7e1bb5ff 2014-09-21 01:00:29 +0000))。 - kennytm
@KennyTM:我已经更新了我的Rust副本。当前版本:rustc 0.12.0-nightly (72841b128 2014-09-21 20:00:29 +0000)。我发现它和你的不完全相同。使用该版本时,我仍然像以前一样出现上述编译器崩溃。我正在使用Ubuntu 14.04 x86_64,这是否提供了任何额外的信息。 - JONNALAGADDA Srinivas
1
@sigma.ml,未封装的闭包是实验性功能,而常规闭包无法从函数返回,因为它们是堆栈分配的。因此,您必须等待未封装的闭包不再是实验性的,或者您可以使用常规结构模拟它们。毕竟,未封装的闭包只是实现某些特征的匿名结构。 - Vladimir Matveev
1
这似乎不适用于最新的夜版。特别是|&:int |部分会出现错误。 - mmtauqir
显示剩余2条评论

5
编译器如何推断是按引用、拷贝还是移动来捕获外部变量?评估标准是什么,应用顺序是什么?是否有文档记录(除了阅读编译器的代码)?
让我补充一下Francis的回答:
|x| a*x+b这样的闭包总是通过引用捕获它们的环境(例如这里的ab)。在你的情况下,这些是函数局部变量,Rust会阻止你返回这样的闭包,因为这些函数局部变量将不再存在。感谢借用检查器捕捉到这个错误。这些闭包的用例通常是将它们作为参数传递给其他函数,而不是返回它们。但是,如果您没有访问任何其他变量,这样的闭包就可以超出创建它的函数的范围:||:'static -> SomeType。这些闭包的表示只是一对指针。一个指向函数,一个指向函数的堆栈帧(如果有东西被引用)。
使用proc编写的闭包总是通过“获取”它们的环境(它们被移动到闭包对象的状态中)来捕获它们的环境。这种类型的闭包的另一个属性是,您只能调用它们一次,因为相关联的函数实际上消耗闭包的状态。这对于启动并发任务很有用。proc闭包会产生堆分配成本,因为它们间接存储其状态(类似于Box所做的)。这样做的好处是,proc闭包的表示(忽略盒装状态)只是一对指针。一个指向函数,一个指向盒装变量。
然后,还有所谓的未盒装闭包。据我所知,未盒装闭包仍被认为是实验性的。但是它们允许您做到您想要的——不是直接,而是在盒装时。未盒装闭包也通过值来捕获它们的环境。但与proc闭包不同的是,这里没有涉及堆分配。它们直接存储它们的变量。您可以将它们视为具有唯一的、无法拼写的类型的结构体,该结构体实现以下trait之一:FnFnMutFnOnce,具有一个采用其自身参数作为&self&mut selfself的单个方法。未盒装闭包很好,因为它们缺少函数和变量的间接级别,这允许更好的内联,从而获得更好的性能。但是目前不可能编写直接返回这样的未盒装闭包的函数。一种解决方案是像Francis在回答的最后一节中所示那样将未盒装闭包盒装。

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