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

Lỗi Rust E0005: refutable pattern in local binding (mẫu có thể không khớp)

E0005 nghĩa là bạn viết một câu let có mẫu (pattern) có thể không khớp, như let Some(x) = value; trong khi value có thể là None. Rust cần biết chuyện gì xảy ra trong trường hợp đó, và một let thông thường không có chỗ nào để nói điều này. Cách sửa là cho trường hợp “không khớp” một lối đi: thường là let ... else, if let hoặc match. Đây là lỗi rất hay gặp khi bạn bắt đầu làm việc với Option và Result, và chỉ mất một phút để sửa. Các lỗi khác trong loạt bài này có ở Giải thích các lỗi trình biên dịch Rust.

error[E0005]: refutable pattern in local binding
--> src/main.rs:4:9
|
4 | let Some(top) = best; // ❌ what if the list was empty?
| ^^^^^^^^^ pattern `None` not covered
|
= note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
= note: for more information, visit https://doc.rust-lang.org/book/ch19-02-refutability.html
= note: the matched value is of type `Option<i32>`
help: you might want to use `let...else` to handle the variant that isn't matched
|
4 | let Some(top) = best else { todo!() }; // ❌ what if the list was empty?
| ++++++++++++++++

Trình biên dịch báo rằng mẫu Some(top) không bao trường hợp None, rằng let cần một mẫu luôn khớp, và gợi ý dùng let...else. Đây là chương trình gây ra lỗi:

fn main() {
let scores = vec![72, 95, 88];
let best = scores.iter().max().copied();
let Some(top) = best; // ❌ what if the list was empty?
println!("Top score: {top}");
}

Bạn sẽ gặp cùng lỗi này với các mẫu khác. Chỉ phần “not covered” (chưa được bao) thay đổi:

3 | let Ok(age) = input.parse::<i32>();
| ^^^^^^^ pattern `Err(_)` not covered
3 | let [host, port] = parts.as_slice();
| ^^^^^^^^^^^^ patterns `&[]`, `&[_]` and `&[_, _, _, ..]` not covered

Có hai từ quan trọng ở đây:

  • Một mẫu refutable (có thể bác bỏ) là mẫu có thể không khớp. Some(top) không khớp khi giá trị là None. Ok(age) không khớp với Err. [host, port] không khớp khi slice không có đúng hai phần tử.
  • Một mẫu irrefutable (không thể bác bỏ) luôn khớp. Một cái tên đơn giản như let top = best; luôn chạy được, tuple và struct cũng vậy, như let (x, y) = point;.

Một let phải luôn thành công, vì dòng tiếp theo sẽ dùng các biến mà nó tạo ra. Nếu best là None, thì ở dòng tiếp theo top sẽ là gì? Không có câu trả lời, nên Rust chặn bạn lại. Nhãn “not covered” liệt kê chính xác những trường hợp bạn đã quên: None, Err(_), hoặc với slice là “rỗng”, “một phần tử” và “ba phần tử trở lên”.

Hãy nghĩ đến một tủ nhận bưu kiện. let Some(top) = best; giống như bỏ đi mà đinh ninh có bưu kiện bên trong, không hề mở cửa tủ. Rust bắt bạn quyết định làm gì nếu tủ trống.

let, tham số hàm và vòng lặp for đều cần mẫu không thể thất bại. match, if let, while let và let ... else chấp nhận mẫu có thể thất bại, vì mỗi cái đều có chỗ cho trường hợp còn lại. Đây chính là quy tắc của E0004, nơi một match phải bao mọi giá trị. Ở đây “match” là một let chỉ có một nhánh.

Trong nhiều ngôn ngữ, đọc một giá trị không tồn tại sẽ cho bạn null, undefined hoặc sập chương trình lúc chạy. Trong Rust, một danh sách rỗng, một con số sai trong ô nhập liệu hay một thiết lập bị thiếu đều là những trường hợp bình thường mà bạn xử lý trong code. Option và Result làm chúng hiện rõ ra, còn E0005 đảm bảo bạn không vô tình bỏ qua chúng. Xem Mẫu trong let và Option.

