Mục lục
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 refutable màletbì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ậnloop { }. 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 letbinding chỉ sống trong block;let elsebinding sống ở outer scope nhưletthông thường. - Áp dụng
let elsecho idiom early-exit / guard clause để flatten code, giảm nestingif 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ùngmatchhoặcif let ... elsecleaner hơn).
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".
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ùnglet elsevớ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ốielse { ... }là bắt buộc —letlà 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
}
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.
Khác Biệt Với if let
Cả if let và let 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 đó.
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.
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.
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 elsegiả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 —
matchvớ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 ...—matchthường rõ hơn. - Binding chỉ cần dùng trong một block ngắn ngay sau đó —
if letgiữ scope hẹp hơn, an toàn hơn. - Bạn muốn
elselà fallback giá trị chứ không phải exit — đó là chỗ củaunwrap_or,unwrap_or_else, hoặcif 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, ?).
Tổng Kết
- let else stable từ Rust 1.65 (3/11/2022, RFC 3137) — extension cho
letcho 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 elselà outer scope, cònif letbinding chỉ sống trong blockif. - 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ặcif let ... else { return ... }— chỉ ngắn hơn, không khác về compile output. - Tránh dùng
let elsekhi cần xử lý cả 2 nhánh phức tạp (chọnmatch), khi else block dài (chọnmatch), hoặc khi muốn fallback giá trị thay vì exit (chọnunwrap_or/ combinator). - Pattern irrefutable trong
let elseđược compiler warn — vì else không bao giờ chạy, dùngletbình thường là đủ.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- 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ằngs.split_whitespace().next(). Dùnglet elseđể trảNonenếu chuỗi rỗng, ngược lại trảSome(word)vớiwordđãto_uppercase(). - Đ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}"); } - Refactor hàm sau từ nested
if letsang chuỗilet elsephẳ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 } } - Hàm
fn unwrap_or_panic(opt: Option<String>) -> Stringtrả String bên trong opt, panic nếu None. Viết bằnglet else. Có nên dùng cách viết này trong production code khioptđến từ user input không? Vì sao? - 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
fn first_word(s: &str) -> Option<String> { let Some(word) = s.split_whitespace().next() else { return None; }; Some(word.to_uppercase()) }— return type đổi sangOption<String>vìto_uppercase()tạo String mới (allocate). Nếu giữOption<&str>thì không thể uppercase được.- 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êmreturn;sauprintln!. 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.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ảResulthoặcOptionđể caller xử lý;panic!chỉ phù hợp khi None là logic bug không thể xảy ra ở runtime (invariant nội bộ).- Compiler warn (không error): "irrefutable `let...else` pattern". Pattern
xluôn match mọi giá trị, else block không bao giờ chạy — dòng này tương đươnglet x = 5;. Sửa: bỏelse { return; };, viếtlet x = 5;bình thường.
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.
