Danh sách bài viết

Bài 107: @ Binding — Bind Và Test

Bài 107 của series Rust Cơ Bản — bài cuối Nhóm 14: @ binding, viết là name @ pattern, cho phép vừa test một pattern (range, literal, sub-pattern) vừa bind giá trị gốc vào tên name. Khác với plain binding kiểu Some(n) chỉ bind không test thêm điều kiện, và khác với pattern thuần như 1..=5 chỉ test mà không có tên để dùng tiếp, n @ 1..=5 kết hợp cả hai mục đích. Đây là công cụ then chốt cho code validate input parser, classifier theo range, port check — bất kỳ tình huống nào cần "chỉ chấp nhận value nếu thỏa pattern, rồi xử lý value đó".

09/06/2026
9 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 @ binding (còn gọi at-binding hoặc identifier pattern with sub-pattern) là gì và vì sao Rust thiết kế cú pháp này.
  • Nắm cú pháp name @ pattern: phần bên phải @ là pattern để test, phần bên trái là tên binding cho giá trị gốc khi pattern match.
  • Áp dụng @ với range pattern (1..=10, 0..=100) để vừa giới hạn miền giá trị vừa lấy số đó ra dùng.
  • Kết hợp @ với struct destructureenum variant: bind từng field theo constraint riêng, không chỉ bind toàn bộ.
  • Viết nested @ binding kiểu Some(n @ 1..=10) — bind variable bên trong enum variant kèm điều kiện sub-pattern.
  • Phân biệt rõ plain binding (Some(n)) — chỉ bind, không thêm constraint — với @ binding (Some(n @ 1..=10)) — bind kèm điều kiện. Biết khi nào cần mỗi loại.
  • Áp dụng vào tình huống thực tế: validate id trong range cho service, classifier nhóm tuổi cho form đăng ký, port range check cho config parser.
  • Tổng kết toàn bộ Nhóm 14 — Pattern Matching — trước khi sang Nhóm 15 — Modules & Crates.
2

@ Binding Là Gì

@ binding là một dạng identifier pattern đặc biệt, cho phép một pattern đồng thời thực hiện hai việc: test giá trị có khớp một sub-pattern hay không, và bind chính giá trị đó vào một cái tên để dùng trong nhánh match. Trong tài liệu Rust Reference, nó nằm ở mục Identifier Patterns: cú pháp tổng quát là NAME @ SUBPATTERN.

Trước khi có @, bạn buộc phải chọn một trong hai: hoặc viết n => ... để bind n nhưng không test thêm điều kiện (sau đó tự kiểm tra bằng if), hoặc viết 1..=5 => ... để test range nhưng không có tên nào trỏ tới giá trị vừa match. @ nối hai mảng đó lại thành một cú pháp duy nhất.

fn main() {
    let n = 7;

    match n {
        // BIND mà KHÔNG test thêm:
        x => println!("x = {x}"),  // arm này chiếm hết mọi giá trị
    }

    match n {
        // TEST mà KHÔNG có tên dùng tiếp:
        1..=5 => println!("small"),
        6..=10 => println!("medium"),
        _ => println!("big"),
    }

    match n {
        // @ binding: TEST range VÀ BIND vào tên `num` cùng lúc
        num @ 1..=5 => println!("{num} nằm trong 1..=5"),
        num @ 6..=10 => println!("{num} nằm trong 6..=10"),
        _ => println!("ngoài range"),
    }
}

Điểm cốt lõi: num @ 1..=5 chỉ match khi giá trị nằm trong 1..=5, và nếu match thì num bind chính giá trị đó (không phải range, không phải copy giả lập). Trong arm body, num có type giống n — bạn dùng tự do như mọi biến khác.

3

Use Case: Test Range + Lấy Value

Use case kinh điển nhất: bạn muốn phân loại số theo range, đồng thời dùng chính giá trị đó trong thông báo hoặc tính toán. Không có @ thì hoặc phải dùng match guard (n if (1..=5).contains(&n)) — verbose, hoặc tự nhân đôi biến — không khả thi vì arm 1..=5 không bind tên.

fn classify(n: i32) {
    match n {
        n @ 1..=5 => println!("{n} là số nhỏ"),
        n @ 6..=10 => println!("{n} là số trung bình"),
        n @ 11..=100 => println!("{n} là số lớn"),
        n => println!("{n} ngoài range hỗ trợ"),
    }
}

fn main() {
    classify(3);   // 3 là số nhỏ
    classify(7);   // 7 là số trung bình
    classify(42);  // 42 là số lớn
    classify(200); // 200 ngoài range hỗ trợ
}

