Danh sách bài viết

Bài 105: let else (Rust 1.65+) — Refutable let

Bài 105 của series Rust Cơ Bản — let else, stable từ Rust 1.65 (phát hành 11/2022, RFC 3137). Đây là phần mở rộng của let cho phép pattern refutable ở vế trái, với một else { ... } block bắt buộc diverge. Tính năng nhỏ về cú pháp nhưng tác động lớn về phong cách viết code: thay vì lồng nhiều tầng if let, bạn có thể flatten thành chuỗi guard clause early-exit — dễ đọc, ít rightward drift, giữ binding ở outer scope để dùng tiếp.

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ẽ:

  • Biết let else là gì, stable từ phiên bản nào, và tại sao đây là extension của let để chấp nhận pattern refutablelet bình thường không cho.
  • Nắm cú pháp đầy đủ let PATTERN = EXPR else { DIVERGE; };: binding bind vào outer scope khi pattern match, else block chạy khi không match và bắt buộc phải diverge.
  • Biết các expression hợp lệ để diverge: return, panic!, break, continue, hoặc loop vô tận loop { }. Compiler enforce — block không diverge sẽ compile error.
  • Phân biệt rõ let else vs if let về phạm vi của binding: if let binding chỉ sống trong block; let else binding sống ở outer scope như let thông thường.
  • Áp dụng let else cho idiom early-exit / guard clause để flatten code, giảm nesting if let { ... } else { return ... }.
  • Biết khi nào không nên dùng let else: khi cần xử lý cả nhánh match và không match (dùng match hoặc if let ... else cleaner hơn).
2

let else Là Gì

let else là một dạng mở rộng của statement let, được stable trong Rust 1.65 (release 3/11/2022) theo RFC 3137. Trước phiên bản này, vế trái của let bắt buộc phải là pattern irrefutable — pattern luôn luôn match với mọi giá trị (vd let x = ..., let (a, b) = ...). Pattern refutable như Some(x), Ok(v) bị từ chối vì có thể không match (gặp None hay Err), khi đó binding sẽ undefined.

let else giải quyết vấn đề này bằng cách cho phép pattern refutable trong let, với điều kiện phải kèm một else { ... } block xử lý trường hợp không match. Block này bắt buộc diverge — tức không bao giờ trả control về dòng tiếp theo. Nhờ vậy, ở dòng sau let else, compiler chắc chắn pattern đã match và binding hợp lệ để dùng.

fn main() {
    let opt: Option<i32> = Some(42);

    // KHÔNG hợp lệ trước Rust 1.65: pattern refutable trong let bình thường
    // let Some(x) = opt;  // error: refutable pattern in local binding

    // let else từ 1.65: thêm else { diverge } để xử lý nhánh None
    let Some(x) = opt else {
        println!("Không có giá trị, thoát sớm");
        return;
    };

    // Tại đây, x đã được bind ở outer scope - dùng được như let bình thường
    println!("x = {x}");  // x = 42
}

Trước 1.65, để có hành vi tương đương bạn phải dùng match hoặc if let ... else { return; } rồi extract binding thủ công — dài và lồng. let else nén lại thành một dòng đúng nghĩa "unwrap hoặc thoát".

3

Cú Pháp

Cú pháp tổng quát:

let PATTERN = EXPR else {
    DIVERGE;
};
  • PATTERN — bất kỳ pattern nào, irrefutable hoặc refutable (dùng let else với pattern irrefutable thì compiler warn "irrefutable let-else pattern" vì else block không bao giờ chạy — vô nghĩa).
  • EXPR — biểu thức cho ra giá trị để match. Có thể là expression bất kỳ (function call, field access, literal...).
  • DIVERGE — body của else block phải kết thúc bằng một expression diverge (return, panic!, break, continue, hoặc loop vô tận). Chi tiết ở mục 4.
  • Dấu chấm phẩy ; sau khối else { ... } là bắt buộc — let là statement, không phải expression.

Binding tạo bởi PATTERN sống ở outer scope (cùng scope với chính let đó), không phải trong else block. Điều này quan trọng và khác với if let:

fn process(input: Option<String>) {
    let Some(s) = input else {
        // Trong block này, s KHÔNG tồn tại - chưa match thì chưa có binding
        eprintln!("input rỗng");
        return;
    };

    // Sau dòng trên, s là String, scope ngang với let bình thường
    println!("độ dài: {}", s.len());
    println!("uppercase: {}", s.to_uppercase());
    // s vẫn dùng được đến cuối hàm
}
4

