通用特征和生命周期的问题

Problem with generic traits and lifetimes

我在较大的上下文中遇到了通用特征的问题,并尝试将其缩小到这个较小的问题。我想要以下功能:

fn count<I, S, T>(pattern: T, item:I) -> usize
where
  I: Atom,
  S: Iterator<Item = I> 
  T: Atoms<I, S>,
{
  pattern.atoms().filter(|i| i == &item).count()
}

该函数应传递两个参数:

该函数应该对两个参数都是通用的(约束条件是 pattern.atoms() 必须 return 一个迭代器,其项类型与 item 相同),例如:

count(Atoms<u8, std::str::Bytes<'_>>, u8) -> usize
count(Atoms<char, std::str::Chars<'_>>, char) -> usize
count(Atoms<u8, std::io::Bytes>, u8) -> usize

对于 Atom,我尝试了这种方法:

pub trait Atom: Copy + Eq + Ord + Display + Debug {}
impl Atom for char {}
impl Atom for u8 {}

接下来我开始使用 Atoms,首先没有生命周期:

pub trait Atoms<I, S>
where
  S: Iterator<Item = I>,
  I: Atom,
{
  fn atoms(&self) -> S;
}

我尝试使用 std::str::Bytes<'a> 作为迭代器为 str 实现这个。因此,编译器错过了原子中的生命周期注释 &'a self。所以我用生命周期改进了这种方法并提出了这段代码:

pub trait Atoms<'a, I, S>
where
  S: Iterator<Item = I> + 'a,
  I: Atom,
{
  fn atoms(&'a self) -> S;
}

impl<'a> Atoms<'a, u8, std::str::Bytes<'a>> for &str {
  fn atoms(&'a self) -> std::str::Bytes<'a> {
    self.bytes()
  }
}

fn count<'a, I, S, T>(pattern: T, item: I) -> usize
where
  I: Atom,
  S: Iterator<Item = I> + 'a,
  T: Atoms<'a, I, S>,
{
  pattern.atoms().filter(|i| *i == item).count() //<--- compiler complains 
}

Playground-Link

现在编译器报错 the parameter type T may not live long enough, ... consider adding ... T:'a。我不明白这个问题。该提示甚至不起作用(接下来是另一个终身问题)所以我不知道我的误解是什么。有人可以帮助我理解(甚至解决)这个问题吗?

atoms函数需要self被借用'a的生命周期。如果你给这个函数一个活不了那么久的类型,这个函数就不能正常工作。

例如,类型 &'b T 的生命周期为 'b。如果您为 &'b T 实现 Atoms 并且 'b'a 更短,则您一定不能调用 atoms 函数。这就是为什么你必须限制 T 至少活到 'a.

Rust 知道这一点并自动对 atoms 函数执行此操作:

fn atoms(&'a self) -> std::str::Chars<'a>
where
    Self: 'a,
{ self.chars() }

但是当你试图在另一个函数中使用那个函数时,Rust 需要你自己添加这个键。这就是为什么您需要为 count 函数添加它。

第二个问题

您的 Atom 特质不需要承载 'a 生命周期。您的第一种方法是正确的。

pub trait Atoms<S>
where
  S: Iterator,
  S::Item: Atom,
{
  fn atoms(&self) -> S;
}

(注意我去掉了泛型参数I,换成了S::Item。这是可能的,因为Item是[=33的关联类型=]。这除了更清晰之外没有任何改变。)

在那之后,实现就非常简单了。

// Note the `&'a str` here, this was missing on your implementation but it is actually
// needed. If you think about it, the bytes that are returned are part of the `&str`
// so they must live at least as long as the reference.
impl<'a> Atoms<std::str::Bytes<'a>> for &'a str {
    // The difference is here. You don't need to chose a lifetime for `&self`.
    // A longer lifetime than `'a` would be ok. That would be a reference to
    // a reference of a lifetime of `'a`.
    //
    // `self` is `&'a str` here.
    // so `&self` is `&&'a str`
    //
    // What you did was telling Rust that that second reference needed to live
    // as long as `'a`. Meaning `&'a &'a str`. But this was wrong. That reference
    // must be able to live longer than `'a`.
    fn atoms(&self) -> std::str::Bytes<'a> {
        self.bytes()
    }
}

在那之后,count 函数按预期工作:

fn count<S, T>(pattern: T, item: I) -> usize
where
  S::Item: Atom,
  S: Iterator,
  T: Atoms<S>,
{
  pattern.atoms().filter(|i| *i == item).count() //<--- compiler does not complain anymore ;)
}