Ở mỗi arm, biến n được bind locally trong scope của arm đó — đây không phải shadowing của tham số n ngoài hàm, mà là một binding mới do pattern tạo ra. Trùng tên hợp lệ và an toàn — compiler hiểu đúng. Bạn có thể đổi tên cho rõ: small @ 1..=5, medium @ 6..=10 v.v., tùy ngữ cảnh.

Lưu ý: range 1..=5inclusive ở cả hai đầu — bao gồm cả 15. Range exclusive 1..5 hiện chỉ stable cho match từ Rust 1.80; trước đó phải dùng inclusive hoặc viết n if n >= 1 && n < 5. Bài này dùng ..= cho mọi ví dụ để an toàn.

4

Use Case: Validate + Capture

Use case thứ hai gần với code production: một hàm chỉ chấp nhận id thuộc một range hợp lệ và xử lý nó; mọi id khác trả lỗi. @ giúp việc kiểm tra và lấy giá trị gọn lại thành một dòng:

#[derive(Debug)]
enum ProcessError {
    OutOfRange(u32),
}

fn process(id: u32) {
    println!("Đang xử lý id hợp lệ: {id}");
}

fn handle_id(id: u32) -> Result<(), ProcessError> {
    match id {
        id @ 100..=999 => {
            process(id);
            Ok(())
        }
        other => Err(ProcessError::OutOfRange(other)),
    }
}

fn main() {
    let _ = handle_id(150);  // process được gọi
    let res = handle_id(50); // ngoài range, trả Err(OutOfRange(50))
    println!("{:?}", res);
}

Cấu trúc id @ 100..=999 => process(id) đọc gần như nói tiếng Việt: "nếu id thuộc 100..=999 thì gọi process với id đó". Tách validate khỏi business logic nhưng vẫn giữ trên cùng một arm — không cần guard, không cần biến trung gian. Đây là dấu hiệu code Rust idiomatic: cho compiler biết constraint ngay tại pattern thay vì đẩy về body.

5

Combine Với Struct / Enum Pattern

@ không chỉ giới hạn ở range. Mọi nơi cú pháp pattern xuất hiện đều có thể dùng name @ subpattern. Đặc biệt mạnh khi kết hợp với destructure struct hoặc enum variant: bạn có thể constraint từng field theo range riêng đồng thời bind từng field theo tên riêng.

enum Message {
    Move { x: i32, y: i32 },
    Resize { w: u32, h: u32 },
    Quit,
}

fn handle(msg: Message) {
    match msg {
        // x phải trong 0..=100 (bind vào pos_x), y bind bình thường
        Message::Move { x: pos_x @ 0..=100, y } => {
            println!("Move hợp lệ: x = {pos_x}, y = {y}");
        }
        // x ngoài range hỗ trợ - vẫn bind cả 2 để log
        Message::Move { x, y } => {
            println!("Move ngoài giới hạn: x = {x}, y = {y}");
        }
        Message::Resize { w: width @ 1..=4096, h: height @ 1..=4096 } => {
            println!("Resize chuẩn: {width}x{height}");
        }
        Message::Resize { w, h } => {
            println!("Resize không hợp lệ: {w}x{h}");
        }
        Message::Quit => println!("Quit"),
    }
}

fn main() {
    handle(Message::Move { x: 50, y: 200 });   // hợp lệ
    handle(Message::Move { x: 500, y: 10 });   // ngoài range
    handle(Message::Resize { w: 1920, h: 1080 });
    handle(Message::Resize { w: 9999, h: 5000 });
}

Cú pháp field_name: binding @ subpattern trong struct pattern hợp lệ ở mọi vị trí. Khi cần bind cả giá trị cấu trúc bao ngoài, viết whole @ Message::Move { ... }whole bind toàn bộ enum variant để pass tiếp cho hàm khác. Mức linh hoạt này khiến @ được dùng nhiều trong parser AST / interpreter.

6

Nested @ Binding

Khi @ xuất hiện bên trong một enum variant, đó là nested @ binding. Mẫu phổ biến nhất là Some(n @ 1..=10): chỉ match khi Option chứa Some giá trị bên trong thuộc 1..=10; biến n bind chính giá trị đó (không phải Option):

fn process(n: i32) {
    println!("Xử lý số nhỏ: {n}");
}

fn check(opt: Option<i32>) {
    match opt {
        Some(n @ 1..=10) => process(n),
        Some(n) => println!("{n} có giá trị nhưng ngoài 1..=10"),
        None => println!("Không có giá trị"),
    }
}

fn main() {
    check(Some(5));    // Xử lý số nhỏ: 5
    check(Some(50));   // 50 có giá trị nhưng ngoài 1..=10
    check(None);       // Không có giá trị
}

Hai arm đầu cùng match Some(...) nhưng arm có @ 1..=10 chiếm ưu tiên (Rust match luôn thử theo thứ tự từ trên xuống). Nếu giá trị trong Some ngoài range, arm thứ nhất fail, control rơi xuống arm thứ hai. Đây là cách viết "filter-then-process" tự nhiên không cần guard hay if let lồng.

