如何为结构体引用实现Add trait?

93
我创建了一个由两个元素组成的Vector结构体,并且想要重载+运算符。
我让所有的函数和方法都采用引用而不是值,同时希望+运算符也能以同样的方式工作。
impl Add for Vector {
    fn add(&self, other: &Vector) -> Vector {
        Vector {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

根据我尝试的不同变体,我要么遇到终身问题,要么遇到类型不匹配。具体来说,`&self`参数似乎没有被视为正确的类型。
我看到了一些在`impl`和`Add`上使用模板参数的示例,但它们只会导致不同的错误。
我发现如何为不同的RHS类型和返回值重载运算符?但是即使我在顶部放置了use std :: ops :: Mul;,答案中的代码也无效。
我正在使用rustc 1.0.0-nightly(ed530d7a3 2015-01-16 22:41:16 +0000)
我不会接受“你只有两个字段,为什么使用引用”作为答案;如果我想要一个100个元素的结构体呢?如果情况确实如此(我不认为是这样),我将接受证明即使有一个大结构体,我应该通过值传递的答案。我对于结构体大小和按值传递与结构体的良好经验法则感兴趣,但这不是当前的问题。

1
“如果我想要一个有100个元素的结构体怎么办” - Rust使用优化技术,如RVO,在适当时自动使用引用并作出更好的选择。 - Shepmaster
@Shepmaster:RVO 只会影响返回值,我通过值返回它。你能指出任何关于大结构体特征应该通过值实现的文档吗? - Jeremy Sorensen
2
我所知道的最好文档是有关返回指针的书籍章节。然而,我创建了一个添加大型结构体的示例,并检查了生成的LLVM(稍微清理了一下):(%struct.Big* sret, %struct.Big*, %struct.Big*)。我不自称是LLVM专家,但这看起来像是它自动通过引用进行接收和返回。 - Shepmaster
2
文档也提到了返回值,我认为它不应该是一个引用。事实上,该文档曾经说过除非你需要,否则不要使用指针作为输入参数,但这实际上已经被删除了。此外,我把你的例子改成了引用传递,并发现它移除了两个分配(%arg7 = alloca %struct.Big, align 8 and %arg8 = alloca %struct.Big, align 8),因此至少对于大型结构体而言,引用更好。 - Jeremy Sorensen
2
我应该指出,我对LLVM的了解比任何人都少,因此我的解释可能是错误的。另外,使用引用进行运算符重载的一个明显劣势是,如果你恰好没有引用,let c = (&a) + (&b);会非常麻烦。 - Jeremy Sorensen
2个回答

111

您需要在&Vector上实现Add,而不是在Vector上实现。

impl<'a, 'b> Add<&'b Vector> for &'a Vector {
    type Output = Vector;

    fn add(self, other: &'b Vector) -> Vector {
        Vector {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

在其定义中,Add :: add 始终按值获取 self 。但是,引用和其他类型一样也可以实现特征。当在引用类型上实现特征时,self 的类型是一个引用;该引用按值传递。通常,在Rust中按值传递意味着转移所有权,但是当引用按值传递时,它们只是被复制(如果是可变引用,则重新借用/移动),并且这不会转移被引用物的所有权(因为引用首先没有拥有其所引用的物)。考虑到所有这些,让 Add :: add (以及许多其他操作符)通过值接收self 是有道理的:如果您需要占有操作数,则可以直接在结构/枚举上实现 Add ,如果不需要,则可以在引用上实现 Add

这里, self 的类型是&'a Vector,因为这是我们正在实现 Add 的类型。

请注意,我还使用不同的生命周期指定了 RHS 类型参数,以强调两个输入参数的生命周期无关。


1实际上,引用类型很特殊,因为您可以为对在您的crate中定义的类型的引用实现特征(即,如果您被允许为 T 实现特征,则您也被允许为& T实现它)。& mut T Box <T>具有相同的行为,但是对于不在同一crate中定义的 U<T>来说通常并非如此。


4
"Add::add总是按值获取self参数。在这里,self的类型为'&a Vector',因为我们正在实现Add trait。这是关键信息,即self的类型取决于trait是否适用于引用类型。谢谢!" - Jeremy Sorensen
12
哇,惊讶于这是正确答案,但它确实是。这一切都感觉相当违反直觉。根据是否是引用,你可以用两种不同的方式来定义Add,这似乎是一个麻烦的源头。 - Squirrel
1
回应 @Squirrel 的评论,我们是否应该从另一个操作中实现一个操作以避免冗余?是通过引用添加复制值并应用按值添加,还是按值添加对移动的值应用按引用添加? - hsandt
1
是的,绝对没错!代码重用总是好的! :) 通常情况下,如果值很容易克隆/复制,则希望按值实现为主要实现;否则,按引用实现。 - Francis Gagné
2
实际上,如果一个是ref,另一个不是,反之亦然,您甚至需要实现所有组合。在Rust中,这是通过宏完成的... https://dev59.com/81kT5IYBdhLWcg3wStkf - Emmanuel Touzery
显示剩余5条评论

25

如果要支持所有场景,必须支持所有组合:

  • &T op U
  • T op &U
  • &T op &U
  • T op U

在rust proper中,这是通过一个内部宏来完成的。

幸运的是,有一个rust crate叫做impl_ops,提供了一个宏来为我们编写这个样板代码:该crate提供了impl_op_ex!宏,可以生成所有的组合。

这是他们的示例:

#[macro_use] extern crate impl_ops;
use std::ops;

impl_op_ex!(+ |a: &DonkeyKong, b: &DonkeyKong| -> i32 { a.bananas + b.bananas });

fn main() {
    let total_bananas = &DonkeyKong::new(2) + &DonkeyKong::new(4);
    assert_eq!(6, total_bananas);
    let total_bananas = &DonkeyKong::new(2) + DonkeyKong::new(4);
    assert_eq!(6, total_bananas);
    let total_bananas = DonkeyKong::new(2) + &DonkeyKong::new(4);
    assert_eq!(6, total_bananas);
    let total_bananas = DonkeyKong::new(2) + DonkeyKong::new(4);
    assert_eq!(6, total_bananas);
}

更好的是,它们拥有一个实现了 impl_op_ex_commutative 的功能! 如果您的运算符是可交换的,它还会生成带有反转参数的运算符。

看起来像是相当过时的箱子:哦,现在有类似的箱子吗? - David 天宇 Wong
看起来像是相当过时的货箱:o 现在有类似的货箱吗? - undefined
1
好的观点。我在他们的GitHub页面上看到:“注意:这个crate是稳定的,但不会再添加新功能。要获取更更新的版本,请参见auto_ops。”:https://github.com/carbotaniuman/auto_ops。那个也没有最近更新,但毕竟,有一种叫做“完成”的库。 - Emmanuel Touzery

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