Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Viết được
if/if / else/if / else if / elsevới cú pháp Rust chuẩn — biết rằng KHÔNG cần ngoặc tròn quanh condition (khác C / JS / Java). - Hiểu vì sao Rust BẮT BUỘC condition là
boolthuần — không tự coi0/""/Nonelà falsy như Python / JavaScript. Viếtif 1 { }trong Rust là compile error. - Biết mỗi nhánh
if/else if/elsebắt buộc có block{ }— không có cú pháp inline kiểu Cif (cond) stmt;. - Viết được
else ifchain với thứ tự first-match-wins, biết khi nào nên chuyển sangmatchđể code rõ hơn. - Dùng đúng
&&/||short-circuit cho logic, phân biệt với&/|bitwise eager (đã học ở Bài 35 phầnbool). - Đọc được
if letở mức preview — biết đây là shortcut chomatchvới 1 pattern, sẽ học sâu ở Bài 103. - Nhận diện và sửa được các anti-pattern:
if x == true,if x.len() > 0— clippy lintbool_comparisonvàcomparison_to_empty. - Biết cách flatten nested
ifsâu bằng early return (Bài 47) hoặcmatch(Group 14) — code 5 tầngiflồng nhau là dấu hiệu cần refactor.
Cú Pháp Cơ Bản
Rust có 3 dạng cấu trúc if theo thứ tự phức tạp tăng dần: chỉ if, if / else 2 nhánh, và if / else if / else chuỗi nhiều nhánh:
fn main() {
let n = 7;
// Dạng 1: chỉ if - chạy block khi cond đúng, không có nhánh ngược
if n > 0 {
println!("dương");
}
// Dạng 2: if / else - 2 nhánh
if n % 2 == 0 {
println!("chẵn");
} else {
println!("lẻ");
}
// Dạng 3: if / else if / else - chuỗi nhiều nhánh
if n < 0 {
println!("âm");
} else if n == 0 {
println!("không");
} else if n < 10 {
println!("dương 1 chữ số");
} else {
println!("dương nhiều chữ số");
}
}
Điểm khác biệt rõ nhất với C / C++ / Java / JavaScript: KHÔNG cần ngoặc tròn ( ) quanh condition. Viết if (n > 0) { ... } vẫn compile được nhưng clippy báo unnecessary_wraps / collapsible_if tùy ngữ cảnh, và idiom Rust là bỏ ngoặc tròn.
Một ví dụ cô đọng — function classify số nguyên thành 4 loại, sẽ tái dùng ở các phần dưới:
fn classify(n: i32) {
if n < 0 {
println!("âm");
} else if n == 0 {
println!("không");
} else if n < 10 {
println!("dương 1 chữ số");
} else {
println!("dương nhiều chữ số");
}
}
fn main() {
classify(-5); // âm
classify(0); // không
classify(7); // dương 1 chữ số
classify(42); // dương nhiều chữ số
}
Cú pháp đơn giản, nhưng Rust nghiêm ngặt hơn các ngôn ngữ khác ở 2 điểm sẽ phân tích kỹ ngay sau: condition phải là bool, và mỗi nhánh phải có block { }.
Condition Phải Là bool Strict
Đây là khác biệt quan trọng nhất so với C / C++ / Python / JavaScript: trong Rust, condition của if bắt buộc có type bool (đã học ở Bài 35). Rust không có khái niệm truthy / falsy — số 0 không phải false, chuỗi rỗng không phải false, None không phải false.
fn main() {
let x = 1;
// COMPILE ERROR - 1 là integer, không phải bool
if x {
println!("not bool");
}
}
Compiler báo lỗi rõ ràng:
error[E0308]: mismatched types
--> src/main.rs:2:8
|
2 | if 1 {
| ^ expected `bool`, found integer
Cách viết đúng phải explicit so sánh để tạo ra giá trị bool:
fn main() {
let x = 1;
let s = String::from("hi");
let v: Vec<i32> = vec![];
let opt: Option<i32> = None;
// ĐÚNG - so sánh tường minh tạo bool
if x != 0 { println!("non-zero"); }
if !s.is_empty() { println!("non-empty string"); }
if !v.is_empty() { println!("non-empty vec"); }
if opt.is_some() { println!("has value"); }
}
Vì sao Rust khắt khe như vậy? Lý do gắn chặt với triết lý ngôn ngữ:
- Tránh bug ngầm: trong C, viết
if (x = 5)(assignment) thay vìif (x == 5)(so sánh) compile bình thường — đây là bug điển hình. Rust ép condition làboolnênx = 5(kiểu()) sẽ fail. - Đọc code rõ hơn:
if !s.is_empty()đọc thành "nếu chuỗi không rỗng" — rõ ý đồ. Trong khiif skiểu Python phụ thuộc người đọc biết quy ước "string rỗng là falsy". - Không có default conversion ngầm: Rust theo nguyên tắc "explicit is better than implicit" — mọi conversion type đều phải tường minh qua
as,From, hoặc method.
So sánh nhanh với các ngôn ngữ khác:
- Python:
if xcoi0,"",[],None,Falselà falsy. - JavaScript:
if (x)coi0,"",null,undefined,NaN,falselà falsy — gây ra hàng đống bug khét tiếng (if (0)false,if ("0")true). - C / C++: bất cứ giá trị numeric / pointer khác 0 là true. Pointer
NULLlà false. - Rust / Go: condition phải là
boolthuần. Đây là design lành mạnh hơn.
Block Bắt Buộc Có { }
Trong C / C++ / Java / JavaScript, bạn có thể viết if (cond) statement; không cần ngoặc { } khi chỉ có 1 statement. Rust KHÔNG cho phép cú pháp này — mỗi nhánh bắt buộc có block { }, kể cả khi body chỉ là 1 statement:
fn main() {
let n = 5;
// SAI - không có block { }, compile error
// if n > 0 println!("dương");
//
// error: expected `{`, found `println`
// ĐÚNG - bắt buộc { } kể cả 1 statement
if n > 0 { println!("dương"); }
// Nhiều statement - cùng cú pháp
if n > 0 {
println!("dương");
println!("xử lý tiếp...");
}
}
Đây không phải hạn chế — đây là design có chủ ý ngăn ngừa một class bug nổi tiếng trong C / JS: dangling else / missing braces. Ví dụ kinh điển từ Apple "goto fail" (CVE-2014-1266) trong code C:
// Pseudo-code minh họa lỗi goto-fail của Apple SSL/TLS
if ((err = SSLHashSHA1.update(...)) != 0)
goto fail;
goto fail; // dòng này LUÔN chạy do indent gây hiểu lầm,
// bypass toàn bộ kiểm tra chứng chỉ HTTPS
if ((err = SSLHashSHA1.final(...)) != 0)
goto fail;
Trong Rust, bug này không thể xảy ra vì cú pháp ép bạn dùng { } — indent không quyết định scope. Một số lợi ích khác:
- Đồng nhất phong cách: mọi nhánh
iftrong codebase Rust trông giống nhau — không có biến thể "1 dòng inline" vs "block" gây tranh cãi review. - Code dễ thêm dòng: muốn thêm statement thứ 2 vào nhánh chỉ cần gõ thêm — không phải nhớ bọc
{ }lại. - Block là expression: trong Rust mọi block
{ }là expression có thể trả value (sẽ học sâu ở Bài 49 —iflà expression). Cú pháp inline không hỗ trợ được điều này.
Nếu thật sự muốn if 1 dòng cho ngắn, viết tất cả trên một dòng vẫn được — chỉ là vẫn phải có { }:
fn main() {
let x = 10;
if x > 0 { println!("dương"); } // 1 dòng - OK
if x > 0 { println!("a"); println!("b"); } // 1 dòng nhiều stmt - OK
}
else if Chain
else if chain là chuỗi các condition đánh giá theo thứ tự từ trên xuống — nhánh ĐẦU TIÊN match (true) sẽ chạy, các nhánh sau bị bỏ qua hoàn toàn. Nguyên tắc first-match-wins:
fn grade(score: i32) -> &'static str {
if score >= 90 {
"A"
} else if score >= 80 {
// score chắc chắn < 90 ở đây (nhánh trên đã false)
"B"
} else if score >= 70 {
// score chắc chắn < 80
"C"
} else if score >= 60 {
"D"
} else {
"F"
}
}
fn main() {
println!("{}", grade(95)); // A
println!("{}", grade(82)); // B - nhánh 90 false, nhánh 80 true, dừng
println!("{}", grade(72)); // C
println!("{}", grade(55)); // F - mọi nhánh trên đều false, rơi vào else
}
Lưu ý thứ tự: nếu đảo nhánh sai, kết quả thay đổi hoàn toàn. Ví dụ viết if score >= 60 { "D" } else if score >= 90 { "A" } thì điểm 95 sẽ ra "D" vì nhánh đầu match trước.
Khi else if chain dài hơn 4-5 nhánh và tất cả đều so với cùng 1 biến, đó là dấu hiệu nên chuyển sang match (Group 14) — rõ ràng hơn, compiler check exhaustive:
// else if chain - chạy được nhưng verbose khi nhiều nhánh
fn day_name(d: u8) -> &'static str {
if d == 1 { "Mon" }
else if d == 2 { "Tue" }
else if d == 3 { "Wed" }
else if d == 4 { "Thu" }
else if d == 5 { "Fri" }
else if d == 6 { "Sat" }
else if d == 7 { "Sun" }
else { "?" }
}
// match - idiom Rust, ngắn và exhaustive check tốt hơn (sẽ học Bài 98)
fn day_name_match(d: u8) -> &'static str {
match d {
1 => "Mon",
2 => "Tue",
3 => "Wed",
4 => "Thu",
5 => "Fri",
6 => "Sat",
7 => "Sun",
_ => "?",
}
}
Rule of thumb: ≤ 3 nhánh, condition khác kiểu → dùng if / else if / else. ≥ 4 nhánh hoặc cùng 1 biến / pattern → dùng match.
Điều Kiện Logic Kết Hợp — && || vs & |
Để ghép nhiều condition, Rust có 2 cặp operator khác nhau dù trông giống nhau — đã đề cập ở Bài 35 phần bool:
&&/||: logical AND / OR — short-circuit (lười). Vế phải chỉ được evaluate nếu cần.&/|: bitwise AND / OR (với integer) hoặc logical AND / OR không short-circuit (vớibool) — eager: luôn evaluate cả 2 vế.
fn is_safe(x: i32) -> bool {
println!("[gọi is_safe({x})]");
x > 0
}
fn main() {
let a = -5;
let b = 10;
// SHORT-CIRCUIT với &&
// nếu a > 0 false thì KHÔNG gọi is_safe(b)
if a > 0 && is_safe(b) {
println!("cả 2 ok");
}
// Output: (không có log "[gọi is_safe(10)]")
// SHORT-CIRCUIT với ||
// nếu a < 0 true thì KHÔNG gọi is_safe(b)
if a < 0 || is_safe(b) {
println!("ít nhất 1 ok");
}
// Output: ít nhất 1 ok
// EAGER với & - LUÔN gọi is_safe(b) dù vế trái đã false
if (a > 0) & is_safe(b) {
println!("dùng & - cả 2 vế đều evaluate");
}
// Output: [gọi is_safe(10)]
// Kết hợp nhiều condition phức tạp - dùng dấu ngoặc cho rõ
let age = 25;
let has_license = true;
let is_drunk = false;
if age >= 18 && has_license && !is_drunk {
println!("được lái xe");
}
}
Khi nào nên dùng && / || (gần như luôn): khi vế phải có side effect hoặc cost, không muốn chạy khi vế trái đã quyết định kết quả. Ví dụ:
fn main() {
let opt: Option<&str> = None;
// ĐÚNG - short-circuit, không gọi unwrap() khi None
if opt.is_some() && opt.unwrap().len() > 5 {
println!("string dài");
}
// SAI nếu dùng & - eager, sẽ gọi opt.unwrap() trên None -> panic!
// if opt.is_some() & (opt.unwrap().len() > 5) { ... }
}
Khi nào dùng & / |: rất hiếm trong if condition. Chủ yếu dùng cho bitwise operation trên integer (đọc/set bit flag), không phải cho logic.
if let — Preview Pattern Match
Khi cần kiểm tra một giá trị có khớp với pattern hay không (tiêu biểu là Option<T> có Some hay None, Result có Ok hay Err), Rust có cú pháp if let — shortcut cho match với 1 pattern. Ở bài này chỉ preview; bài sâu là Bài 103.
fn main() {
let maybe_name: Option<&str> = Some("Canh");
// CÁCH DÀI - dùng match (sẽ học Bài 98)
match maybe_name {
Some(n) => println!("Xin chào, {n}!"),
None => { /* không làm gì */ }
}
// CÁCH NGẮN - if let (idiom khi chỉ care 1 pattern)
if let Some(n) = maybe_name {
println!("Xin chào, {n}!");
}
// if let có thể đi kèm else
let no_name: Option<&str> = None;
if let Some(n) = no_name {
println!("Có tên: {n}");
} else {
println!("Không có tên");
}
}
Cú pháp đọc là: "nếu giá trị bên phải khớp pattern bên trái thì bind biến và chạy block". Trong if let Some(n) = maybe_name, n được bind giá trị bên trong Some, sẵn sàng dùng trong block.
Khi nào dùng if let thay if cond thường:
- Khi giá trị có pattern (
Option,Result, enum custom) và bạn cần bind giá trị bên trong. - Khi
matchchỉ có 2 arm (1 pattern care +_bỏ qua) —if letngắn hơn.
Ngược lại, không lạm dụng if let khi có nhiều pattern cần xử lý — dùng match để compiler check exhaustive. Một biến thể mạnh hơn cho early-return là let else (Bài 105):
fn print_len(opt: Option<&str>) {
// let else: bind nếu match, else block phải diverge (return/panic/break)
let Some(s) = opt else {
println!("không có giá trị");
return;
};
// dòng dưới biết chắc s là &str (đã unwrap)
println!("độ dài: {}", s.len());
}
Anti-Pattern: Redundant Comparison
Có vài cách viết if tuy đúng cú pháp nhưng thừa — clippy sẽ bắt và nhắc bạn sửa. Đây là 2 anti-pattern hay gặp nhất ở newcomer:
fn main() {
let is_logged_in = true;
let s = String::from("hello");
let v = vec![1, 2, 3];
// ANTI-PATTERN 1: so sánh bool với true / false
if is_logged_in == true { // thừa "== true"
println!("logged in");
}
if is_logged_in == false { // thừa "== false"
println!("not logged in");
}
// clippy warning:
// warning: equality checks against true are unnecessary
// = help: try simplifying it as shown:
// | if is_logged_in {
// (lint: clippy::bool_comparison)
// ĐÚNG - bool tự là condition
if is_logged_in { println!("logged in"); }
if !is_logged_in { println!("not logged in"); }
// ANTI-PATTERN 2: kiểm tra rỗng qua so sánh length
if s.len() > 0 { // thừa
println!("string không rỗng");
}
if v.len() == 0 { // thừa
println!("vec rỗng");
}
// clippy warning:
// warning: length comparison to zero
// = help: using `!is_empty` is clearer and more explicit
// (lint: clippy::comparison_to_empty / len_zero)
// ĐÚNG - dùng is_empty()
if !s.is_empty() { println!("string không rỗng"); }
if v.is_empty() { println!("vec rỗng"); }
}
Vì sao is_empty() tốt hơn .len() == 0?
- Rõ ý đồ hơn:
is_empty()nói "tôi muốn biết có rỗng không";len() == 0nói "tôi muốn biết độ dài có bằng 0 không" — phải suy ra ý. - Có thể nhanh hơn: với một số collection (ví dụ
LinkedList),.len()phải duyệt O(n), còn.is_empty()chỉ checkhead == None(O(1)). VớiString/Vecthì cả 2 đều O(1) nhưng vẫn nên nhất quán dùngis_empty().
Anti-pattern khác cùng họ: if cond { true } else { false } → viết thẳng cond. Lint clippy::needless_bool. Idiom Rust: tin compiler và clippy, tránh boilerplate.
Nested if vs Flatten
Khi business logic phức tạp, if lồng nhau 3-4-5 tầng là chuyện thường — gọi là arrow code (do indent dồn về bên phải tạo hình mũi tên). Đọc rất khó, dễ bug khi sửa:
// XẤU - nested 4 tầng, khó đọc, hard to follow
fn process_user(user_opt: Option<&str>, age: i32, has_license: bool) -> Result<String, String> {
if let Some(name) = user_opt {
if !name.is_empty() {
if age >= 18 {
if has_license {
Ok(format!("{name} được lái xe"))
} else {
Err("chưa có bằng lái".into())
}
} else {
Err("chưa đủ tuổi".into())
}
} else {
Err("tên rỗng".into())
}
} else {
Err("không có user".into())
}
}
Fix bằng early return (đã học Bài 47) — đảo điều kiện thành guard clause, thoát sớm khi sai, để happy path nằm thẳng ở dưới:
// TỐT - flatten bằng early return, đọc tuyến tính từ trên xuống
fn process_user(user_opt: Option<&str>, age: i32, has_license: bool) -> Result<String, String> {
let Some(name) = user_opt else {
return Err("không có user".into());
};
if name.is_empty() { return Err("tên rỗng".into()); }
if age < 18 { return Err("chưa đủ tuổi".into()); }
if !has_license { return Err("chưa có bằng lái".into()); }
// Happy path - không indent, dễ đọc
Ok(format!("{name} được lái xe"))
}
Phiên bản này có cùng logic nhưng:
- Indent phẳng — không có "mũi tên" lệch phải.
- Mỗi guard rõ ràng — đọc đến đâu biết đến đó: "nếu sai thì thoát, đi tiếp nghĩa là đúng".
- Happy path nổi bật — dòng cuối là kết quả thành công, không phải tìm trong 4 tầng nest.
Một fix khác — khi nest phức tạp hơn và có nhiều enum / pattern: chuyển sang match (Group 14, đặc biệt Bài 100 match guards). Quy tắc chung khi review code Rust: ≥ 3 tầng if lồng nhau là dấu hiệu refactor.
Tổng Kết
- Rust có 3 dạng
if: chỉif,if / else,if / else if / else. KHÔNG cần ngoặc tròn quanh condition. - Condition BẮT BUỘC là
bool— Rust không có truthy / falsy.if 1 { }compile errorexpected bool, found integer. Phải viết explicit:if x != 0,if !v.is_empty(). - Mỗi nhánh bắt buộc có block
{ }, kể cả 1 statement — không có cú pháp inline kiểu C. Ngăn ngừa class bug "goto fail / missing braces". else ifchain — first-match-wins, đánh giá tuần tự. ≥ 4 nhánh trên cùng 1 biến → chuyển sangmatch.&&/||short-circuit (lười, an toàn cho side effect) vs&/|eager (luôn evaluate cả 2 vế). Dùng&&/||trong conditionifgần như luôn đúng.if let— preview pattern match, shortcut chomatchvới 1 pattern. Sẽ học sâu Bài 103. Biến thể mạnh hơn cho early-return:let else(Bài 105).- Anti-pattern:
if x == true,if x.len() > 0— clippy bắt qua lintbool_comparison,comparison_to_empty,needless_bool. Fix: viết thẳngif x, dùng!is_empty(). - Nested
ifsâu ≥ 3 tầng — flatten bằng early return (Bài 47), let else (Bài 105), hoặc chuyển sangmatch(Group 14).
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Đoạn code
fn check(x: i32) { if x { println!("ok"); } }không compile. Sửa lại 2 cách: (a) so sánh tường minh, (b) dùng method có sẵn nếu x làOption<i32>thay vìi32. Giải thích vì sao Rust không cho viếtif xtrực tiếp. - Viết function
fn weather(temp: i32) -> &'static strtrả:"lạnh"khitemp < 15,"mát"khi15 ≤ temp < 25,"nóng"khitemp ≥ 25. Dùngif / else if / else. - Phân tích đoạn dưới và cho biết output, giải thích short-circuit:
fn log(name: &str, val: bool) -> bool { println!("[eval {name}]"); val } fn main() { if log("A", true) || log("B", false) { println!("OR ok"); } if log("C", false) && log("D", true) { println!("AND ok"); } } - Đoạn code dưới bị clippy bắt với 2 lint khác nhau. Đọc và sửa cho idiomatic:
fn check(items: &Vec<i32>, ready: bool) { if items.len() > 0 { if ready == true { println!("xử lý"); } } } - Refactor function nested dưới bằng early return /
let else:fn handle(opt: Option<i32>) -> Result<i32, &'static str> { if let Some(n) = opt { if n > 0 { if n < 100 { Ok(n * 2) } else { Err("quá lớn") } } else { Err("không dương") } } else { Err("rỗng") } }
Đáp án
- (a)
if x != 0 { println!("ok"); }. (b) Nếux: Option<i32>→if x.is_some() { ... }hoặcif let Some(_) = x { ... }. Rust không có truthy/falsy: condition phải có typeboolđể loại trừ bug ngầm (nhưif (x = 5)trong C) và để code đọc tường minh — không phụ thuộc quy ước "0 là falsy".
Không cần điều kiệnfn weather(temp: i32) -> &'static str { if temp < 15 { "lạnh" } else if temp < 25 { "mát" } else { "nóng" } }temp >= 15ở nhánh giữa vìelse ifđã đảm bảotemp >= 15(nhánh trên đã false).- Output:
Giải thích: với[eval A] OR ok [eval C]||,log("A", true)trảtruengay nên short-circuit bỏ qualog("B", ...)→ in "OR ok". Với&&,log("C", false)trảfalse→ short-circuit bỏ qualog("D", ...), "AND ok" không in. - 2 lint:
clippy::len_zero(.len() > 0→!is_empty()) vàclippy::bool_comparison(ready == true→ready). Sửa:
Bonus: đổifn check(items: &[i32], ready: bool) { if !items.is_empty() && ready { println!("xử lý"); } }&Vec<i32>→&[i32](slice) cho idiomatic — lintclippy::ptr_arg.
Flatten từ 3 tầng nest xuống tuyến tính: 1fn handle(opt: Option<i32>) -> Result<i32, &'static str> { let Some(n) = opt else { return Err("rỗng"); }; if n <= 0 { return Err("không dương"); } if n >= 100 { return Err("quá lớn"); } Ok(n * 2) }let else+ 2 guard clause + happy path. Mỗi điều kiện đảo ngược logic gốc vàreturn Errsớm.
Bài Tiếp Theo
Bài 49: if Là Expression — let x = if cond { a } else { b } — đào sâu một đặc trưng của Rust: if không chỉ là statement điều khiển luồng mà còn là expression trả value. Bạn sẽ viết let max = if a > b { a } else { b }; theo idiom ternary, hiểu vì sao cả 2 nhánh phải cùng type, đọc được compile error if and else have incompatible types, và thấy if-as-expression kết hợp với match-as-expression tạo nên phong cách functional gọn của Rust.
