简体   繁体   English

用 Rust 结构驯服终生传染

[英]Taming Lifetime Contagion with Rust Structs

I am attempting to define a struct , in Rust , that contains a member of type async_executor::LocalExecutor , a type that, itself, is generic over a lifetime, 'a :我试图在Rust 中定义一个struct ,其中包含async_executor::LocalExecutor类型的成员,该类型本身在整个生命周期内都是通用的, 'a

pub struct LocalExecutor<'a> {
    inner: ......<Executor<'a>>,
    ...
    ...
}

My own struct must now, apparently, also be generic over a lifetime, 'a , which means nothing to it, itself -- that lifetime is a detail of async_executor::LocalExecutor .我自己的结构必须现在,显然,也是通用的过一辈子, 'a ,这意味着什么呢,本身-这是一辈子的细节async_executor::LocalExecutor

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

My struct exists only when building unit-tests, where I need a mock, single-threaded executor for running async code.我的结构仅在构建单元测试时存在,我需要一个模拟的单线程执行器来运行async代码。 Herein lies the problem: the only consumer of my struct uses #[cfg(...)] conditional compilation, internally, to这就是问题所在:我的结构的唯一使用者在内部使用#[cfg(...)]条件编译来

  1. use my mock when compiling for unit-tests (and not WebAssembly)编译单元测试时使用我的模拟(而不是 WebAssembly)
  2. or one real implementation when compiling for WebAssembly或为 WebAssembly 编译时的一种实际实现
  3. or another real implementation, otherwise.或另一个真正的实现,否则。

This is done with conditional compilation to ensure that the consumer, itself, is not unnecessarily generic, which would polute its public API and just push the problem of contagious generics to everything that consumes it -- a large number of things.这是通过条件编译完成的,以确保使用者本身不是不必要的泛型,这会污染其公共 API 并将传染性泛型的问题推到使用它的所有东西上——大量的东西。 Conditional compilation provides compile-time duck-typing, of a sort, and because that conditional compilation only exists in one place, knowledge of the implementation details is unnecessary for everyone else -- as it should be.条件编译提供了一种编译时鸭子类型,并且因为条件编译只存在于一个地方,所以其他人都不需要了解实现细节——应该是这样。

