为什么这个 Rust 代码编译时在结构上有生命周期绑定,但如果绑定仅在 impl 上,则会给出生命周期错误?

Alp*_*der 5 lifetime rust

最近想写一段类似下面的代码:

pub struct Foo<'a, F> /* where F: Fn(&u32) -> bool */ {
    u: &'a u32,
    f: F
}

impl<'a, F> Foo<'a, F> 
    where F: Fn(&u32) -> bool 
{
    pub fn new_foo<G: 'static>(&self, g: G) -> Foo<impl Fn(&u32) -> bool + '_>
        where G: Fn(&u32) -> bool 
    {
        Foo { u: self.u, f: move |x| (self.f)(x) && g(x) }
    }
}
Run Code Online (Sandbox Code Playgroud)

在这里, 的实例Foo表示一段数据 (the u32) 的条件,其中Foo可以通过 由限制较少的数据构建限制性更强的数据new_foo,而不会消耗旧数据。然而,上面的代码并没有像编写的那样编译,而是给出了相当神秘的错误消息:

error[E0308]: mismatched types
 --> src/lib.rs:9:52
  |
9 |     pub fn new_foo<G: 'static>(&self, g: G) -> Foo<impl Fn(&u32) -> bool + '_>
  |                                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^ one type is more general than the other
  |
  = note: expected type `std::ops::FnOnce<(&u32,)>`
             found type `std::ops::FnOnce<(&u32,)>`

error: higher-ranked subtype error
  --> src/lib.rs:9:5
   |
9  | /     pub fn new_foo<G: 'static>(&self, g: G) -> Foo<impl Fn(&u32) -> bool + '_>
10 | |         where G: Fn(&u32) -> bool 
11 | |     {
12 | |         Foo { u: self.u, f: move |x| (self.f)(x) && g(x) }
13 | |     }
   | |_____^

error: aborting due to 2 previous errors
Run Code Online (Sandbox Code Playgroud)

经过大量实验,我确实找到了一种使代码编译的方法,并且我相信它会按预期运行。当声明可以在不依赖于这些边界的情况下编写时,我习惯于在 impls 而不是声明上放置边界的约定,但出于某种原因取消对where上述条款的注释,即将边界F: Fn(&u32) -> bool从 impl复制到Foo自身的声明已解决问题。但是,我不知道为什么这会有所作为(一开始我也不太了解错误消息)。有没有人解释这里发生了什么?

egg*_*yal 2

Rust 中存在的唯一子类型是生命周期,因此您的错误(神秘地)暗示存在某种生命周期问题。此外,该错误清楚地指向您的闭包签名,其中涉及两个生命周期:

  1. 您已明确声明的闭包本身的生命周期比匿名生命周期长'_;和

  2. 它的 argument 的生命周期&u32,您没有明确说明,因此会推断出更高排名的生命周期,就像您声明了以下内容一样:

    pub fn new_foo<G: 'static>(&self, g: G) -> Foo<impl for<'b> Fn(&'b u32) -> bool + '_>
        where G: Fn(&u32) -> bool
    
    Run Code Online (Sandbox Code Playgroud)

使用上面更明确的签名会给出一个(非常)稍微有用的错误:

错误[E0308]:类型不匹配
 --> src/lib.rs:9:52
  |
9 | pub fn new_foo<G: 'static>(&self, g: G) -> Foo<impl for<'b> Fn(&'b u32) -> bool + '_>
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 一种类型比另一种更通用
  |
  = 注意:预期类型 `std::ops::FnOnce<(&'b u32,)>`
             找到类型 `std::ops::FnOnce<(&u32,)>`

至少我们现在可以看到“一种类型比另一种类型更通用”:我们期望一个闭包可以接受任何生命周期的参数,但由于某种原因,Rust 认为我们拥有的是一个闭包,它接受一个可能的参数有一些更受限制的寿命范围。

发生了什么事?那么,该函数的返回值是以下表达式:

Foo { u: self.u, f: move |x| (self.f)(x) && g(x) }
Run Code Online (Sandbox Code Playgroud)

这当然是 的一个实例struct Foo<'a, F>,其中这与块上声明的实例(及其特征绑定)F没有关系。事实上,由于结构定义中impl没有显式绑定,因此编译器必须从表达式本身完全推断出此类型。通过给结构定义一个特征边界,您告诉编译器 的实例(包括上面的表达式)具有实现: 的实例,即参数的生命周期范围是无界的。FFFooFfor<'b> Fn(&'b u32) -> bool&u32

好的,所以编译器需要进行推断F,事实上它确实推断它实现了Fn(&u32) -> bool. 然而,它还不够聪明,无法确定该&u32参数可能被限制在什么生命周期范围内。正如上面@rodrigo 的评论中所建议的,添加显式类型注释表明该参数确实可以具有任何生命周期。

如果实际上对闭包参数的可能生命周期有一些限制,您需要通过将 的定义'b从较高级别的特征边界(即for<'b>上面的返回类型)更改为适合您的任何内容来更明确地表明这一点。情况。

希望一旦chalk完全集成到编译器中,它将能够在不受限制和受限制的情况下执行此推理。与此同时,编译器出于谨慎的考虑而犯了错误,没有做出潜在的错误假设。不过,这些错误肯定会更有帮助!