Có thể lồng sâu hơn: Some(Some(n @ 1..=10)) với Option<Option<i32>>, hoặc Ok(point @ Point { x: 0..=10, y: 0..=10 }) với Result<Point, _>. Quy tắc duy nhất: @ luôn theo dạng identifier @ pattern, không bao giờ là pattern @ identifier.

7

Khác Biệt Với Plain Binding

Phân biệt rõ giữa plain binding và @ binding là điểm hay gây nhầm khi mới học:

fn demo(opt: Option<i32>) {
    // Plain binding: Some(n) - bind n nhưng KHÔNG test thêm điều kiện.
    // Mọi Some(...) đều match arm này.
    match opt {
        Some(n) => println!("plain: {n}"),
        None => println!("none"),
    }

    // @ binding: Some(n @ 1..=10) - bind n VÀ test range.
    // Chỉ Some chứa 1..=10 mới match arm đầu.
    match opt {
        Some(n @ 1..=10) => println!("trong range: {n}"),
        Some(n) => println!("ngoài range: {n}"),
        None => println!("none"),
    }
}

Ghi nhớ: phía trái @ luôn là identifier mới (không phải pattern), phía phải là pattern (range, literal, sub-struct, sub-enum, hoặc thậm chí lồng tiếp _). Plain binding như Some(n) tương đương Some(n @ _) nhưng không ai viết dài như vậy — vì sub-pattern _ luôn match nên không thêm thông tin.

So sánh nhanh với match guard (đã học bài 102): Some(n) if (1..=10).contains(&n) đạt cùng hiệu quả nhưng dùng biểu thức boolean ở vế phải. @ được khuyến khích khi điều kiện có thể biểu diễn bằng pattern (range, literal, struct shape...) vì exhaustive checker của compiler nhìn thấy được; guard với boolean tùy ý thì compiler không phân tích được, không cảnh báo missing case.

8

Use Case Thực Tế

Ba ví dụ điển hình bạn sẽ gặp lại nhiều lần khi viết Rust cho web service, CLI tool, parser.

Validate Input Parser

Khi parse config file (TOML/YAML) hoặc query param HTTP, bạn thường nhận Option<u32> rồi cần chia thành: không có → default, có nhưng quá lớn → reject, có và hợp lệ → dùng. @ binding gói gọn cả ba nhánh:

fn pick_workers(opt: Option<u32>) -> u32 {
    match opt {
        Some(n @ 1..=64) => n,               // chọn giá trị user cấp
        Some(n) => {
            eprintln!("workers={n} quá cao, dùng 64");
            64
        }
        None => 4,                            // mặc định
    }
}

Age Group Classifier

Form đăng ký phân loại user theo độ tuổi để hiển thị nội dung phù hợp. Code đọc một lèo, không nhánh boolean:

fn age_group(age: u8) -> &'static str {
    match age {
        a @ 0..=12 => { println!("kid, age={a}"); "kid" }
        a @ 13..=19 => { println!("teen, age={a}"); "teen" }
        a @ 20..=59 => { println!("adult, age={a}"); "adult" }
        a @ 60..=120 => { println!("senior, age={a}"); "senior" }
        a => { println!("age không hợp lệ: {a}"); "invalid" }
    }
}

Port Range Check

Service network thường cần phân biệt port hệ thống (<1024) cần quyền root, port user (1024-49151), port dynamic (49152-65535). @ cho phép log đúng dải mà không tra lại bảng:

fn classify_port(port: u16) {
    match port {
        p @ 0..=1023 => println!("port hệ thống {p}, cần root"),
        p @ 1024..=49151 => println!("port user {p}, OK"),
        p @ 49152..=65535 => println!("port dynamic {p}, ephemeral"),
    }
}

Cả ba ví dụ đều tận dụng đúng một đặc tính: vừa lọc theo pattern vừa giữ lại giá trị. Trước khi có @, code Rust 2015 thường phải viết match-guard hoặc unwrap thủ công — dài hơn và compiler không kiểm tra exhaustive được.

9

Tổng Kết

  • @ binding cú pháp name @ pattern: vừa test sub-pattern vừa bind giá trị gốc vào name để dùng trong arm.
  • Khắc phục giới hạn cũ: pattern thuần (như 1..=5) không bind tên, plain binding (như Some(n)) không test thêm điều kiện.
  • Kết hợp tự do với range pattern, struct destructure, enum variant, và nested pattern lồng nhiều tầng.
  • So với match guard: @ được exhaustive checker hiểu, guard với biểu thức boolean tùy ý thì không.
  • Pattern luôn theo dạng identifier @ pattern — không bao giờ pattern @ identifier.

