为什么函数体在结构体中可以编译,但在特质中不能?

11
这段代码定义了一个非常简单的特征来表示二叉树,以及实现该特征的结构体:
pub trait BTree<T> {
    fn all(&self) -> Option<(&Self, &Self, &T)>;
    fn left(&self) -> Option<&Self>;
    fn right(&self) -> Option<&Self>;
    fn value(&self) -> Option<&T>;
}

pub struct MyBTree<T> {
    opt: Option<Box<(MyBTree<T>, MyBTree<T>, T)>>,
}

impl<T> BTree<T> for MyBTree<T> {
    fn all(&self) -> Option<(&Self, &Self, &T)> {
        match self.opt {
            None => None,
            Some(ref tuple) => Some((&tuple.0, &tuple.1, &tuple.2)),
        }
    }

    fn left(&self) -> Option<&Self> {
        match self.all() {
            None => None,
            Some((left, _, _)) => Some(left),
        }
    }

    fn right(&self) -> Option<&Self> {
        match self.all() {
            None => None,
            Some((right, _, _)) => Some(right),
        }
    }

    fn value(&self) -> Option<&T> {
        match self.all() {
            None => None,
            Some((_, _, value)) => Some(value),
        }
    }
}

leftrightvalue的实现可以移动到特质内部,因为它们仅依赖于特质定义的all方法,而不依赖于具体实现细节。

这适用于value,但对于leftright不适用。例如,如果我尝试将left的实现移到特质的主体中,那么我会得到以下编译错误:

error[E0311]: the parameter type `T` may not live long enough
--> src/lib.rs:6:24
  |
6 |             match self.all() {
  |                        ^^^
  |
= help: consider adding an explicit lifetime bound for `T`
note: the parameter type `T` must be valid for the anonymous lifetime #1 defined on the method body at 5:9...
--> src/lib.rs:5:9
  |
5 | /         fn left(&self) -> Option<&Self> {
6 | |             match self.all() {
7 | |                 None => None,
8 | |                 Some((left, _, _)) => Some(left),
9 | |             }
10| |         }
  | |_________^
note: ...so that the reference type `&T` does not outlive the data it points at
--> src/lib.rs:6:24
  |
6 |             match self.all() {
  |

为什么这个问题出现在trait中而不是MyBTree的实现中?

为什么编译器会抱怨在忽略T值的方法中T的生命周期,但是在返回类型中提到T的value方法可以正常工作?


4
代码可以使用非词法生命周期 #![feature(nll)] 进行编译 - Tim Diekmann
是的,核心区别似乎在于NLL允许引用引用不会超出引用寿命的数据。fn f<'a, 'b>() { let _: &'a &'b (); } - dtolnay
1
如果您使用关联类型而不是类型参数,那么它可以编译。除非有一个原因,需要单个类型能够实现BTree特质的多个实例,否则我建议使用关联类型版本。这样,当您编写使用BTree的通用函数时,您不需要为BTreeT添加额外的类型参数。 - Francis Gagné
@FrancisGagné 您是正确的,关联类型在这里可能更好;我仍然不太擅长在类型参数和关联类型之间进行选择。感谢您指出这一点。话虽如此,我不清楚为什么关联类型没有与类型参数相同的生命周期问题... :-/ - Pierre-Antoine
1个回答

11
为什么这个问题会在特性中出现,而不是在MyBTree的实现中出现?
当考虑为具有生命周期的类型实现BTree时,这些方法签名变得更加微妙。我对涉及泛型类型参数或Self类型的所有生命周期错误的一般建议是:将重点放在类型为借用类型的情况上。
借用类型的问题在于,你永远无法拥有一个比它所引用的数据寿命更长的引用。这个原则的最简单例子是:
fn f<'a, 'b>() {
    // error[E0491]: in type `&'a &'b ()`, reference has a longer
    // lifetime than the data it references
    let _: &'a &'b ();
}

Rust要求我们保证被引用的数据的生命周期比引用本身更长,即在这个案例中,'b的生命周期必须比'a更长。
fn f<'a, 'b: 'a>() {
    let _: &'a &'b ();
}

现在让我们将这个应用到你的BTree情况中,考虑如果T是一个借用类型,比如&(),会出现什么问题。首先,看一下你放置在impl BTree for MyBTree中的以下两种方法。我已经明确写出了省略的生命周期以澄清讨论。
impl<T> BTree<T> for MyBTree<T> {
    fn left<'a>(&'a self) -> Option<&'a Self> { /* ... */ }
    fn value<'a>(&'a self) -> Option<&'a T> { /* ... */ }
}

为了调用left,调用者必须知道Self比生命周期'a更长。为了调用value,调用者必须知道Self比生命周期'a更长T比生命周期'a更长(正如我们上面看到的那样,为了使&'a T成为有意义的类型)。除非满足这些要求,否则借用检查器不会让他们调用这些方法,因此实现可以假定这些要求得到满足。
此外,借用检查器可以看到如果Self'a更长,则T也比'a更长,因为MyBTree<T>包含一个类型为T的值。
这就是为什么在impl<T> BTree<T> for MyBTree<T>中实现leftvalue时没有问题的原因。调用者和MyBTree<T>结构共同保证了所需的所有内容都被保存了足够长的时间。
现在,假设我们把这些方法放在BTree<T> trait定义中。
trait BTree<T> {
    fn left<'a>(&'a self) -> Option<&'a Self> { /* ... */ }
    fn value<'a>(&'a self) -> Option<&'a T> { /* ... */ }
}

调用者必须知道如果调用了left,则Self的生命周期要超过'a的生命周期,但是他们没有保证T的生命周期也超过'a。例如,他们可能有T=&'b(),其中'b是其他完全无关的较短生命周期。如上所述,这将使得&'a T等于&'a &'b(),这不是一种合法类型。
Rust之所以接受在trait中定义value,是因为调用者保证了SelfT都超过了输入生命周期'a。Rust之所以不接受在trait中定义left,是因为调用者只保证了Self超过了'a的生命周期,并没有保证T也超过了'a的生命周期,而实现却假定了这一点。
“忽略”T值的方法中编译器如何抱怨T的生命周期,而对于在其返回类型中提到T的value方法,则可以工作?
那么错误与返回值无关,而与对all()的调用有关。仔细看看。
error[E0311]: the parameter type `T` may not live long enough
--> src/lib.rs:6:24
  |
6 |             match self.all() {
  |                        ^^^

为了调用all(),调用者需要证明输入和输出类型是有效的。但是如果T是类似于&'b ()这样的类型,则可能不成立。因此all()会返回&'a &'b (),因此借用检查器会阻止调用。
我们可以通过明确我们所假定的保证来解决这个问题,在这种情况下,T'a存在时间更长。
trait BTree<T> {
    fn left<'a>(&'a self) -> Option<&'a Self>
    where
        T: 'a,
    { 
        /* ... */ 
    }
}

1
非常感谢您提供如此详细的解释。现在我对这个问题更加明白了。实际上,我忽略了一点,即特质可以用许多方式来实现,包括借用类型!Rust 真是太棒了! - Pierre-Antoine

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接