Neither implementation 2 nor 3 requires generic lifetimes but, because the mock one (1) must be generic over 'a , I now have to make everything, throughout my code-base, generic over some lifetime, 'a' !实现 2 和 3 都不需要泛型生命周期,但是,因为模拟一 (1) 必须在'a是泛型'a ,所以我现在必须在我的代码库中使所有东西在某个生命周期内都是通用的, 'a' (and faff about with PhantomData to stop the compiler from complaining that 'a is meaningless, which it is, most of the time.) (并且用PhantomData以阻止编译器抱怨'a毫无意义,大多数情况下确实如此。)

Is there some way that I could define my mock struct in a way that does not strike this problem?有没有一些方法,我可以在打破这一问题的方法定义我的模拟结构? It would be really convenient if I could use '_ in the member definition, like...如果我可以在成员定义中使用'_ ,那将非常方便,例如...

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

... to indicate that the lifetime of executor should be deduced from the lifetime of the MockThing . ... 表示executor的生命周期应该从MockThing的生命周期中推导出来。 (Of course, this does not work.) (当然,这行不通。)

I suppose I could also just use another async runtime, with ambient executor, for my unit tests, and bypass the problem but that would not help me to understand what is going on, here, and, in general, how one should encapsulate lifetimes as implementation details, in structs that have members that are generic over some lifetime.我想我也可以使用另一个带有环境执行器的async运行时进行单元测试,并绕过问题,但这无助于我理解这里发生了什么,以及一般来说,应该如何将生命周期封装为实现细节,在具有在某个生命周期内通用的成员的结构中。

There is something I am not understanding, though: why Executor (inside LocalExecutor ) must be generic over 'a -- it does not contain a reference with lifetime 'a -- and why they use PhantomData to ensure that it is generic over an invariant lifetime, 'a , and, even, what lifetime invariance means in this case.不过,我有些不明白:为什么Executor (在LocalExecutor内部)必须是通用的,而不是'a —— 它不包含具有生命周期'a的引用 —— 以及为什么他们使用PhantomData来确保它在不变量上是通用的生命周期, 'a ,甚至生命周期不变性在这种情况下意味着什么。 I've been reading about it, in the nomicon and elsewhere, but it will be many days of learning before I can say I understand lifetime variance and all I want to do is " put one of those in my struct ".我一直在阅读它,nomicon和其他地方,但要学习很多天才能说我理解生命周期差异,我想做的就是“将其中一个放入我的struct ”。

Surely there must be some way to tame lifetime contagion and prevent it polluting one's entire code-base just because one type is generic over a lifetime?当然必须有某种方法来驯服终生传染并防止它仅仅因为一种类型在一生中是通用的而污染一个人的整个代码库? Help, please!请帮忙!

TL;DR If you don't need the surrounding lifetime, you should just use 'static : TL;DR 如果你不需要周围的生命周期,你应该只使用'static

#[cfg(test)]
struct MockThing {
    executor: LocalExecutor<'static>
}

Both Executor and LocalExecutor sport a lifetime so the futures they run can refer to data from an outside environment. ExecutorLocalExecutor都有生命周期,因此它们运行的​​期货可以引用来自外部环境的数据。 For example, this compiles and runs as expected:例如,这会按预期编译和运行:

// 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);

LocalExecutor (like its cousin Executor ) is perfectly fine with its futures borrowing values from the environment because it can track the lifetime of all the lending values, and statically prove that no borrow will outlive the value. LocalExecutor (就像它的表亲Executor )非常适合从环境中借用价值的期货,因为它可以跟踪所有借出价值的生命周期,并静态证明任何借入都不会超过该价值。 This is what the lifetime 'a on its struct means: it represents an intersection of the scopes of the values the futures submitted to the executor refer.这就是其结构上的生命周期'a含义:它代表提交给执行程序的期货所引用的值的范围的交集。

With the exception of the 'static lifetime, you cannot explicitly specify a lifetime 'foo and name it when constructing LocalExecutor::<'foo>::new() .除了'static生命周期,您不能在构造LocalExecutor::<'foo>::new()时显式指定生命周期'foo并命名它。 Instead, the lifetime gets deduced automatically, in this case to the scope of the greeting variable, and gets named in the types and functions that work with it.相反,生命周期被自动推导出,在这种情况下是greeting变量的范围,并在使用它的类型和函数中命名。 Think of it like passing a closure to a function that expects a T: Fn() .把它想象成将一个闭包传递给一个需要T: Fn()的函数。 You cannot name the closure's type in the caller, but you the callee can name it as T .您不能在调用者中命名闭包的类型,但被调用者可以将其命名为T Analogously, the caller can't specify a lifetime on LocalExecutor , but LocalExecutor<'a> sees it as 'a .类似地,调用者无法在LocalExecutor上指定生命周期,但LocalExecutor<'a>将其视为'a

Now let's try the same with tokio:现在让我们用 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);
println!("done {}", greeting);

That fails to compile:编译失败:

error[E0373]: async block may outlive the current function, but it borrows `greeting`, which is owned by the current function
 --> src/main.rs:6:38
  |
6 |       let handle = runtime.spawn(async {
  |  ______________________________________^
7 | |         println!("Hello {}", greeting);
  | |                              -------- `greeting` is borrowed here
8 | |     });
  | |_____^ may outlive borrowed value `greeting`
  |
  = note: async blocks are not executed immediately and must either take a reference or ownership of outside variables they use
help: to force the async block to take ownership of `greeting` (and any other referenced variables), use the `move` keyword
  |
6 |     let handle = runtime.spawn(async move {
  |                                      ++++

tokio is not happy with any outside borrowing happening in any of its futures. tokio 对其任何期货中发生的任何外部借款都不满意。

They must satisfy the 'static bound, meaning that it must not contain references to anything from its outside environment, except for 'static data.它们必须满足'static边界”,这意味着它不得包含对其外部环境的任何内容的引用, 'static数据'static除外。 (They may also own any data they choose, which is why the compiler suggests a move - except in that case the last println!() would fail to compile because greeting would be gone.) (他们也可能拥有他们选择的任何数据,这就是编译器建议move - 除非在这种情况下最后一个println!()将无法编译,因为greeting将消失。)

If you don't need the borrowing functionality, just use 'static as lifetime:如果您不需要借用功能,只需使用'static作为生命周期:

#[cfg(test)]
struct MockThing {
    executor: LocalExecutor<'static>
}

...and you'll be no worse off than with tokio. ......你的情况不会比使用 tokio 更糟。 No lifetime contagion, at the "cost" of futures only being allowed to own values (which is probably just fine).没有终生传染,以只允许期货拥有价值的“成本”为代价(这可能很好)。

It would be really convenient if I could use '_ in the member definition, [...] to indicate that the lifetime of executor should be deduced from the lifetime of the MockThing .如果我可以在成员定义中使用'_ , [...] 来表示 executor 的生命周期应该从MockThing的生命周期中推导出来,那将会非常方便。

That's not how lifetimes work.生命周期不是这样运作的。 A lifetime 'a is a scope, a set of source code lines in the caller's environment, and that's not something the parent struct can provide ( yet ).生命周期'a是一个范围,是调用者环境中的一组源代码行,这不是父结构可以提供的()。 A non-static lifetime has to be connected to a local object in the surrounding environment.非静态生命周期必须连接到周围环境中的本地对象。

In the first code snippet above, the lifetime of the LocalExecutor is automatically deduced to the lifetime of the greeting local variable.在上面的第一个代码片段中, LocalExecutor的生命周期被自动推导出到greeting局部变量的生命周期。 If we had borrowed multiple variables, the lifetime would be the lifetime of the shortest-lived one.如果我们借用了多个变量,则生命周期将是生命周期最短的变量的生命周期。 If we had borrowed ones whose scopes don't overlap, we'd get a compilation error.如果我们借用了范围不重叠的那些,我们会得到一个编译错误。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM