So khớp mẫu trong Rust
Ở bài Match, bạn đã học những điều cơ bản: các nhánh, _, gom giá trị bằng |, và khoảng. Đó mới chỉ là khởi đầu. Một mẫu (pattern) trong Rust còn có thể nhìn vào bên trong một giá trị, lấy ra những phần bạn cần, và kiểm tra thêm điều kiện — tất cả trong một dòng.
Hãy hình dung một mẫu như cái khuôn cắt bánh quy. Bạn ấn nó lên một giá trị. Nếu hình dạng vừa khít, Rust đưa cho bạn những mảnh bên trong. Nếu không vừa, Rust thử nhánh tiếp theo. Khi đã hiểu điều này, bạn sẽ dùng mẫu ở khắp nơi: trong match, trong let, trong tham số hàm, và trong if let.
Match guard: thêm một if
Phần tiêu đề “Match guard: thêm một if”Đôi khi hình dạng thôi là chưa đủ và bạn cần kiểm tra thêm. Một match guard là một điều kiện if viết sau mẫu. Nhánh chỉ chạy khi mẫu khớp và điều kiện là đúng.
fn main() { let temperatures = [-5, 0, 18, 35];
for t in temperatures { let feeling = match t { t if t < 0 => "lạnh cóng", 0 => "đúng bằng không", t if t < 25 => "dễ chịu", _ => "nóng", }; println!("{t}°C thì {feeling}"); }}Kết quả:
-5°C thì lạnh cóng0°C thì đúng bằng không18°C thì dễ chịu35°C thì nóngRust kiểm tra các nhánh từ trên xuống dưới, nên thứ tự rất quan trọng. 18 không qua được guard đầu tiên, không phải 0, và thỏa t < 25, nên nó là “dễ chịu”.
Phân rã tuple và struct
Phần tiêu đề “Phân rã tuple và struct”Phân rã (destructuring) nghĩa là tách một giá trị thành các phần của nó. Với một tuple, hãy viết mẫu theo đúng hình dạng của tuple. Với một struct, hãy gọi tên các trường bạn muốn.
struct Point { x: i32, y: i32,}
fn main() { let pair = (3, -7); match pair { (0, 0) => println!("gốc tọa độ"), (x, 0) => println!("trên trục x tại {x}"), (0, y) => println!("trên trục y tại {y}"), (x, y) => println!("ở chỗ khác: ({x}, {y})"), }
let p = Point { x: 0, y: 5 }; match p { Point { x: 0, y } => println!("trên trục y, y = {y}"), Point { x, y: 0 } => println!("trên trục x, x = {x}"), Point { x, y } => println!("tại ({x}, {y})"), }}Kết quả:
ở chỗ khác: (3, -7)trên trục y, y = 5Để ý sự kết hợp. 0 là một giá trị cố định phải khớp chính xác. x và y là những biến mới bắt lấy bất cứ thứ gì ở đó. Point { x, y } là cách viết tắt của Point { x: x, y: y }.
Phân rã enum có dữ liệu
Phần tiêu đề “Phân rã enum có dữ liệu”Đây là lúc mẫu thực sự tỏa sáng. Một biến thể (variant) của enum có thể mang theo dữ liệu, và một mẫu có thể mở dữ liệu đó ra trong lúc kiểm tra bạn đang có biến thể nào.
enum Shape { Circle { radius: f64 }, Rectangle { width: f64, height: f64 }, Square(f64),}
fn area(shape: &Shape) -> f64 { match shape { Shape::Circle { radius } => std::f64::consts::PI * radius * radius, Shape::Rectangle { width, height } => width * height, Shape::Square(side) => side * side, }}
fn main() { let shapes = [ Shape::Circle { radius: 2.0 }, Shape::Rectangle { width: 3.0, height: 4.5 }, Shape::Square(1.5), ]; for shape in &shapes { println!("diện tích = {:.2}", area(shape)); }}Kết quả:
diện tích = 12.57diện tích = 13.50diện tích = 2.25Bạn không thể chạm tới radius khi chưa chứng minh được giá trị là một Circle. Đó là cái lợi về an toàn: trình biên dịch không cho bạn đọc dữ liệu từ nhầm biến thể.
Mẫu lồng nhau và binding @
Phần tiêu đề “Mẫu lồng nhau và binding @”Mẫu có thể nằm bên trong mẫu khác, sâu bao nhiêu tùy thích. Ở đây một enum Command chứa một enum Direction, và một nhánh khớp cả hai tầng cùng lúc.
Một binding @ cho phép bạn kiểm tra một giá trị với một mẫu và giữ nó lại trong một biến. n @ 1..=9 nghĩa là “kiểm tra xem nó có nằm trong khoảng 1 đến 9 không, và gọi nó là n”.
enum Direction { Up, Down,}
enum Command { Move(Direction, u32), Say(String), Quit,}
fn run(cmd: Command) { match cmd { Command::Move(Direction::Up, steps @ 1..=9) => println!("lên {steps} (bước nhỏ)"), Command::Move(Direction::Up, steps) => println!("lên {steps} (cú nhảy lớn!)"), Command::Move(Direction::Down, steps) => println!("xuống {steps}"), Command::Say(text) => println!("đang nói: {text}"), Command::Quit => println!("tạm biệt"), }}
fn main() { run(Command::Move(Direction::Up, 3)); run(Command::Move(Direction::Up, 50)); run(Command::Move(Direction::Down, 2)); run(Command::Say(String::from("xin chào"))); run(Command::Quit);}Kết quả:
lên 3 (bước nhỏ)lên 50 (cú nhảy lớn!)xuống 2đang nói: xin chàotạm biệtKhông có @, một khoảng như 1..=9 kiểm tra giá trị nhưng rồi bỏ nó đi. Có @, bạn được giữ nó lại.
Bỏ qua một phần với _ và ..
Phần tiêu đề “Bỏ qua một phần với _ và ..”Thường thì bạn chỉ quan tâm đến một vài mảnh. Dùng _ để bỏ qua một giá trị. Dùng .. để bỏ qua “mọi thứ còn lại”.
struct Player { name: String, level: u32, gold: u32,}
fn main() { let rgb = (255, 128, 0); let (red, _, _) = rgb; println!("đỏ = {red}");
let numbers = (1, 2, 3, 4, 5); let (first, .., last) = numbers; println!("đầu = {first}, cuối = {last}");
let hero = Player { name: String::from("Mira"), level: 7, gold: 120 }; let Player { name, level, .. } = hero; println!("{name} đang ở cấp {level}");}Kết quả:
đỏ = 255đầu = 1, cuối = 5Mira đang ở cấp 7.. chỉ được xuất hiện một lần trong một mẫu. (first, .., last) thì được, nhưng (.., middle, ..) thì không — Rust không biết bạn muốn giá trị nào.
Mẫu trong let và tham số hàm
Phần tiêu đề “Mẫu trong let và tham số hàm”Mỗi câu lệnh let thực chất là một mẫu. Mỗi tham số hàm cũng vậy. Nghĩa là bạn có thể phân rã ngay tại chỗ giá trị được đưa vào.
fn print_coords(&(x, y): &(i32, i32)) { println!("x: {x}, y: {y}");}
fn main() { let (width, height) = (1920, 1080); println!("{width}x{height}");
print_coords(&(4, -2));
let points = vec![(1, 2), (3, 4), (5, 6)]; let sums: Vec<i32> = points.iter().map(|&(a, b)| a + b).collect(); println!("{:?}", sums);}Kết quả:
1920x1080x: 4, y: -2[3, 7, 11]Closure |&(a, b)| a + b dùng cùng ý tưởng. Mỗi phần tử là một &(i32, i32), và mẫu mở nó ra thẳng vào a và b. Bạn sẽ thấy điều này rất nhiều với iterator.
matches! và let-else
Phần tiêu đề “matches! và let-else”Hai công cụ tiện lợi giúp bạn khỏi phải viết cả một match.
matches!(giá_trị, mẫu) trả về true hoặc false. Nó hoàn hảo bên trong if hay .filter() khi bạn chỉ cần câu trả lời có/không. Guard cũng dùng được ở đây.
let-else xử lý tình huống “mở cái này ra, hoặc rời đi ngay”. Viết let MẪU = giá_trị else { ... };. Nếu mẫu khớp, các biến dùng được trong phần còn lại của hàm. Nếu không, khối else sẽ chạy, và nó bắt buộc phải rời đi — bằng return, break, continue, hoặc một panic.
fn parse_age(input: &str) -> Option<u32> { let Ok(age) = input.parse::<u32>() else { println!("'{input}' không phải là số"); return None; }; Some(age)}
fn main() { let grade = 'B'; println!("{}", matches!(grade, 'A' | 'B'));
let scores = [45, 72, 90, 38, 66]; let passed = scores.iter().filter(|s| matches!(s, 50..=100)).count(); println!("{passed} người đạt");
println!("{:?}", parse_age("31")); println!("{:?}", parse_age("ba mươi"));}Kết quả:
true3 người đạtSome(31)'ba mươi' không phải là sốNonelet-else giữ cho “con đường suôn sẻ” (happy path) luôn phẳng. Thay vì lồng code bên trong if let ... { }, bạn xử lý trường hợp xấu trước rồi đi tiếp. Hãy kết hợp nó với các giá trị Option và Result.
Lỗi thường gặp
Phần tiêu đề “Lỗi thường gặp”Guard không được tính khi kiểm tra “đã bao quát mọi trường hợp”. Trình biên dịch không thể suy luận về các điều kiện if của bạn, nên đoạn này lỗi dù hai guard đã bao quát mọi con số:
fn main() { let n: i32 = 7; match n { x if x < 0 => println!("số âm"), x if x >= 0 => println!("bằng không hoặc dương"), }}error[E0004]: non-exhaustive patterns: `i32::MIN..=i32::MAX` not covered --> src/main.rs:3:11 |3 | match n { | ^ pattern `i32::MIN..=i32::MAX` not covered | = note: the matched value is of type `i32` = note: match arms with guards don't count towards exhaustivityTrình biên dịch báo các mẫu chưa bao quát hết: toàn bộ khoảng i32::MIN..=i32::MAX chưa được xử lý, vì các nhánh có guard không được tính. Cách sửa: biến nhánh cuối thành một nhánh bắt mọi thứ đơn thuần, chẳng hạn _ => println!("bằng không hoặc dương").
Một tên biến trong mẫu là một biến mới, không phải một phép so sánh. Đoạn này trông như đang kiểm tra guess == expected, nhưng thực ra không phải:
fn main() { let expected = 3; let guess = 5; match guess { expected => println!("Đúng rồi!"), _ => println!("Sai"), }}Nó biên dịch được và in ra Đúng rồi!, vì expected trong nhánh chỉ tạo ra một biến mới bắt lấy bất kỳ giá trị nào. Rust cảnh báo bạn:
warning: unreachable pattern --> src/main.rs:6:9 |5 | expected => println!("Đúng rồi!"), | -------- matches any value6 | _ => println!("Sai"), | ^ no value can reach thisCảnh báo nói rằng nhánh _ không bao giờ được chạy tới, vì expected đã khớp với mọi giá trị. Cách sửa là dùng một guard: g if g == expected => println!("Đúng rồi!").
Luyện tập
Phần tiêu đề “Luyện tập”1. FizzBuzz với một tuple
Phần tiêu đề “1. FizzBuzz với một tuple”In các số từ 9 đến 15, nhưng in Fizz cho bội số của 3, Buzz cho bội số của 5, và FizzBuzz cho bội số của cả hai. Hãy làm bằng một match trên một tuple thay vì một chuỗi if. Gợi ý: match trên (n % 3, n % 5). Phần dư bằng 0 nghĩa là “chia hết”, còn _ nghĩa là “không quan tâm”.
Xem lời giải
fn main() { for n in 9..=15 { match (n % 3, n % 5) { (0, 0) => println!("FizzBuzz"), (0, _) => println!("Fizz"), (_, 0) => println!("Buzz"), _ => println!("{n}"), } }}Kết quả:
FizzBuzz11Fizz1314FizzBuzz(0, 0) phải đứng đầu tiên. Nếu (0, _) đứng trước, 15 sẽ in ra Fizz.
2. Giao dịch ngân hàng
Phần tiêu đề “2. Giao dịch ngân hàng”Định nghĩa enum Transaction { Deposit(u32), Withdraw(u32), Fee }. Bắt đầu balance ở 100, rồi áp dụng lần lượt danh sách này: gửi 50, rút 30, rút 500, phí. Một lần phí tốn 5. Một lần rút lớn hơn số dư phải bị từ chối kèm một thông báo. In ra số dư cuối cùng. Gợi ý: dùng một match guard, Transaction::Withdraw(amount) if amount <= balance, theo sau là một nhánh Withdraw thứ hai cho trường hợp bị từ chối.
Xem lời giải
enum Transaction { Deposit(u32), Withdraw(u32), Fee,}
fn main() { let mut balance: u32 = 100; let history = [ Transaction::Deposit(50), Transaction::Withdraw(30), Transaction::Withdraw(500), Transaction::Fee, ];
for t in history { match t { Transaction::Deposit(amount) => balance += amount, Transaction::Withdraw(amount) if amount <= balance => balance -= amount, Transaction::Withdraw(amount) => println!("bị từ chối: không thể rút {amount}"), Transaction::Fee => balance -= 5, } }
println!("số dư = {balance}");}Kết quả:
bị từ chối: không thể rút 500số dư = 115Nhánh Withdraw thứ hai không có guard, nên nó bắt mọi lần rút mà nhánh đầu đã từ chối. Không có nó, match sẽ không biên dịch được: guard không được tính khi Rust kiểm tra xem mọi trường hợp đã được bao quát chưa.
Tóm tắt
Phần tiêu đề “Tóm tắt”- Một match guard (
mẫu if điều_kiện) thêm một bước kiểm tra cho một nhánh. - Mẫu có thể phân rã tuple, struct và biến thể enum, và chúng có thể lồng nhau.
tên @ mẫukiểm tra một giá trị và đồng thời giữ nó trong một biến._bỏ qua một giá trị;..bỏ qua phần còn lại. Mẫu cũng dùng được tronglet, tham số hàm và closure.matches!cho nhanh mộttrue/false; let-else mở một giá trị ra hoặc rời đi sớm.
Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Kiểm tra nhanh
1. n @ 1..=9 làm gì trong một nhánh match?
2. Trong let-else, khối else bắt buộc phải làm gì?
3. let (first, .., last) = (1, 2, 3, 4, 5); cho bạn kết quả gì?
4. Vì sao một match chỉ có các nhánh có guard thường không biên dịch được?
Điểm: 0 / 4
Đọc thêm
Phần tiêu đề “Đọc thêm”- Patterns and Matching — The Rust Book (tiếng Anh): chương chính thức về mọi chỗ mẫu có thể xuất hiện và khi nào chúng có thể không khớp.
- Pattern Syntax — The Rust Book (tiếng Anh): đi qua mọi dạng mẫu, từ khoảng và phân rã đến guard và binding
@. - Staying on the Happy Path with let…else — The Rust Book (tiếng Anh): một ví dụ nhẹ nhàng, chi tiết về let-else.
- Destructuring — Rust by Example (tiếng Anh): ví dụ chạy được về phân rã tuple, mảng, enum, con trỏ và struct.
matches!macro — std docs (tiếng Anh): cú pháp chính xác của macro, kể cả cách thêm guard.