跳转到内容

Rust 错误 E0368:binary assignment operation += cannot be applied to type(不支持 +=)

E0368 的意思是:你对一个还不支持复合运算符的类型用了 +=、-= 或 *= 这样的运算符。它是 E0369 的 += 版“表亲”。你自己定义的结构体不会自动获得 +=,即使它已经支持 + 也不会。修法通常是一个简短的 impl AddAssign 块;如果是引用背后的数字,只需要加一个 *。这个系列的其他错误,请看 Rust 编译错误详解。

error[E0368]: binary assignment operation `+=` cannot be applied to type `Money`
--> src/main.rs:8:5
|
8 | wallet += tip; // ❌ Money 不知道怎么做 +=
| ------^^^^^^^
| |
| cannot use `+=` on type `Money`
|
note: an implementation of `AddAssign` might be missing for `Money`
--> src/main.rs:1:1
|
1 | struct Money {
| ^^^^^^^^^^^^ must implement `AddAssign`

第一行的意思是“二元赋值运算 += 不能用在 Money 类型上”。note 说 Money 可能缺少 AddAssign 的实现。

产生这个错误的程序是这样的:

struct Money {
cents: i64,
}
fn main() {
let mut wallet = Money { cents: 500 };
let tip = Money { cents: 150 };
wallet += tip; // ❌ Money 不知道怎么做 +=
println!("{}", wallet.cents);
}

第一行里的运算符会变,但错误是同一个。p -= step 要求 SubAssign,p *= 2 要求 MulAssign<{integer}>,意思是“乘以一个整数并赋值”。

左边下面的标签说明了哪里出了问题:“cannot use += on type Money”(不能对 Money 类型使用 +=)。接着 note: 指向你的结构体,说它必须实现 AddAssign(must implement AddAssign)。这就是 += 背后的 trait。

想象一个存钱罐。往里投一枚硬币,这个动作很清楚。但如果有人递给你一个标着“Money”的盒子,说“把另一个盒子加到它上面”,你就得知道该怎么加:把分(cents)相加?还是把盒子摞起来?Rust 需要你把这条规则写下来,写一次就够。

每个复合运算符其实都是一次伪装起来的方法调用。a += b 调用的是 AddAssign::add_assign(&mut a, b),a -= b 调用 SubAssign::sub_assign,以此类推。这些 trait 都在 std::ops 里。数字已经实现了它们。你新写的结构体一个都没有,而且数学运算符没有 #[derive] 可用。

最让人意外的是这一点:Add 和 AddAssign 是两个独立的 trait。即使 Point 已经实现了 Add,p += q 照样报 E0368。原因是它们做的事不一样。a + b 接收两个值,返回一个新值。a += b 通过 &mut self 原地修改 a,什么也不返回。原地修改可能便宜得多:String 的 += 会追加到已有的缓冲区,而不是新建一个字符串。Rust 让每个类型自己选择,所以它从不会悄悄地把 += 替你换成 +。

运算符 trait 在常用 trait和高级 trait里有讲解。

修复 1:实现 AddAssign(以及它的同伴们)

Section titled “修复 1:实现 AddAssign(以及它的同伴们)”

当你的类型上的 += 有明确的含义(比如加钱或移动位置)时,选这个方法。你只需写一个修改 self 的方法。需要 SubAssign、MulAssign 等其他运算符时,也照同样的方式添加:

use std::ops::{AddAssign, SubAssign};
#[derive(Debug)]
struct Money {
cents: i64,
}
impl AddAssign for Money {
fn add_assign(&mut self, other: Money) {
self.cents += other.cents;
}
}
impl SubAssign for Money {
fn sub_assign(&mut self, other: Money) {
self.cents -= other.cents;
}
}
fn main() {
let mut wallet = Money { cents: 500 };
wallet += Money { cents: 150 }; // 收到小费
wallet -= Money { cents: 200 }; // 买了午饭
println!("{:?}", wallet);
}

输出:

Money { cents: 450 }
▶ 动手试一试

注意 &mut self,而且没有返回类型。这就是它和 Add 的全部区别。在方法内部,self.cents += other.cents 能用,是因为 i64 已经实现了 AddAssign。

修复 2:已经有 Add 了?写 a = a + b,或者复用它

Section titled “修复 2:已经有 Add 了?写 a = a + b,或者复用它”

当你的类型已经实现了 Add(也许是在修好 E0369 之后)时,选这个方法。如果这个类型是 Copy 的,直接写 pos = pos + step 就行,它只需要 Add。或者用一行调用你的 Add 来实现 AddAssign,这样 += 也能用了:

use std::ops::{Add, AddAssign};
#[derive(Debug, Clone, Copy)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point { x: self.x + other.x, y: self.y + other.y }
}
}
impl AddAssign for Point {
fn add_assign(&mut self, other: Point) {
*self = *self + other; // 复用 Add
}
}
fn main() {
let mut pos = Point { x: 0, y: 0 };
let step = Point { x: 1, y: 2 };
pos = pos + step; // 只需要 Add
pos += step; // 需要 AddAssign
println!("{:?}", pos);
}

