指定Rust闭包的生命周期

14

我在制作执行器/反应堆时发现了这个终身难题。它与异步/Future无关,并且可以在没有异步语法糖的情况下重现。

Translated:

我在制作executor/reactor时发现了一个持久性的问题。 它与async/Future无关,可以在没有async语法糖的情况下重现。

use std::future::Future;

struct Runtime;

fn start_with_runtime<C, F>(closure: C)
where
    C: for<'a> FnOnce(&'a Runtime) -> F,
    F: Future
{
    let rt = Runtime;
    let _future = closure(&rt);
    // block_on(future); 
}

async fn async_main(_rt: &Runtime) {
    // I can use _rt to do async stuff here
}

fn main() {
    start_with_runtime(|rt| { async_main(rt) });
}

我想让 start_with_runtime() 运行未来并将异步运行时引用作为参数提供。

它无法编译:

error: lifetime may not live long enough
  --> src/main.rs:17:31
   |
17 |     start_with_runtime(|rt| { async_main(rt) });
   |                         ---   ^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2`
   |                         | |
   |                         | return type of closure is impl std::future::Future
   |                         has type `&'1 Runtime`

我认为这个问题似乎是由于Rust如何推断闭包的生命周期而导致的:

https://github.com/rust-lang/rust/issues/58052

fn main() {
    let f = |x: &i32| x;
    let i = &3;
    let j = f(i);
}

也无法编译:

error: lifetime may not live long enough
 --> src/main.rs:2:23
  |
2 |     let f = |x: &i32| x;
  |                 -   - ^ returning this value requires that `'1` must outlive `'2`
  |                 |   |
  |                 |   return type of closure is &'2 i32
  |                 let's call the lifetime of this reference `'1`

看起来我的闭包签名被推断为|&'a Runtime| -> impl Future + 'b,因此出现了生命周期错误。我觉得如果给出正确的闭包预期签名将会有帮助,但是我如何在start_with_runtime中提供正确的签名呢?

fn start_with_runtime<C>(closure: C)
where
    C: for<'a> FnOnce(&'a Runtime) -> (impl Future + 'a),

无法工作,因为在此处不允许使用impl Trait

fn start_with_runtime<C,F>(closure: C)
where
    C: for<'a> FnOnce(&'a Runtime) -> F,
    F: Future + 'a

由于'a在HRTB表达式之外是未知的,因此不起作用。

如果我知道类型,它就能工作:


struct MyType<'a> {
    _rt: &'a Runtime
} 
fn start_with_runtime<C>(closure: C)
where
    C: for<'a> FnOnce(&'a Runtime) -> MyType<'a>,
这有点悲伤,当你思考了所有的生命,但语言无法表达出来。也许在 Rust 中有什么诀窍可以解决这个问题吗?

这有点悲伤,当你思考了所有的生命,但语言无法表达出来。也许在 Rust 中有什么诀窍可以解决这个问题吗?


我猜你不能把 start_with_runtime 提取到主函数中吧?因为那样应该可以工作,而不需要任何明确的生命周期。 - L. Riemer
1
start_with_runtime 应该在一个 crate 中,并被应用程序使用(例如,将 Runtime 构造从应用程序中隐藏)。这是一种备份计划,应用程序可以 let rt = Runtime::new(); rt.run(|rt| my_async_fn(rt)); - orcy
异步函数的返回类型确实捕获所有参数生命周期。这是必须的,因为当异步函数等待其他future时,参数需要存储在Future中。 - Sven Marnach
你是否可以接受像 Rc<Runtime> 这样的共享指针,而不是对运行时的引用? - Sven Marnach
我想 Rc<> 可以工作,但这会增加开销,并且对我来说不像是正确的所有权模型。 - orcy
可能相关:https://github.com/rust-lang/rfcs/pull/3216 - Lukas Kalbertodt
2个回答

7

这个问题似乎有两个不同的方面:第一个是所需关系能否用Rust语法表达,第二个是它是否能与闭包类型推断协同工作。

让我们从第一个方面开始。你说得对,这不能仅用where从句表达。为了表达这个,需要添加一个辅助trait。

trait BorrowingFn<'a> {
    type Fut: std::future::Future<Output = Something> + 'a;
    fn call(self, arg: &'a Runtime) -> Self::Fut;
}

它允许我们将所需的界限写成

    C: for<'a> BorrowingFn<'a>,

提供此特质的全面实现,适用于所有相关函数。
impl<'a, Fu: 'a, F> BorrowingFn<'a> for F
where
    F: FnOnce(&'a Runtime) -> Fu,
    Fu: std::future::Future<Output = ()> + 'a,
{
    type Fut = Fu;
    fn call(self, rt: &'a Runtime) -> Fu {
        self(rt)
    }
}

(playground)

好的,它可以与异步函数一起使用,但是它能够与需要类型推断的闭包一起使用吗?不幸的是,答案是否定的。

error: implementation of `BorrowingFn` is not general enough
  --> src/main.rs:33:5
   |
5  | / trait BorrowingFn<'a> {
6  | |     type Fut: std::future::Future<Output = ()> + 'a;
7  | |     fn call(self, arg: &'a Runtime) -> Self::Fut;
8  | | }
   | |_- trait `BorrowingFn` defined here
...
33 |       start_with_runtime(|rt| async_main(rt)); // however, it does not work with closure type inference :-(
   |       ^^^^^^^^^^^^^^^^^^ implementation of `BorrowingFn` is not general enough
   |
   = note: `[closure@src/main.rs:33:24: 33:43]` must implement `BorrowingFn<'0>`, for any lifetime `'0`...
   = note: ...but `[closure@src/main.rs:33:24: 33:43]` actually implements `BorrowingFn<'1>`, for some specific lifetime `'1`

这个问题正在 rust-lang/rust#70263 中跟踪。编译器目前还不够聪明,无法注意到这个闭包需要更高阶的类型。

出于好奇,我尝试在Nightly上使用-Z chalk编译,但它还没有为此准备好(内部编译器错误)。


看起来是非常聪明的方法!我认为这对我来说是一个很好的折衷方案(它不涉及堆,但需要FnOnce是一个函数,而不是闭包,这还算可以接受)。 - orcy

1

抱歉,这是语言的限制。您只能在具体类型上指定生命周期。一种解决方法是使用特质对象类型。

fn start_with_runtime<C, F, T>(closure: C)
where
    C: for<'a> FnOnce(&'a Runtime) -> Pin<Box<dyn Future<Item = T> + Send + 'a>>,
{
    let rt = Runtime;
    let _future = closure(&rt);
    // block_on(future); 
}

经过一些修复,我成功编译了这个版本。在这种情况下,关闭看起来像是 start_with_runtime(|rt| { Box::pin(async_main(rt)) }); 所以这是一个可能的解决方法。然而,我发现 Tanriol 的解决方案更可取,因为它不需要堆和运行时多态性。 - orcy

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