用 Rust 结构驯服终生传染

Xha*_*lie 6 generics lifetime rust

我试图在Ruststruct中定义一个, ,它包含 type 的一个成员,该类型本身在整个生命周期内是通用的:async_executor::LocalExecutor'a

pub struct LocalExecutor<'a> {
    inner: ......<Executor<'a>>,
    ...
    ...
}
Run Code Online (Sandbox Code Playgroud)

显然,我自己的结构现在在整个生命周期中'a必须是通用的, ,这对它本身来说没有任何意义——生命周期是 的一个细节async_executor::LocalExecutor

#[cfg(all(test, not(target = "wasm32")))]
struct MockThing<'a> {
    executor: async_executor::LocalExecutor<'a>,
}
Run Code Online (Sandbox Code Playgroud)

我的结构仅在构建单元测试时才存在,其中我需要一个模拟的单线程执行器来运行async代码。问题在于:我的结构的唯一使用者#[cfg(...)]在内部使用条件编译来

  1. 编译单元测试时使用我的模拟(而不是 WebAssembly)
  2. 或者为 WebAssembly 编译时的一种实际实现
  3. 或另一个实际的实现,否则。

这是通过条件编译来完成的,以确保使用者本身不是不必要的泛型,这会污染其公共 API,并将传染性泛型的问题推向所有使用它的事物——大量的事物。条件编译提供了某种编译时鸭子类型,并且由于条件编译仅存在于一个位置,因此其他人都不需要了解实现细节 - 正如它应该的那样。

实现 2 和 3 都不需要通用生命周期,但是,因为模拟一 (1) 必须是通用的'a,所以我现在必须在整个代码库中使所有内容在某个生命周期内通用,'a'!(并且热衷于PhantomData阻止编译器抱怨这'a是毫无意义的,大多数时候确实如此。)

有没有什么方法可以让我定义我的模拟结构而不会遇到这个问题?'_如果我可以在成员定义中使用它会非常方便,例如......

#[cfg(test)]
struct MockThing {
    executor: async_executor::LocalExecutor<'_>,
}
Run Code Online (Sandbox Code Playgroud)

...表明 的生命周期executor应从 的生命周期推算出来MockThing。(当然,这是行不通的。)

我想我也可以使用另一个async运行时,带有环境执行器,用于我的单元测试,并绕过问题,但这无助于我理解这里发生了什么,以及一般来说,应该如何将生命周期封装为实现详细信息,在具有在某个生命周期内通用的成员的结构中。

不过,有一些我不明白的地方:为什么 Executor(内部LocalExecutor)必须是通用的'a——它不包含具有生命周期的引用'a——以及为什么他们使用PhantomData来确保它在不变的生命周期内是通用的'a,甚至,在这种情况下,生命周期不变性意味着什么。我一直书本和其他地方阅读有关它的内容,但需要很多天的学习时间才能说我理解生命周期差异,而我想做的就是“将其中之一放入我的struct”。

当然,必须有某种方法来抑制生命周期传染,并防止它仅仅因为一种类型在生命周期内是通用的而污染整个代码库?请帮助!

use*_*342 2

TL;DR 如果您不需要执行器的 future 来引用执行器周围的本地数据,您应该只使用'static

#[cfg(test)]
struct MockThing {
    executor: LocalExecutor<'static>
}
Run Code Online (Sandbox Code Playgroud)

Executor都有LocalExecutor生命周期,以便允许它们运行的​​期货从外部环境借用数据。例如,它按预期编译并运行:

// local data
let greeting = "foo".to_owned();

// executor
let local_ex = LocalExecutor::new();

// spawn a future that references local data
let handle = local_ex.spawn(async {
    println!("Hello {}", greeting);
});
future::block_on(local_ex.run(handle));

// data still alive
println!("done {}", greeting);
Run Code Online (Sandbox Code Playgroud)

LocalExecutor(像它的表兄弟一样Executor)跟踪借贷价值的生命周期,并静态地证明没有借贷会比该价值更长久。这就是其结构体上的生命周期的'a含义:它表示提交给执行器的 future 借用的值的范围的交集。

除了生命周期之外'static,您无法在构造 时显式指定生命周期'foo并为其命名LocalExecutor::<'foo>::new()。相反,生命周期会自动推导,在本例中为 的范围greeting,并且仅在接收它的类型和函数中命名。这就像闭包的类型,在声明闭包时它是未命名的,但当泛型函数将其接受为T: Fn(). 类似地,调用者无法指定 的生命周期LocalExecutor,但LocalExecutor<'a>将其视为'a

现在让我们对 tokio进行同样的尝试:

let greeting = "foo".to_owned();

let runtime = tokio::runtime::Runtime::new().unwrap();
let handle = runtime.spawn(async {
    println!("Hello {}", greeting);
});
runtime.block_on(handle);
drop(runtime);  // greeting outlives runtime

println!("done {}", greeting);
Run Code Online (Sandbox Code Playgroud)

上面的代码显然是合理的,因为greeting它的寿命比运行时长,但它无法编译:

#[cfg(test)]
struct MockThing {
    executor: LocalExecutor<'static>
}
Run Code Online (Sandbox Code Playgroud)

tokio 不允许在其任何期货中进行任何外部借贷。

它们必须满足'static界限,这意味着未来不得包含对外部环境中任何内容的引用('static数据除外)。(他们也可能拥有他们选择的任何数据,这就是为什么编译器建议 -move除非在这种情况下,最后一个println!()将无法编译,因为greeting会消失。)

如果不需要从本地上下文借用,只需用作'static生命周期:

#[cfg(test)]
struct MockThing {
    executor: LocalExecutor<'static>
}
Run Code Online (Sandbox Code Playgroud)

...你的情况不会比在 tokio 更糟糕。没有终生传染,代价是期货只允许拥有价值(这对于大量用例来说很好,正如 tokio 接受限制所证明的那样)。

如果我可以'_在成员定义中使用 [...] 来指示执行器的生命周期应该从MockThing.

生命周期不是这样运作的。生命周期是一个作用域,是调用者环境'a中的一组源代码行,而这不是父结构可以提供的(目前)。非静态生命周期必须连接到周围环境中的本地对象。

在上面的第一个代码片段中, 的生命周期LocalExecutor自动推导为greeting局部变量的生命周期。如果我们借用了多个变量,则生命周期将是寿命最短的变量的生命周期。如果我们借用了范围不重叠的内容,我们就会收到编译错误。

  • @Xharlie 请注意,关于 Polonius 的谈话很大程度上与这里的问题无关,但“尚未”链接太酷了,不容错过。[这个最近的问题](/sf/ask/4889516691/)是当前借用检查器拒绝的完全有效代码的一个更好的例子,而 Polonius 将接受(不对代码进行任何修改)。 (2认同)