为啥函数体在结构中编译,而不是在特征中?

Posted

技术标签:

【中文标题】为啥函数体在结构中编译,而不是在特征中?【英文标题】:Why does a function body compile in a struct, but not in a trait?为什么函数体在结构中编译,而不是在特征中? 【发布时间】:2018-08-08 15:03:33 【问题描述】:

这段代码定义了一个非常简单的特征来表示二叉树和一个实现该特征的结构:

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 的实现可以移动到 trait 内部,因为它们只依赖于 trait 定义的 all 方法,而不依赖于实现细节。

这适用于value,但适用于leftright。例如,如果我尝试将 left 的实现移动到 trait 的主体中,我会收到以下编译错误:

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 的生命周期——而它与确实的方法value 一起工作在其返回类型中提及 T?

【问题讨论】:

代码 compiles 和 non lexical lifetimes #![feature(nll)] 是的,核心区别似乎是 NLL 允许引用不超过引用的数据。 fn f&lt;'a, 'b&gt;() let _: &amp;'a &amp;'b (); 如果你使用an associated type instead of a type parameter,那么它会编译。除非有理由认为单个类型应该能够实现 BTree 特征的多个实例,否则我建议您改用关联的类型版本。这样,当您使用BTree 编写泛型函数时,您将不需要为BTreeT 添加额外的类型参数。 @FrancisGagné 你说得对,关联类型在这里可能更好;我仍然不太擅长在这些参数和类型参数之间进行选择。感谢您指出了这一点。话虽如此,我还不清楚为什么关联类型没有与类型参数相同的生命周期问题...:-/ 【参考方案1】:

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

当您考虑为具有生命周期的类型实现 BTree&lt;T&gt; 时,这些方法签名会变得更加细微。对于涉及泛型类型参数或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 是像&amp;() 这样的借用类型会出现什么问题。首先,查看您放在impl&lt;T&gt; BTree&lt;T&gt; for MyBTree&lt;T&gt; 中的以下两个方法。我已经明确地写了省略的生命周期来澄清讨论。

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的寿命长(为了&amp;'a T 是一个有意义的类型,正如我们在上面看到的)。除非满足这些要求,否则借用检查器不会让他们调用这些方法,因此实现可以假定满足这些要求。

此外,借用检查器可以看到 if Self'a then 也比 T'a 还要长,因为 MyBTree&lt;T&gt; 包含一个值输入T

这就是为什么在impl&lt;T&gt; BTree&lt;T&gt; for MyBTree&lt;T&gt; 中实现leftvalue 没有问题。调用者和MyBTree&lt;T&gt; 结构共同保证一切都活到我们需要的时候。

现在我们在 BTree&lt;T&gt; 特征定义中有这些方法。

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=&amp;'b () 一些完全不相关的较短生命周期'b。正如我们在上面看到的,这将使&amp;'a T 等于&amp;'a &amp;'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 类似于&amp;'b (),这可能不是真的。 all() 将返回 &amp;'a &amp;'b (),因此借用检查器会阻止调用。

我们可以通过明确我们的实现所假设的保证来解决这个问题,在这种情况下,T'a 寿命更长。

trait BTree<T> 
    fn left<'a>(&'a self) -> Option<&'a Self>
    where
        T: 'a,
     
        /* ... */ 
    

【讨论】:

非常感谢您的详细解释。事情对我来说越来越清楚了。事实上,我忽略了一个事实,即特征可以被许多东西实现,包括借用的类型! Rust 太棒了 :)

以上是关于为啥函数体在结构中编译,而不是在特征中?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我可以在 C 中调用一个函数而不声明它,但在 C++ 中却不能?

Swift中结构体的方法调度&内存分区

为啥从命令行编译而不是从 IDE 编译时会出现歧义错误?

如何使 std::vector 的 operator[] 编译在 DEBUG 中而不是在 RELEASE 中进行边界检查

库在可执行文件内调用函数,而不是在库内

同一函数中的异常处理会使编译时间减慢 2 倍以上,为啥?