else Block Phải Diverge

Điều kiện cốt lõi của let else: else block không được "rơi xuống" (fall through) dòng tiếp theo. Type của expression cuối block phải là ! (never type) — biểu thức không bao giờ trả về. Compiler enforce điều này ngay tại compile time.

Các cách hợp lệ để diverge:

// 1) return - thoát hàm sớm
fn parse_id(s: &str) -> Option<u32> {
    let Ok(n) = s.parse::<u32>() else {
        return None;
    };
    Some(n + 1000)
}

// 2) panic! - dừng program (chỉ dùng khi thực sự bug, không dùng cho user input)
fn must_have(opt: Option<i32>) -> i32 {
    let Some(x) = opt else {
        panic!("invariant: opt must be Some");
    };
    x
}

// 3) break - thoát loop bên ngoài
fn first_positive(data: &[i32]) -> Option<i32> {
    let mut result = None;
    for &x in data {
        let true = x > 0 else {
            break;  // break ra khỏi for loop
        };
        result = Some(x);
        break;
    }
    result
}

// 4) continue - sang vòng lặp tiếp theo
fn sum_even(nums: &[i32]) -> i32 {
    let mut total = 0;
    for &n in nums {
        let 0 = n % 2 else {
            continue;  // bỏ qua số lẻ
        };
        total += n;
    }
    total
}

// 5) loop {} vô tận - hiếm dùng, chủ yếu trong embedded / panic handler
fn never_returns(opt: Option<i32>) -> i32 {
    let Some(x) = opt else {
        loop { /* spin forever */ }
    };
    x
}

Trường hợp không hợp lệ và compile error:

fn bad(opt: Option<i32>) -> i32 {
    let Some(x) = opt else {
        println!("no value");
        // fall through - không return / panic / break / continue
        // compile error: `else` clause of `let...else` does not diverge
    };
    x
}

Lý do compiler nghiêm khắc: nếu else không diverge mà fall through, dòng x sẽ truy cập binding chưa được khởi tạo — đúng kiểu unsoundness mà Rust luôn cấm. Diverge bảo đảm static rằng nếu code chạy đến dòng sau let else, pattern đã chắc chắn match.

5

Khác Biệt Với if let

Cả if letlet else đều test pattern refutable, nhưng scope của binding là điểm khác biệt cốt lõi.

fn with_if_let(opt: Option<i32>) {
    if let Some(x) = opt {
        // x chỉ sống trong block này
        println!("inside: {x}");
    }
    // println!("outside: {x}");  // ERROR: x không tồn tại ở đây
}

fn with_let_else(opt: Option<i32>) {
    let Some(x) = opt else {
        return;
    };
    // x sống ở outer scope - đến hết hàm
    println!("outside: {x}");
}

Hệ quả thực tế: khi bạn có chuỗi nhiều Option/Result cần unwrap trước khi xử lý chính, nesting if let nhanh chóng tạo rightward drift khó đọc; let else flatten thành chuỗi guard clause tuyến tính.

// Nested if let - rightward drift
fn nested(a: Option<i32>, b: Option<i32>, c: Option<i32>) -> i32 {
    if let Some(x) = a {
        if let Some(y) = b {
            if let Some(z) = c {
                return x + y + z;
            }
        }
    }
    0
}

// Phẳng với let else - dễ đọc theo dòng dọc
fn flat(a: Option<i32>, b: Option<i32>, c: Option<i32>) -> i32 {
    let Some(x) = a else { return 0; };
    let Some(y) = b else { return 0; };
    let Some(z) = c else { return 0; };
    x + y + z
}

Quy tắc chọn: dùng if let khi binding chỉ cần trong một block ngắn và phần lớn code không cần binding đó; dùng let else khi binding sẽ được dùng tiếp ở phần lớn hàm sau đó.

6

Use Case: Early Exit / Guard Clause

Use case kinh điển: hàm parse / validate — đầu hàm có vài bước "phải đúng mới đi tiếp", mỗi bước có thể fail và trả lỗi sớm. Đây là idiom guard clause được khuyến khích trong nhiều ngôn ngữ (Go, Swift, JS) và let else mang nó vào Rust tự nhiên.

#[derive(Debug)]
enum Error {
    MissingPrefix,
    NotNumeric,
    OutOfRange,
}

