コンテンツにスキップ

Rustのエラー E0368: binary assignment operation += cannot be applied to type(+= が使えない)

E0368 は、+=、-=、*= のような複合代入演算子を、まだそれに対応していない型に使った、という意味のエラーです。E0369 の += 版のいとこのようなものです。自分で作った構造体は、+= を自動では使えるようになりません。+ にすでに対応していても、です。たいていは短い impl AddAssign ブロックで直りますし、参照の先にある数値なら * を1つ付けるだけです。このシリーズのほかのエラーは、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);
}

1行目の演算子は変わることがありますが、同じエラーです。p -= step なら SubAssign が求められ、p *= 2 なら MulAssign<{integer}> が求められます。これは「整数で掛け算して代入する」という意味です。

左側の下にあるラベルが、何がいけなかったのかを教えてくれます。「cannot use += on type Money」(型 Money には += を使えない)です。続く note: はあなたの構造体を指して、AddAssign を実装しなければならない(must implement AddAssign)と言っています。これが += の裏にあるトレイトです。

貯金箱を思い浮かべてみてください。硬貨を1枚入れるのは、はっきりした動作です。でも「Money」と書かれた箱を渡されて、「この別の箱を足しておいて」と言われたら、どうすればいいのか知る必要があります。セントを足し合わせるのか、箱を積み重ねるのか。Rust は、そのルールを一度書いておいてほしいのです。

複合代入演算子はどれも、メソッド呼び出しが姿を変えたものです。a += b は AddAssign::add_assign(&mut a, b) を呼び、a -= b は SubAssign::sub_assign を呼ぶ、というように続きます。これらのトレイトはすべて std::ops にあります。数値はもう実装していますが、新しく作った構造体はどれも持っていない状態から始まります。しかも、算術演算子には #[derive] がありません。

多くの人が驚くのがここです。Add と AddAssign は別々のトレイトなのです。Point が Add を実装していても、p += q はやはり E0368 になります。理由は、この2つが違う仕事をするからです。a + b は2つの値を受け取って新しい値を返します。a += b は &mut self を通して a をその場で書き換え、何も返しません。その場で書き換えるほうが、ずっと安く済むことがあります。String の += は、新しい文字列を作るのではなく、今あるバッファの後ろに付け足します。Rust はそれぞれの型に選ばせるので、+= を黙って + に置き換えたりはしません。

演算子のトレイトは、よく使うトレイトと高度なトレイトで説明しています。

直し方1:AddAssign(とその仲間)を実装する

Section titled “直し方1:AddAssign(とその仲間)を実装する”

自分の型で += にはっきりした意味があるとき(お金を足す、位置を動かすなど)は、これを選びます。self を書き換えるメソッドを1つ書くだけです。必要なら、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 を呼ぶ1行で 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 でなければなりません。よくつまずくケースが2つあります。

  • cannot be applied to type `&str`:左側が "Hello, " のような文字列スライスになっています。&str は読み取り専用のビューなので、伸ばすことができません。先に String にしましょう。
  • t も String のときの s += t は E0368 ではありません。これは E0308 の「expected &str, found String」で、help として 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 はまったく同じことをします。読みやすいほうを使いましょう。文字列を見てください。

直し方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`(型パラメータ T をトレイト AddAssign でさらに制限してはどうか)です。

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 は、コピーできて、初期値があって、+= に対応している型なら何にでも使えます。整数、浮動小数点数、そしてこれらのトレイトを実装すれば自分の Money 型にもです。ただの数値なら、items.iter().sum() の1行で同じことができます。ジェネリクスを見てください。

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; と書きます。HashMapを見てください。

構造体を数で拡大するとき。 p *= 2 には MulAssign<{integer}> が必要です。右側が別の Point ではなく数値だからです。直し方:その右側の型に対してトレイトを実装します。

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

ループで合計を積み上げるとき。 price が自分の Money 型のときの total += item.price です。直し方1で動くようになります。prices.iter().copied().sum() と書きたいなら、代わりに Sum トレイトが必要です。それがないと 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 です。s の後ろに t を付け足すにはどう書きますか?

スコア: 0 / 3