Tổng Kết Nhóm 14 — Pattern Matching

  • Bài 98 match expression: cú pháp match value { arm => expr, ... }, exhaustive checker, mọi arm trả cùng type.
  • Bài 99 match arm binding: bind giá trị từ pattern vào tên dùng trong arm body.
  • Bài 100 range pattern: 1..=10 inclusive, dùng cho integer / char.
  • Bài 101 if let / 102 match guards / 103 tuple destructure / 104 while let / 105 let else / 106 destructure struct-tuple-enum: bộ công cụ pattern matching đầy đủ cho code idiomatic.
  • Bài 107 @ binding: ghép test + bind, hoàn thiện bộ pattern.
  • Nguyên tắc xuyên suốt nhóm: thay thế if-else và guard boolean bằng pattern để compiler kiểm tra exhaustive và phát hiện logic bug sớm.
10

Bài Tập Củng Cố

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

  1. Viết hàm fn grade(score: u8) -> char trả về 'A' (90..=100), 'B' (80..=89), 'C' (70..=79), 'D' (60..=69), 'F' (0..=59). Với mỗi arm, in ra "Điểm X, xếp loại Y" bằng cách dùng @ binding để lấy số điểm gốc.
  2. Đoạn code match opt { Some(1..=10 @ n) => ..., _ => ... } compile được không? Vì sao?
  3. Viết hàm phân loại HTTP status: 1xx info, 2xx success, 3xx redirect, 4xx client error, 5xx server error. Mỗi nhánh in ra status code gốc bằng @ binding.
  4. Refactor match n { x if (1..=10).contains(&x) => ..., x if (11..=100).contains(&x) => ..., _ => ... } sang dạng dùng @ binding. Phiên bản mới ngắn hơn bao nhiêu dòng?
  5. Cho enum enum Coord { Point2D(i32, i32), Point3D(i32, i32, i32) }. Viết match arm chỉ xử lý Point2D khi cả x và y trong -100..=100, bind tên rõ px, py qua @. Mọi case khác trả về thông báo "ngoài giới hạn".
Đáp án
  1. fn grade(score: u8) -> char { match score { s @ 90..=100 => { println!("Điểm {s}, xếp loại A"); 'A' } s @ 80..=89 => { println!("Điểm {s}, xếp loại B"); 'B' } s @ 70..=79 => { println!("Điểm {s}, xếp loại C"); 'C' } s @ 60..=69 => { println!("Điểm {s}, xếp loại D"); 'D' } s @ 0..=59 => { println!("Điểm {s}, xếp loại F"); 'F' } _ => 'F' } } — cuối arm bắt buộc trả char; arm cuối _ bao tất cả giá trị >100 (không thể với u8 >255 nhưng cần để exhaustive).
  2. Không compile. Cú pháp @ bắt buộc identifier ở vế trái, không phải pattern. Phải viết Some(n @ 1..=10), không phải Some(1..=10 @ n). Compiler báo lỗi parse.
  3. fn classify_http(code: u16) { match code { c @ 100..=199 => println!("1xx info: {c}"), c @ 200..=299 => println!("2xx success: {c}"), c @ 300..=399 => println!("3xx redirect: {c}"), c @ 400..=499 => println!("4xx client error: {c}"), c @ 500..=599 => println!("5xx server error: {c}"), c => println!("status không chuẩn: {c}"), } }.
  4. match n { x @ 1..=10 => ..., x @ 11..=100 => ..., _ => ... } — mỗi arm gọn từ x if (1..=10).contains(&x) (3 phần) thành x @ 1..=10 (1 phần). Cùng số dòng nhưng ngắn hơn về ký tự, dễ đọc hơn, và compiler kiểm tra exhaustive được.
  5. fn handle(c: Coord) { match c { Coord::Point2D(px @ -100..=100, py @ -100..=100) => { println!("Point2D hợp lệ: ({px}, {py})"); } _ => println!("ngoài giới hạn"), } } — tuple-variant Point2D(x, y) destructure trực tiếp, mỗi field gắn @ range riêng. Mọi case không khớp (kể cả Point3D) rơi vào arm _.
11

Bài Tiếp Theo

Bài 108: Package vs Crate vs Module — 3 Khái Niệm — kết thúc Nhóm 14, chuyển sang Nhóm 15 Modules & Crates. Ba khái niệm package, crate, module dễ lẫn cho người mới: package là đơn vị Cargo (Cargo.toml + một hoặc nhiều crate); crate là đơn vị biên dịch (một binary hoặc một library); module là namespace tổ chức code bên trong crate. Bài 108 sẽ vẽ rõ ranh giới giữa ba khái niệm trước khi đi sâu vào mod, pub, use ở các bài kế tiếp.