// Parse chuỗi dạng "v123" thành u32, có nhiều bước validate
fn parse_version(input: &str) -> Result<u32, Error> {
    let Some(stripped) = input.strip_prefix("v") else {
        return Err(Error::MissingPrefix);
    };

    let Ok(n) = stripped.parse::<u32>() else {
        return Err(Error::NotNumeric);
    };

    let true = n <= 1000 else {
        return Err(Error::OutOfRange);
    };

    // Tại đây cả 3 điều kiện đã pass, logic chính nằm thẳng dòng
    Ok(n * 10)
}

fn main() {
    println!("{:?}", parse_version("v42"));     // Ok(420)
    println!("{:?}", parse_version("42"));      // Err(MissingPrefix)
    println!("{:?}", parse_version("vabc"));    // Err(NotNumeric)
    println!("{:?}", parse_version("v9999"));   // Err(OutOfRange)
}

So sánh với cùng logic viết kiểu nested — phần "happy path" bị đẩy sâu vào trong, lúc đọc phải nhảy ngang nhiều và rất dễ bỏ sót nhánh error. Phong cách guard clause với let else giữ "happy path" ở mức indent 1, mọi lỗi xử lý ngay tại nơi phát hiện.

7

Tương Đương Match Form

Trước khi có let else, để đạt hành vi tương đương phải viết một match verbose, bind binding qua statement riêng. Hai snippet sau làm chính xác cùng việc:

// === Phiên bản let else (ngắn gọn) ===
fn double_or_exit(opt: Option<i32>) -> i32 {
    let Some(x) = opt else {
        return -1;
    };
    x * 2
}

// === Phiên bản match tương đương (verbose) ===
fn double_or_exit_match(opt: Option<i32>) -> i32 {
    let x = match opt {
        Some(v) => v,
        None => return -1,
    };
    x * 2
}

// === Phiên bản if let ... else trước Rust 1.65 (cũng verbose) ===
fn double_or_exit_iflet(opt: Option<i32>) -> i32 {
    let x = if let Some(v) = opt {
        v
    } else {
        return -1;
    };
    x * 2
}

Ba phiên bản sinh ra cùng MIR sau khi compiler optimize. let else chỉ là sugar trên syntax — không thêm performance cost, không thay đổi semantics, chỉ giúp đọc và viết gọn hơn. Khi review code legacy bạn sẽ gặp cả ba dạng, cần đọc được tất cả.

Lưu ý nhỏ: trong match form, arm None => return -1 phải dùng return expression (type là !) chứ không phải đặt return ngoài match — vì match là expression và mọi arm phải cùng type. Khi mỗi arm tự diverge, type được suy thành type của arm còn lại.

8

Best Practice & Anti-Pattern

Dùng let else khi:

  • Bạn chắc chắn muốn thoát sớm nếu pattern không match và tiếp tục với binding ở phần còn lại của hàm/block.
  • Có nhiều bước unwrap tuyến tính — viết let else giảm nesting đáng kể.
  • Hàm theo phong cách guard clause: validate đầu hàm, body chính sau cùng.

Không nên dùng let else khi:

  • Cần xử lý cả hai nhánh (match và không match) với logic phức tạp ở mỗi nhánh — match với mỗi arm rõ nét sẽ dễ đọc hơn nhiều.
  • else block của bạn cần làm nhiều việc (tính toán, log, build error chi tiết) chứ không đơn giản là return ...match thường rõ hơn.
  • Binding chỉ cần dùng trong một block ngắn ngay sau đó — if let giữ scope hẹp hơn, an toàn hơn.
  • Bạn muốn elsefallback giá trị chứ không phải exit — đó là chỗ của unwrap_or, unwrap_or_else, hoặc if let ... else { ... } expression form.

Anti-pattern thường gặp khi mới quen let else:

// ANTI: lạm dụng panic! trong let else cho user input
fn bad_parse(s: &str) -> u32 {
    let Ok(n) = s.parse::<u32>() else {
        panic!("invalid input");  // panic với user data => crash production
    };
    n
}

// TỐT HƠN: trả Result, để caller quyết
fn good_parse(s: &str) -> Result<u32, std::num::ParseIntError> {
    s.parse::<u32>()
}

// ANTI: dùng let else khi else block to và phức tạp
fn awkward(opt: Option<i32>) -> i32 {
    let Some(x) = opt else {
        // 20 dòng tính toán, build error, log, metric...
        // đến cuối mới return một giá trị fallback
        return 0;
    };
    x
}
// → match form / if let ... else form sẽ rõ hơn ở case này

