(Rust)是否有一种方法可以自动借用重载运算符?

3

我正在尝试在Rust中实现一个基本的多项式类型,并通过重载运算符来进行数学运算。到目前为止,我已经成功地实现了加法、减法等基本运算。问题是,我希望通过引用来获取操作数,以避免不必要的复制。例如:

impl ops::Add for &Polynomial {
    type Output = Polynomial;
    fn add(self, other: Self) -> Polynomial {
        let mut result = self.clone();
        for (power, coeff) in other.coefficients.iter().enumerate() {
            result.add_coeff(power, *coeff);
        }
        result
    }
}
impl ops::Sub for &Polynomial {
    //  Implementation
    //  ...
}
impl ops::Mul for &Polynomial {
    //  Implementation
    //  ...
}
//  Etc.

这种方法可以按照预期工作,但是它要求用户每次都显式地借用操作数,随着表达式变得更加复杂,代码的可读性变得降低:

let (quotient, remainder) = p2.div_rem(&p1);

assert_eq!(&(&quotient * &p1) + &remainder, p2);

//  Even worse example found on the internet.
//  Note that you need to explicitly borrow each and every intermediate result :
let result = &(&(&a * &b) + &(&c * &d)) / 2;

我认为,与其写最后两行代码,如果我可以只写以下代码,那么代码会更加简洁易读:

assert_eq!(quotient * p1 + remainder, p2);

let result = (a * b + c * d) / 2;

我希望能够让借用发生“自动化”(就像在任何值上调用具有&self作为其第一个参数的方法时一样:它会自动且隐式地被借用,而不必显式地进行借用)。

然而,我不知道是否可能实现,或者如何实现?

3个回答

4
据我所知,没有简单的方法来实现这个,因为 Rust 引用始终是显式的,从未隐式,这是由于设计选择(即避免 C++ 引用的相同设计问题,这些引用很奇怪)和必要性(由于借用检查器)所决定的。
`T, &T, &mut T, *const T` 等都是具有不同语义的不同类型,因此允许 `impl ops::Add for &Polynomial` 也适用于 `Polynomial` 将基本意味着 `impl ops::Add for &T` 将自动实现 `ops::Add` 以支持 `T`,这可能会导致非常奇怪且潜在不安全的结果,特别是如果反过来也是如此。那么 `&mut` 又该怎么办呢? `&mut T` 自动转换为 `&`,对借用本身的生命周期有什么影响呢?
另外,在非 `Copy` 值类型上实现 `ops::Add` 是相当奇怪的,因为运算符会导致其操作数被移动,这绝对不是大多数人使用 `+` 时期望的结果。

是的,这是其中一种理论上的理想(“我希望在Polynomial上有+-”)与实际情况(但它们不是Copy,所以很奇怪)相冲突的情况之一。由于Rust是一种低级语言,它更倾向于实际情况的一面。 - Silvio Mayolo
谢谢你的回答。不过,我不确定我理解你最后一段的意思。当人们添加两个值时,比如 let c = a+b;,他们期望的是在操作之后,值 ab 仍然可用,并且没有被消耗掉(这样你可以在同一个表达式中两次使用相同的值,比如 let c = a*a;)。但是,我不明白为什么它会排除非复制类型,如果我们可以通过引用将值传递给运算符呢?实际上,当我看到常见的运算符(比如 +、-、*、/、%)一开始并不接受引用时,我感到很惊讶,原因就是这个。 - Jor
此外,在非 Copy 类型上实现 ops::Add 是相当奇怪的,因为该运算符会导致操作数被移动,这绝对不是大多数人在使用 + 时所期望的情况。 - 这是不正确的,bigint crate 倾向于同时实现加法和减法,并且确实会移动数据。 - user4815162342
请注意,String 是一个非Copy类型,它实现了 Add,将左操作数按值传递,右操作数按引用传递。 - L. F.
@user4815162342 实现这两个操作并不改变我所说的 - 它仍然使得它变得奇怪,因为通常人们期望在进行加法或减法运算后,两个操作数仍然可用。在大多数人的心目中,它们不是破坏性的运算符,并且他们本能地期望它们的行为就像它们定义在引用上一样。 - mcilloni
显示剩余5条评论

2
标准的方法是为引用和值定义算术运算。这对于实现来说可能有点繁琐(通常需要使用宏来减少代码重复),但可以为API用户提供最大的可用性。
用户可以编写常规代码,而无需额外的借用,但也可以选择在稍后使用时借用(从而避免不必要的复制)。例如,使用 num_bigint crate,所有这些都可以工作:
    assert!(BigUint::from(1u8) + BigUint::from(1u8) == BigUint::from(2u8));
    assert!(&BigUint::from(1u8) + &BigUint::from(1u8) == BigUint::from(2u8));
    assert!(BigUint::from(1u8) + &BigUint::from(1u8) == BigUint::from(2u8));
    assert!(&BigUint::from(1u8) + BigUint::from(1u8) == BigUint::from(2u8));

1

您可以使用std::borrow::Borrow来实现当右操作符为借用或移动时的运算符,这样可以解决大部分问题。这是因为Borrow<T>对于&T有一个默认实现,实际上什么也不做。不幸的是,您不能为另一个trait实现一个trait。因此,您需要为左操作符为借用和移动的情况进行impl

impl <Rhs> std::ops::Add<Rhs> for Polynomial
where
    Rhs: std::borrow::Borrow<Polynomial>
{
    type Output = Polynomial;

    fn add(self, rhs: Rhs) -> Self::Output {
        // Just call the other impl by borrowing self.
        &self + rhs
    }
}

impl <Rhs> std::ops::Add<Rhs> for &Polynomial
where
    Rhs: std::borrow::Borrow<Polynomial>
{
    type Output = Polynomial;

    fn add(self, rhs: Rhs) -> Self::Output {
        let rhs = rhs.borrow();

        // Now both self and rhs are &Polynomial. Implement add.
    }
}

你肯定可以为这个写一个宏。 - leo848

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