Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Hiểu or-pattern trong Rust: dấu
|đặt giữa các pattern trong một armmatchđể gom nhiều giá trị vào cùng một code block, ví dụ1 | 2 | 3 => "small". - Biết khi nào nên dùng
|để tránh duplicate arm body và khi nào tách arm riêng lại rõ hơn — không phải lúc nào gom cũng là tốt. - Kết hợp được
|vớirange patternđã học ở Bài 101:1 | 5..=10 | 100 => "special"— một arm phủ nhiều "khoảng" rời nhau. - Áp dụng
|cho enum variant cùng nhóm (HTTP success, retry-able error) và cho tuple pattern ((1, _) | (_, 1)). - Nắm chắc quy tắc binding mọi vế phải cùng tên cùng type:
Some(n) | Nonecompile error vìNonekhông bind ran;Some(n) | Ok(n)chỉ hợp lệ khi cả haincó cùng type. - Áp dụng được
|vào 3 use case phổ biến: gom HTTP success status, bỏ qua nhóm typo, phân biệt error retry-able vs fatal.
Cú Pháp | Or-Pattern
Cú pháp đơn giản: đặt nhiều pattern trong cùng một arm, ngăn cách bằng dấu |. Khi value match bất kỳ pattern nào trong nhóm, arm sẽ chạy. Đây là cách viết ngắn cho "nhiều case, cùng một xử lý".
fn size(n: i32) -> &'static str {
match n {
1 | 2 | 3 => "small",
4 | 5 | 6 => "medium",
7 | 8 | 9 => "large",
_ => "out of range",
}
}
fn main() {
println!("{}", size(2)); // small
println!("{}", size(5)); // medium
println!("{}", size(8)); // large
println!("{}", size(42)); // out of range
}
So sánh với phiên bản viết bung ra:
// CÁCH DÀI - duplicate arm body
match n {
1 => "small",
2 => "small",
3 => "small",
// ... lặp lại tương tự cho medium, large
_ => "out of range",
}
Bản dùng | ngắn hơn, ít rủi ro sửa một chỗ quên sửa chỗ khác. Nhưng có ngưỡng: nếu mỗi case cần code khác nhau dù chỉ chút xíu (log message khác, tag khác), vẫn nên tách arm riêng — gom | sai sẽ phải if bên trong arm, rối hơn nguyên bản.
Lưu ý cú pháp: dấu | giữa các pattern là or-pattern, hoàn toàn khác với | dùng làm closure parameter (|x| x + 1). Compiler phân biệt qua context — trong nhánh arm của match, | luôn là or-pattern.
Cách Đọc | Trong Pattern
Đọc 1 | 2 | 3 thành câu Việt: "1 hoặc 2 hoặc 3". Trong ngữ cảnh pattern, | không phải bit-or như trong biểu thức số học — không có chuyện 1 | 2 | 3 bị tính ra 3 rồi đem so sánh. Compiler nhìn ra đây là pattern context (vế trái của =>) nên đọc thành "match bất kỳ trong các giá trị này".
Một số biến thể về cú pháp thường gặp:
- Leading
|: Rust cho phép viết| 1 | 2 | 3 => ...với dấu|đứng đầu. Hữu ích khi format nhiều arm trên nhiều dòng cho dễ đọc, tương tự cú pháp leading|trong OCaml / F#. - Trailing
|: không được phép —1 | 2 | 3 |là syntax error. - Wrap ngoặc:
(1 | 2 | 3)hợp lệ và đôi khi cần thiết khi|dùng lồng trong pattern phức tạp (ví dụ tuple variant).
fn level(n: i32) -> &'static str {
match n {
| 1 | 2 | 3 => "low", // leading | hợp lệ
| 4 | 5 | 6 => "mid",
| 7 | 8 | 9 => "high",
_ => "unknown",
}
}
Phong cách phổ biến của Rust formatter (rustfmt) là không chèn leading | mặc định, nên dùng leading khi project có quy ước riêng. Còn lại giữ format chuẩn: 1 | 2 | 3 => ....
Kết Hợp Với Range
Sức mạnh thật sự của or-pattern hiện ra khi gộp chung với range pattern đã học ở Bài 101. Một arm có thể phủ nhiều khoảng rời nhau + nhiều giá trị lẻ, viết một dòng mà cover nhiều trường hợp.
fn classify(n: i32) -> &'static str {
match n {
0 => "zero",
1 | 5..=10 | 100 => "special", // 1 hoặc 5..10 hoặc 100
11..=99 | 101..=999 => "normal", // hai khoảng nối lại
i32::MIN..=-1 => "negative",
_ => "very large",
}
}
fn main() {
println!("{}", classify(1)); // special
println!("{}", classify(7)); // special (trong 5..=10)
println!("{}", classify(100)); // special
println!("{}", classify(50)); // normal
println!("{}", classify(500)); // normal
println!("{}", classify(-3)); // negative
}
Trước khi có or-pattern, lập trình viên phải viết ba arm 1 => "special", 5..=10 => "special", 100 => "special". Hợp nhất lại thành 1 | 5..=10 | 100 => "special" giúp code "nói" được ý: tất cả những giá trị này được xem là special.
Lưu ý cú pháp: trong arm match chỉ chấp nhận range inclusive (a..=b) — Bài 101 đã nhắc. Exclusive a..b trong vế pattern sẽ compile error. Sai lầm hay gặp khi mới chuyển từ range của for sang pattern context.
Trong Enum Variant
Use case "đáng đồng tiền" nhất của or-pattern là gom variant cùng nhóm trong một enum. Khi enum có nhiều biến thể mang cùng ý nghĩa logic (success, retry-able, fatal), | giúp viết ngắn và đọc thấy ngay nhóm:
enum Status {
Ok,
Created,
Accepted,
BadRequest,
NotFound,
InternalError,
BadGateway,
}
fn category(s: Status) -> &'static str {
match s {
Status::Ok | Status::Created | Status::Accepted => "success",
Status::BadRequest | Status::NotFound => "client error",
Status::InternalError | Status::BadGateway => "server error",
}
}
fn main() {
println!("{}", category(Status::Created)); // success
println!("{}", category(Status::NotFound)); // client error
println!("{}", category(Status::BadGateway)); // server error
}
Lưu ý: match ở đây không cần arm _ => ... vì or-pattern đã phủ hết các variant — compiler tự kiểm tra exhaustive. Khi sau này thêm variant mới (ví dụ Status::ServiceUnavailable), compiler báo lỗi ngay tại function này. Đây là một trong những "siêu năng lực" của Rust enum + match: thêm variant = lập tức biết chỗ nào cần update.
Nếu enum có quá nhiều variant trong cùng nhóm (10+ HTTP success status thật), nên cân nhắc thêm method is_success(), is_client_error() trên enum thay vì gom | dài lê thê. Khi đó match chỉ còn 4-5 arm dùng method, dễ đọc hơn hẳn.
Trong Tuple Pattern
Or-pattern cũng làm việc tốt với tuple. Dấu | thường đặt ngoài cùng tuple để gom nhiều "hình" tuple chung một xử lý:
fn has_one(t: (i32, i32)) -> &'static str {
match t {
(1, _) | (_, 1) => "có 1 trong tuple", // | ngoài cùng tuple
(0, 0) => "cả hai đều 0",
_ => "không có 1",
}
}
fn main() {
println!("{}", has_one((1, 5))); // có 1 trong tuple
println!("{}", has_one((5, 1))); // có 1 trong tuple
println!("{}", has_one((0, 0))); // cả hai đều 0
println!("{}", has_one((3, 4))); // không có 1
}
Cũng có thể đặt | bên trong tuple — gom giá trị cho từng vị trí: (1 | 2, 3 | 4) match khi phần tử đầu là 1 hoặc 2 và phần tử sau là 3 hoặc 4. Nhưng cú pháp này dễ nhầm với or-pattern ngoài tuple, nên thường wrap ngoặc để rõ:
match (a, b) {
((1 | 2), (3 | 4)) => "góc đặc biệt", // mỗi vị trí có or-pattern riêng
(1, _) | (_, 1) => "có 1 ở 1 vị trí",
_ => "khác",
}
Cùng nguyên tắc áp dụng cho struct destructure (Bài 106 sẽ đi sâu): Point { x: 0 | 1, y: 0 | 1 } match góc gần origin.
Binding Phải Cùng Type Mọi Vế
Đây là pitfall đầu bảng của or-pattern: nếu một vế của | bind biến (ví dụ Some(n)), thì mọi vế khác cũng phải bind biến cùng tên với cùng type. Nếu không, compiler từ chối ngay.
// SAI - None không bind ra n
fn unwrap_or_zero(opt: Option<i32>) -> i32 {
match opt {
Some(n) | None => n, // compile error: variable `n` is not bound in all patterns
}
}
// ĐÚNG - dùng arm riêng
fn unwrap_or_zero(opt: Option<i32>) -> i32 {
match opt {
Some(n) => n,
None => 0,
}
}
Quy tắc compiler: tại đầu vào của arm body, biến n phải luôn có giá trị bất kể đi qua vế nào của |. Some(n) bind ra n: i32 nhưng None không bind gì — khi runtime đi qua đường None, n không có giá trị, không an toàn. Rust thà compile error còn hơn để lọt qua.
Quy tắc cùng type còn chặt hơn. Hai vế cùng bind tên n nhưng type lệch cũng sẽ bị từ chối:
enum Msg {
Num(i32),
Text(String),
}
// SAI - n ở hai vế khác type
fn show(m: Msg) {
match m {
Msg::Num(n) | Msg::Text(n) => println!("{n}"),
// error: variable `n` is bound inconsistently
// i32 ở vế trái, String ở vế phải
}
}
Trong những tình huống thực sự cần share xử lý, thường tách arm và gọi chung một hàm:
fn show(m: Msg) {
match m {
Msg::Num(n) => print(&n.to_string()),
Msg::Text(s) => print(&s),
}
}
fn print(s: &str) { println!("{s}"); }
Khi nào or-pattern bind được? Khi cả hai vế bind cùng tên cùng type — ví dụ Ok(n) | Err(n) trên Result<i32, i32> (cả hai variant đều mang i32), hoặc các variant cùng struct: Shape::Square(side) | Shape::Circle(side) nếu cả hai field cùng type f64.
Use Case Thực Tế
Ba pattern thực tế hay gặp trong code production:
1. Phân loại error retry-able vs fatal — quyết định có thử lại request hay bỏ luôn:
enum AppError {
Timeout,
ConnectionReset,
ServiceUnavailable,
Unauthorized,
NotFound,
InvalidInput,
}
fn should_retry(e: &AppError) -> bool {
matches!(
e,
AppError::Timeout | AppError::ConnectionReset | AppError::ServiceUnavailable
)
}
fn handle(e: AppError) {
match e {
AppError::Timeout
| AppError::ConnectionReset
| AppError::ServiceUnavailable => schedule_retry(),
AppError::Unauthorized
| AppError::NotFound
| AppError::InvalidInput => abort_with_log(),
}
}
fn schedule_retry() { /* ... */ }
fn abort_with_log() { /* ... */ }
2. Bỏ qua nhóm typo / synonym — gom các input người dùng gõ sai chính tả cùng một xử lý:
fn parse_yes(s: &str) -> bool {
matches!(
s.to_lowercase().as_str(),
"y" | "yes" | "yep" | "yeah" | "ok" | "true" | "1"
)
}
3. Gom HTTP status theo họ — đã thấy ở Bước 5: tất cả 2xx là success, 4xx là client error, 5xx là server error. Hoặc dùng range thay cho liệt kê tên variant nếu lưu status dưới dạng u16:
fn http_class(code: u16) -> &'static str {
match code {
100..=199 => "info",
200..=299 => "success",
300..=399 => "redirect",
400..=499 => "client error",
500..=599 => "server error",
_ => "unknown",
}
}
Khi viết REST client / server, http_class kiểu này thường gặp ở middleware logging, metrics, hoặc retry policy.
Tổng Kết
- Or-pattern dùng dấu
|giữa các pattern trong cùng một arm: value match bất kỳ vế nào thì arm chạy. - Đọc
1 | 2 | 3là "1 hoặc 2 hoặc 3" — không phải bit-or. Cho phép leading|, không cho trailing|. - Kết hợp được với range (chỉ inclusive
a..=b):1 | 5..=10 | 100 => "special". - Gom enum variant cùng nhóm để giảm duplicate arm body — compiler vẫn check exhaustive.
- Áp dụng cho tuple / struct:
|ngoài cùng gom nhiều "hình",|bên trong gom giá trị từng vị trí — wrap ngoặc cho rõ. - Quy tắc binding nghiêm ngặt: mọi vế của
|phải bind cùng tên với cùng type, nếu không compile error "variable is not bound in all patterns" hoặc "bound inconsistently". - Use case quen thuộc: retry-able vs fatal error, gom HTTP status theo họ, accept nhiều spelling user input.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Viết hàm
day_type(d: u32) -> &'static strtrả"weekday"cho 1..5,"weekend"cho 6 hoặc 7,"invalid"cho còn lại. Dùng|kết hợp range. - Đoạn code
match opt { Some(n) | None => n }compile được không? Vì sao? Sửa lại thế nào. - Cho
enum Coin { Penny, Nickel, Dime, Quarter, Dollar }. Viết match phân loại"small"cho Penny/Nickel,"big"cho Dime/Quarter/Dollar. Compiler có yêu cầu_ => ...không? Vì sao? - Match một tuple
(i32, i32)và trả"diagonal"nếu hai phần tử bằng nhau (giả sử chỉ test các giá trị 0, 1, 2). Có thể viết bằng or-pattern không? Nếu không, vì sao? - Function
matches!(code, 200..=299 | 304)trả gì khicode = 304? Khicode = 250? Khicode = 404?
Đáp án
match d { 1..=5 => "weekday", 6 | 7 => "weekend", _ => "invalid" }. Có thể gộp6 | 7thành6..=7cũng được — tuỳ phong cách.- Không compile.
Nonekhông bind ran, vi phạm quy tắc "mọi vế phải bind cùng tên". Sửa: tách armSome(n) => n, None => 0(hoặc default value khác). Đây là pattern phổ biến tới mức Rust có sẵnopt.unwrap_or(0). match c { Coin::Penny | Coin::Nickel => "small", Coin::Dime | Coin::Quarter | Coin::Dollar => "big" }. Không cần_vì hai arm đã phủ hết 5 variant — exhaustive. Lợi: nếu thêm variant mới (HalfDollar), compiler báo lỗi đúng chỗ này.- So sánh hai phần tử của tuple không làm được bằng or-pattern thuần. Or-pattern chỉ liệt kê pattern cố định, không expression. Phải dùng match guard (Bài 100):
(a, b) if a == b => "diagonal". Or-pattern và guard là hai công cụ khác nhau, bổ trợ lẫn nhau. code = 304: true (match vế304).code = 250: true (nằm trong200..=299).code = 404: false.matches!là macro tiện trảboolcho biểu thức match đơn arm.
Bài Tiếp Theo
Bài 103: if let — Short Match Cho 1 Pattern — chuyển sang một anh em "ngắn gọn" của match: if let Some(x) = opt { ... } dùng khi chỉ quan tâm một pattern duy nhất, không muốn viết arm _ => () bỏ trống. Đi kèm là else nhánh, idiom thay cho match dài 2 arm, và lưu ý về exhaustive — if let bỏ qua kiểm tra này nên cần cẩn trọng với enum mở.
