指定 Rust 闭包的生命周期

Posted

技术标签:

【中文标题】指定 Rust 闭包的生命周期【英文标题】:Specify Rust closures lifetime 【发布时间】:2020-12-10 12:13:03 【问题描述】:

当我发现这是一个终身问题时,我正在制作执行器/反应器。它与 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`

看起来我的闭包签名被推断为|&amp;'a Runtime| -&gt; 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 中有一个技巧可以使这项工作发挥作用?

【问题讨论】:

我认为你不能只提取 start_with_runtime 到 main 吗?因为这应该可以工作,没有任何明确的生命周期。 start_with_runtime 应该在 crate 中并由应用程序使用(例如,对应用程序隐藏运行时构造)。这是应用程序可以let rt = Runtime::new(); rt.run(|rt| my_async_fn(rt)); 的一种备份计划 异步函数的返回类型确实是captures all argument lifetimes。它必须这样做,因为每当异步函数等待其他未来时,参数都需要存储在 Future 中。 接收像Rc&lt;Runtime&gt; 这样的共享指针而不是对运行时的引用对你有用吗? 我想Rc&lt;&gt; 会起作用,但这是开销,对我来说不喜欢权利所有权模型。 【参考方案1】:

在这个问题中似乎有两个不同的问题:所需的关系能否用 Rust 语法表达,它是否适用于闭包类型推断。

让我们从第一个开始。你是对的,这不能只用where 子句来表达。要表达这一点,需要添加一个辅助特征

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 是一个函数,而不是闭包,这还不错)。【参考方案2】:

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

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 的解决方案看起来更可取,因为它不需要堆和运行时多态性。

以上是关于指定 Rust 闭包的生命周期的主要内容,如果未能解决你的问题,请参考以下文章

如何为闭包参数声明生命周期?

在 Rust 中与生命周期的子类型关系作斗争

什么是非词汇生命周期?

使用字符串时 Rust 中的生命周期

Rust 的生命周期

了解生命周期:最大生命周期和“静态”