Danh sách bài viết

Bài 102: Multiple Patterns Với |

Bài 102 của series Rust Cơ Bản — gom nhiều giá trị vào cùng một arm bằng toán tử | (or-pattern): 1 | 2 | 3 => "small". Cú pháp này tránh duplicate code khi nhiều case cùng chia sẻ một xử lý, kết hợp được với range (1 | 5..=10), với enum variant cùng nhóm (Status::Ok | Status::Created), và cả với tuple. Đi kèm là một quy tắc binding rất dễ vướng: mọi vế của | phải bind cùng tên với cùng type, nếu không compile sẽ từ chối thẳng tay.

09/06/2026
8 phút đọc
1 lượt xem
1

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 arm match để 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ới range 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) | None compile error vì None không bind ra n; Some(n) | Ok(n) chỉ hợp lệ khi cả hai n có 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.
2

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.

3

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 => ....

4

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.

5

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.

6

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.

7

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.

8

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.

9

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 | 3 là "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.
10

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. Viết hàm day_type(d: u32) -> &'static str trả "weekday" cho 1..5, "weekend" cho 6 hoặc 7, "invalid" cho còn lại. Dùng | kết hợp range.
  2. Đoạn code match opt { Some(n) | None => n } compile được không? Vì sao? Sửa lại thế nào.
  3. 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?
  4. 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?
  5. Function matches!(code, 200..=299 | 304) trả gì khi code = 304? Khi code = 250? Khi code = 404?
Đáp án
  1. match d { 1..=5 => "weekday", 6 | 7 => "weekend", _ => "invalid" }. Có thể gộp 6 | 7 thành 6..=7 cũng được — tuỳ phong cách.
  2. Không compile. None không bind ra n, vi phạm quy tắc "mọi vế phải bind cùng tên". Sửa: tách arm Some(n) => n, None => 0 (hoặc default value khác). Đây là pattern phổ biến tới mức Rust có sẵn opt.unwrap_or(0).
  3. 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.
  4. 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.
  5. code = 304: true (match vế 304). code = 250: true (nằm trong 200..=299). code = 404: false. matches! là macro tiện trả bool cho biểu thức match đơn arm.
11

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ở.