Bỏ qua để đến nội dung

Lỗi Rust E0368: binary assignment operation += cannot be applied to type (không dùng được +=)

E0368 nghĩa là bạn dùng một toán tử kết hợp như +=, -= hay *= trên một kiểu chưa hỗ trợ nó. Đây là người anh em += của E0369. Struct của bạn không tự động có +=, kể cả khi nó đã hỗ trợ +. Cách sửa thường là một khối impl AddAssign ngắn, còn với số nằm sau một tham chiếu thì chỉ cần một dấu *. Các lỗi khác trong loạt bài này nằm ở lỗi trình biên dịch Rust.

error[E0368]: binary assignment operation `+=` cannot be applied to type `Money`
--> src/main.rs:8:5
|
8 | wallet += tip; // ❌ Money doesn't know how to do +=
| ------^^^^^^^
| |
| 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`

Trình biên dịch báo rằng không thể dùng += trên kiểu Money, vì Money chưa cài đặt AddAssign.

Đây là chương trình gây ra lỗi:

struct Money {
cents: i64,
}
fn main() {
let mut wallet = Money { cents: 500 };
let tip = Money { cents: 150 };
wallet += tip; // ❌ Money doesn't know how to do +=
println!("{}", wallet.cents);
}

Chú thích ở dòng 8 giữ tiếng Anh vì nó xuất hiện trong thông báo lỗi; nó nói rằng Money không biết cách làm +=.

Toán tử ở dòng đầu tiên có thể khác, nhưng vẫn là cùng một lỗi. p -= step đòi SubAssign, còn p *= 2 đòi MulAssign<{integer}>, nghĩa là “nhân rồi gán với một số nguyên”.

Nhãn dưới vế trái cho biết điều gì sai: “cannot use += on type Money” (không dùng được += trên kiểu Money). Sau đó dòng note: chỉ vào struct của bạn và nói nó phải cài đặt AddAssign. Đó là trait đứng sau +=.

Hãy nghĩ tới một con heo đất. Bỏ một đồng xu vào là một hành động rõ ràng. Nhưng nếu ai đó đưa bạn một cái hộp ghi “Tiền” và bảo “cộng cái hộp này vào nó”, bạn cần biết cộng thế nào: cộng số xu lại với nhau? chồng hai cái hộp lên nhau? Rust cần bạn viết quy tắc đó ra một lần.

Mỗi toán tử kết hợp thật ra là một lời gọi phương thức được ngụy trang. a += b gọi AddAssign::add_assign(&mut a, b), a -= b gọi SubAssign::sub_assign, v.v. Tất cả các trait này nằm trong std::ops. Các kiểu số đã cài đặt sẵn chúng. Struct mới của bạn thì chưa có cái nào, và không có #[derive] cho toán tử số học.

Đây là phần khiến nhiều người ngạc nhiên: Add và AddAssign là hai trait riêng biệt. Kể cả khi Point đã cài đặt Add, p += q vẫn gây E0368. Lý do là chúng làm những việc khác nhau. a + b nhận hai giá trị và trả về một giá trị mới. a += b thay đổi a tại chỗ qua &mut self và không trả về gì. Thay đổi tại chỗ có thể rẻ hơn nhiều: += của String nối thêm vào bộ đệm sẵn có thay vì tạo một chuỗi mới. Rust để mỗi kiểu tự chọn, nên nó không bao giờ lặng lẽ biến += thành + cho bạn.

Các trait toán tử được giải thích ở Các trait thông dụng và Trait nâng cao.

Cách sửa 1: Cài đặt AddAssign (và các trait anh em)

Phần tiêu đề “Cách sửa 1: Cài đặt AddAssign (và các trait anh em)”

Chọn cách này khi += trên kiểu của bạn có ý nghĩa rõ ràng, như cộng tiền hay dời một vị trí. Bạn viết một phương thức thay đổi self. Thêm SubAssign, MulAssign và các trait khác theo cùng cách khi cần:

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 }; // được tiền boa
wallet -= Money { cents: 200 }; // mua bữa trưa
println!("{:?}", wallet);
}

Kết quả:

Money { cents: 450 }
▶ Tự chạy thử

Để ý &mut self và không có kiểu trả về. Đó là toàn bộ khác biệt so với Add. Bên trong, self.cents += other.cents chạy được vì i64 đã có AddAssign.

Cách sửa 2: Đã có Add? Viết a = a + b, hoặc dùng lại nó

Phần tiêu đề “Cách sửa 2: Đã có Add? Viết a = a + b, hoặc dùng lại nó”

Chọn cách này khi kiểu của bạn đã cài đặt Add (có thể sau khi sửa E0369). Nếu kiểu là Copy, bạn có thể viết đơn giản pos = pos + step, chỉ cần Add. Hoặc cài đặt AddAssign trong một dòng bằng cách gọi Add của bạn, để += cũng chạy được:

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; // dùng lại Add
}
}
fn main() {
let mut pos = Point { x: 0, y: 0 };
let step = Point { x: 1, y: 2 };
pos = pos + step; // chỉ cần Add
pos += step; // cần AddAssign
println!("{:?}", pos);
}

Kết quả:

Point { x: 2, y: 4 }
▶ Tự chạy thử

*self = *self + other sao chép giá trị hiện tại ra, cộng, rồi ghi kết quả trở lại. Cách này chỉ chạy được vì Point là Copy. Với một kiểu sở hữu dữ liệu trên heap (như có một trường Vec), hãy viết phiên bản thay đổi tại chỗ như ở Cách sửa 1.

Chọn cách này khi vế trái là một chuỗi. += chạy được trên một String sở hữu, và vế phải phải là &str. Có hai trường hợp hay làm người ta vấp:

  • cannot be applied to type `&str`: vế trái là một slice chuỗi như "Hello, ". Một &str là góc nhìn chỉ đọc, nên nó không thể dài thêm. Hãy biến nó thành String trước.
  • s += t khi t cũng là String thì không phải E0368. Đó là E0308, “expected &str, found String”, kèm help s += &t. Chỉ cần mượn vế phải.
fn main() {
let mut greeting = String::from("Xin chào, "); // sở hữu, nên dài thêm được
let name = String::from("Ferris");
greeting += &name; // mượn vế phải
greeting.push_str("!"); // cùng việc đó, viết dưới dạng phương thức
let mut tag = "cua".to_string(); // &str ở vế trái? biến nó thành String
tag += " #1";
println!("{greeting}");
println!("{tag}");
println!("{name} vẫn dùng được");
}

Kết quả:

Xin chào, Ferris!
cua #1
Ferris vẫn dùng được
▶ Tự chạy thử

Vì vế phải chỉ được mượn, name vẫn là của bạn sau đó. += và push_str làm y hệt nhau; dùng cách nào đọc dễ hơn. Xem Chuỗi.

Cách sửa 4: Thêm ràng buộc T: AddAssign trong code generic

Phần tiêu đề “Cách sửa 4: Thêm ràng buộc T: AddAssign trong code generic”

Chọn cách này khi thông báo lỗi ghi cannot be applied to type `T`. Bên trong một hàm generic, T có thể là bất kỳ kiểu nào, kể cả kiểu không có +=. Dòng help của rustc nói chính xác cần thêm gì: consider further restricting type parameter `T` with trait `AddAssign` (hãy ràng buộc thêm tham số kiểu T bằng trait 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]));
}

Kết quả:

6
0.75
▶ Tự chạy thử

Giờ total chạy được với mọi kiểu có thể sao chép, có giá trị khởi đầu và hỗ trợ +=: số nguyên, số thực, và cả Money của bạn khi nó cài đặt các trait đó. Với số thuần, items.iter().sum() làm cùng việc chỉ trong một dòng. Xem Generic.

Đếm bằng HashMap. let count = counts.entry(word).or_insert(0); rồi count += 1; cho ra cannot be applied to type `&mut i32`. count là một tham chiếu, không phải con số. Dòng help của rustc: “+= can be used on i32 if you dereference the left-hand side” (+= dùng được trên i32 nếu bạn giải tham chiếu vế trái). Cách sửa: *count += 1;. Xem HashMap.

Nhân một struct với một số. p *= 2 cần MulAssign<{integer}>, vì vế phải là một con số, không phải một Point khác. Cách sửa: cài đặt trait cho đúng kiểu vế phải đó:

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

Cộng dồn trong vòng lặp. total += item.price khi price là kiểu Money của riêng bạn. Cách sửa 1 giúp nó chạy được. Nếu bạn thích viết prices.iter().copied().sum(), thì cái đó lại cần trait Sum, và nếu thiếu nó bạn sẽ gặp E0277 (“a value of type Money cannot be made by summing an iterator”).

Kiểm tra nhanh

1. Point đã cài đặt Add, nhưng p += q vẫn gây E0368. Vì sao?

2. count đến từ map.entry(word).or_insert(0). Dòng nào biên dịch được?

3. s và t đều là String. Bạn nối t vào s thế nào?

Điểm: 0 / 3