Nguyên tắc chung: let else tối ưu cho trường hợp "thoát ngắn gọn, đi tiếp"; mọi use case khác nên xem xét match, if let, hoặc combinator (map, and_then, ok_or, ?).

9

Tổng Kết

  • let else stable từ Rust 1.65 (3/11/2022, RFC 3137) — extension cho let cho phép pattern refutable.
  • Cú pháp: let PATTERN = EXPR else { DIVERGE; };. Binding bind vào outer scope nếu pattern match.
  • else block bắt buộc diverge: kết thúc bằng return, panic!, break, continue, hoặc loop vô tận. Compiler enforce static — block không diverge sẽ compile error.
  • Khác if let: scope của binding ở let elseouter scope, còn if let binding chỉ sống trong block if.
  • Use case chính: early-exit / guard clause để flatten chuỗi unwrap nhiều tầng, giảm rightward drift, giữ "happy path" ở indent 1.
  • Tương đương về semantics với match { Some(v) => v, None => return ... } hoặc if let ... else { return ... } — chỉ ngắn hơn, không khác về compile output.
  • Tránh dùng let else khi cần xử lý cả 2 nhánh phức tạp (chọn match), khi else block dài (chọn match), hoặc khi muốn fallback giá trị thay vì exit (chọn unwrap_or / combinator).
  • Pattern irrefutable trong let else được compiler warn — vì else không bao giờ chạy, dùng let bình thường là đủ.
10

Bài Tập Củng Cố

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

  1. Viết hàm fn first_word(s: &str) -> Option<&str> trả về từ đầu tiên (cắt bởi khoảng trắng) bằng s.split_whitespace().next(). Dùng let else để trả None nếu chuỗi rỗng, ngược lại trả Some(word) với word đã to_uppercase().
  2. Đoạn code sau có compile được không? Vì sao? fn f(opt: Option<i32>) { let Some(x) = opt else { println!("none"); }; println!("{x}"); }
  3. Refactor hàm sau từ nested if let sang chuỗi let else phẳng: fn lookup(map: &std::collections::HashMap<&str, Vec<i32>>, key: &str) -> i32 { if let Some(v) = map.get(key) { if let Some(&first) = v.first() { first * 2 } else { 0 } } else { 0 } }
  4. Hàm fn unwrap_or_panic(opt: Option<String>) -> String trả String bên trong opt, panic nếu None. Viết bằng let else. Có nên dùng cách viết này trong production code khi opt đến từ user input không? Vì sao?
  5. Một học viên viết: let x = 5 else { return; };. Compiler nói gì và sửa thế nào?
Đáp án
  1. fn first_word(s: &str) -> Option<String> { let Some(word) = s.split_whitespace().next() else { return None; }; Some(word.to_uppercase()) } — return type đổi sang Option<String>to_uppercase() tạo String mới (allocate). Nếu giữ Option<&str> thì không thể uppercase được.
  2. Không compile. Else block chỉ có println!("none") rồi rơi xuống cuối block — không diverge. Compiler báo "`else` clause of `let...else` does not diverge". Sửa bằng cách thêm return; sau println!.
  3. fn lookup(map: &HashMap<&str, Vec<i32>>, key: &str) -> i32 { let Some(v) = map.get(key) else { return 0; }; let Some(&first) = v.first() else { return 0; }; first * 2 } — phẳng hơn nhiều, "happy path" first * 2 ở indent 1.
  4. fn unwrap_or_panic(opt: Option<String>) -> String { let Some(s) = opt else { panic!("expected Some"); }; s }. Không nên dùng với user input — panic! sẽ crash chương trình hoặc abort request. Với user input nên trả Result hoặc Option để caller xử lý; panic! chỉ phù hợp khi None là logic bug không thể xảy ra ở runtime (invariant nội bộ).
  5. Compiler warn (không error): "irrefutable `let...else` pattern". Pattern x luôn match mọi giá trị, else block không bao giờ chạy — dòng này tương đương let x = 5;. Sửa: bỏ else { return; };, viết let x = 5; bình thường.
11

Bài Tiếp Theo

Bài 106: Destructure Struct / Tuple / Enum Trong match — khi vào match, các pattern Point { x, y }, (a, b), Some(MyStruct { f1, f2 }) kết hợp được tự do với nhau. Học cú pháp destructure lồng, dùng _ ignore từng field và .. ignore rest cho cả struct và tuple-variant.