Chọn cách này khi phần còn lại của hàm cần giá trị đó, và thiếu nó thì chẳng còn gì hữu ích để làm. Đây là điều mà dòng help: của rustc gợi ý. Bạn giữ nguyên mẫu và thêm một khối else. Khối đó phải thoát ra: return, break, continue hoặc một panic.

fn port_from(text: &str) -> u16 {
// lấy con số ra, hoặc thoát khỏi hàm sớm
let Ok(port) = text.trim().parse::<u16>() else {
println!("'{text}' không phải là cổng hợp lệ, dùng 8080");
return 8080;
};
port
}
fn main() {
let scores = vec![72, 95, 88];
let Some(top) = scores.iter().max() else {
println!("Chưa có điểm nào");
return;
};
println!("Điểm cao nhất: {top}");
println!("Cổng: {}", port_from("3000"));
println!("Cổng: {}", port_from("abc"));
}

Kết quả:

Điểm cao nhất: 95
Cổng: 3000
'abc' không phải là cổng hợp lệ, dùng 8080
Cổng: 8080
▶ Tự chạy thử

Đừng giữ nguyên else { todo!() } của rustc. todo!() sẽ panic khi chạy tới, nên hãy thay nó bằng một lối thoát thật sự. Xem thêm ở let-else.

Cách sửa 2: match hoặc if let khi cả hai trường hợp đều có việc

Phần tiêu đề “Cách sửa 2: match hoặc if let khi cả hai trường hợp đều có việc”

Chọn cách này khi trường hợp “không khớp” có việc riêng phải làm, như in lỗi ra, hoặc khi code vẫn tiếp tục trong cả hai trường hợp. match cho phép bạn xử lý mọi trường hợp và dùng giá trị lỗi. Đó cũng là cách để tách một slice khi bạn không biết độ dài của nó. Dùng if let khi chỉ một trường hợp là quan trọng:

fn main() {
// match: xử lý cả hai kết quả, và dùng giá trị lỗi
for input in ["42", "forty-two"] {
match input.parse::<i32>() {
Ok(age) => println!("Tuổi: {age}"),
Err(e) => println!("Không đọc được '{input}': {e}"),
}
}
// một slice có thể dài bao nhiêu cũng được: match theo hình dạng của nó
for address in ["localhost:8080", "localhost"] {
let parts: Vec<&str> = address.split(':').collect();
match parts.as_slice() {
[host, port] => println!("host = {host}, port = {port}"),
_ => println!("'{address}' phải có dạng host:port"),
}
}
// if let: chỉ làm gì đó khi có giá trị
let nickname: Option<&str> = None;
if let Some(name) = nickname {
println!("Chào {name}!");
} else {
println!("Chào người lạ!");
}
}

Kết quả:

Tuổi: 42
Không đọc được 'forty-two': invalid digit found in string
host = localhost, port = 8080
'localhost' phải có dạng host:port
Chào người lạ!
▶ Tự chạy thử

Thông báo invalid digit found in string (có chữ số không hợp lệ trong chuỗi) là lỗi của chính Rust nên vẫn là tiếng Anh. Nhánh _ bắt mọi độ dài còn lại. Không có nó, match sẽ cho bạn E0004 với cùng danh sách “not covered”. Xem if let và match.

Cách sửa 3: Giá trị mặc định với unwrap_or, hoặc chuyển tiếp bằng ?

Phần tiêu đề “Cách sửa 3: Giá trị mặc định với unwrap_or, hoặc chuyển tiếp bằng ?”

Chọn cách này khi có một giá trị mặc định hợp lý, hoặc khi bên gọi nên tự xử lý vấn đề. unwrap_or(x) cho bạn giá trị, hoặc x nếu không có. ? dùng được bên trong hàm trả về Result (hoặc Option): nó trả lỗi về cho bên gọi và dừng hàm.