输出:

Point { x: 2, y: 4 }
▶ 动手试一试

*self = *self + other 先把当前的值复制出来,相加,再把结果写回去。这只有在 Point 是 Copy 时才行得通。对于拥有堆数据的类型(比如带 Vec 字段的),请改用修复 1 那种原地修改的写法。

修复 3:字符串:s += &t 或 push_str

Section titled “修复 3:字符串:s += &t 或 push_str”

当左边是字符串时,选这个方法。+= 可以用在拥有所有权的 String 上,而右边必须是 &str。有两种情况经常让人栽跟头:

  • cannot be applied to type `&str`:左边是一个字符串 slice,比如 "Hello, "。&str 是只读的视图,没法变长。先把它变成 String。
  • 当 t 也是 String 时,s += t 报的不是 E0368,而是 E0308,“expected &str, found String”(期望 &str,却得到 String),并附带提示 s += &t。借用右边就好。
fn main() {
let mut greeting = String::from("你好,"); // 拥有所有权,所以可以变长
let name = String::from("Ferris");
greeting += &name; // 借用右边
greeting.push_str("!"); // 同样的事,写成方法
let mut tag = "crab".to_string(); // 左边是 &str?把它变成 String
tag += " #1";
println!("{greeting}");
println!("{tag}");
println!("{name} 仍然可以使用");
}

输出:

你好,Ferris!
crab #1
Ferris 仍然可以使用
▶ 动手试一试

因为右边只是被借用,之后 name 仍然归你。+= 和 push_str 做的事完全一样,哪个读起来顺就用哪个。参见让 String 变长。

修复 4:在泛型代码里加上 T: AddAssign 约束

Section titled “修复 4:在泛型代码里加上 T: AddAssign 约束”

当错误说 cannot be applied to type `T` 时,选这个方法。在泛型函数里,T 可以是任何类型,包括不支持 += 的类型。rustc 的 help 会准确告诉你要加什么:consider further restricting type parameter `T` with trait `AddAssign`(考虑用 trait AddAssign 进一步约束类型参数 T)。

use std::ops::AddAssign;
fn total<T>(items: &[T]) -> T
where
T: Copy + Default + AddAssign,
{
let mut sum = T::default();
for &item in items {
sum += item;
}
sum
}
fn main() {
println!("{}", total(&[1, 2, 3]));
println!("{}", total(&[0.5, 0.25]));
}

输出:

6
0.75
▶ 动手试一试

现在 total 适用于任何能复制、有初始值、支持 += 的类型:整数、浮点数,以及实现了这些 trait 之后的你自己的 Money。如果只是普通数字,items.iter().sum() 一行就能做同样的事。参见泛型函数。

用 HashMap 计数。 先写 let count = counts.entry(word).or_insert(0);,再写 count += 1;,就会报 cannot be applied to type `&mut i32`。count 是一个引用,不是数字本身。rustc 的 help 是:“+= can be used on i32 if you dereference the left-hand side”(如果对左边解引用,就可以对 i32 使用 +=)。修法:*count += 1;。参见更新值。

把结构体乘以一个数。 p *= 2 需要 MulAssign<{integer}>,因为右边是一个数字,而不是另一个 Point。修法:为这个右侧类型实现 trait:

impl std::ops::MulAssign<i32> for Point {
fn mul_assign(&mut self, factor: i32) {
self.x *= factor;
self.y *= factor;
}
}

在循环里累加。 total += item.price,而 price 是你自己的 Money 类型。修复 1 就能让它工作。如果你更想写 prices.iter().copied().sum(),那需要的是 Sum trait,没有它你会得到 E0277(“a value of type Money cannot be made by summing an iterator”,无法通过对迭代器求和得到 Money 类型的值)。

小测验

1. Point 实现了 Add,但 p += q 仍然报 E0368。为什么?

2. count 来自 map.entry(word).or_insert(0)。哪一行能编译?

3. s 和 t 都是 String。怎么把 t 追加到 s 后面?

得分: 0 / 3