use std::num::ParseIntError;
// ? chuyển lỗi cho bên gọi thay vì xử lý ở đây
fn total_minutes(hours: &str, minutes: &str) -> Result<i32, ParseIntError> {
let h = hours.parse::<i32>()?;
let m = minutes.parse::<i32>()?;
Ok(h * 60 + m)
}
fn main() {
// một giá trị mặc định khi không có gì
let scores: Vec<i32> = Vec::new();
let top = scores.iter().max().copied().unwrap_or(0);
println!("Điểm cao nhất: {top}");
let volume = "loud".parse::<u8>().unwrap_or(50);
println!("Âm lượng: {volume}");
println!("{:?}", total_minutes("2", "30"));
println!("{:?}", total_minutes("2", "half"));
}

Kết quả:

Điểm cao nhất: 0
Âm lượng: 50
Ok(150)
Err(ParseIntError { kind: InvalidDigit })
▶ Tự chạy thử

Nếu bạn chắc chắn giá trị có ở đó (ví dụ một con số bạn vừa viết trong code), .expect("lý do") cũng ổn. Nó panic với thông điệp của bạn nếu bạn đã sai, nên hãy để dành nó cho những trường hợp thật sự không thể thất bại. Dùng ? trong một main không trả về gì sẽ cho ra E0277; xem Toán tử ? và Lối tắt: unwrap_or.

Cách sửa 4: Dùng một kiểu mà mẫu của nó không thể thất bại

Phần tiêu đề “Cách sửa 4: Dùng một kiểu mà mẫu của nó không thể thất bại”

Chọn cách này khi dữ liệu luôn có cùng một hình dạng, và mẫu chỉ thất bại vì kiểu mà bạn đã chọn. Một slice hay Vec có thể dài bao nhiêu cũng được, nhưng một mảng (array) như [u8; 3] luôn có đúng ba phần tử, nên let [r, g, b] = color; luôn khớp. Tuple và struct cũng luôn khớp. Còn split_once trả về cả hai nửa trong một tuple, nên bạn chỉ phải xử lý một Option thay vì một slice không rõ độ dài:

struct Point {
x: i32,
y: i32,
}
fn main() {
// mảng có độ dài cố định: mẫu này luôn khớp
let color: [u8; 3] = [255, 140, 0];
let [r, g, b] = color;
println!("r={r} g={g} b={b}");
// tuple và struct cũng luôn khớp
let (host, port) = ("localhost", 8080);
let Point { x, y } = Point { x: 3, y: -1 };
println!("{host}:{port} tại ({x}, {y})");
// split_once trả về một Option chứa cả hai nửa
let Some((key, value)) = "theme=dark".split_once('=') else {
return;
};
println!("{key} -> {value}");
}

Kết quả:

r=255 g=140 b=0
localhost:8080 tại (3, -1)
theme -> dark
▶ Tự chạy thử

Enum của riêng bạn cũng vậy: let Shape::Circle(radius) = shape; lỗi với pattern `Shape::Square(_)` not covered. Nếu giá trị thật sự chỉ có thể là hình tròn, có lẽ nó nên là một struct Circle thay vì một enum. Xem Mảng và Struct.

Vòng lặp for qua các Option. for Some(temp) in readings cho ra “refutable pattern in for loop binding”. Cách sửa: for temp in readings.iter().flatten() bỏ qua các None, hoặc lặp qua mọi phần tử và dùng if let Some(t) = reading bên trong.

Một tham số hàm. fn greet(Some(name): Option<&str>) cho ra “refutable pattern in function argument”. Cách sửa: nhận name: Option<&str> và dùng match bên trong hàm, hoặc nhận name: &str và để bên gọi quyết định làm gì với None.

Phân tích dữ liệu nhập. let Ok(n) = line.trim().parse::<i32>(); khi đọc dữ liệu người dùng nhập hoặc đối số dòng lệnh. Cách sửa: let Ok(n) = ... else { println!("Hãy nhập một con số"); continue; }; bên trong vòng lặp nhập liệu, hoặc dùng match để in lỗi ra.

Kiểm tra nhanh

1. Câu let nào cho ra E0005?

2. Khối else của một let ... else phải làm gì?

3. Bạn cần một con số từ văn bản, và 0 là giá trị mặc định ổn nếu văn bản không phải số. Cách sửa ngắn nhất là gì?

Điểm: 0 / 3