Danh sách bài viết

Bài 48: if / else if / else

Bài 48 của series Rust Cơ Bản — cấu trúc rẽ nhánh cốt lõi của Rust: cú pháp if / else if / else KHÔNG cần ngoặc tròn quanh condition (khác C / JS), condition BẮT BUỘC là bool strict (Rust không có truthy / falsy như Python / JS, viết if 1 { } sẽ compile error), block { } bắt buộc cho mọi nhánh kể cả 1 statement (không có inline if cond stmt; như C), else if chain theo thứ tự first-match-wins, kết hợp logic && || short-circuit vs bitwise & | eager, preview if let cho pattern match đơn, anti-pattern == true / .len() > 0 bị clippy bắt, và cách flatten nested if sâu bằng early return (Bài 47) hoặc match (Group 14).

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

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 / else vớ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à bool thuần — không tự coi 0 / "" / Nonefalsy như Python / JavaScript. Viết if 1 { } trong Rust là compile error.
  • Biết mỗi nhánh if / else if / else bắt buộc có block { } — không có cú pháp inline kiểu C if (cond) stmt;.
  • Viết được else if chain với thứ tự first-match-wins, biết khi nào nên chuyển sang match để 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ần bool).
  • Đọc được if let ở mức preview — biết đây là shortcut cho match vớ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 lint bool_comparisoncomparison_to_empty.
  • Biết cách flatten nested if sâu bằng early return (Bài 47) hoặc match (Group 14) — code 5 tầng if lồng nhau là dấu hiệu cần refactor.
2

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 { }.

3

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à bool nên x = 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 khi if s kiể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 x coi 0, "", [], None, False là falsy.
  • JavaScript: if (x) coi 0, "", null, undefined, NaN, false là 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 NULL là false.
  • Rust / Go: condition phảibool thuần. Đây là design lành mạnh hơn.
4

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 if trong 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 — if là 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
}
5

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.

6

Đ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ới bool) — 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.

7

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>Some hay None, ResultOk 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 match chỉ có 2 arm (1 pattern care + _ bỏ qua) — if let ngắ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());
}
8

Anti-Pattern: Redundant Comparison

Có vài cách viết if tuy đúng cú pháp nhưng thừaclippy 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() == 0 nó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ỉ check head == None (O(1)). Với String/Vec thì cả 2 đều O(1) nhưng vẫn nên nhất quán dùng is_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.

9

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.

10

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 error expected 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 if chain — first-match-wins, đánh giá tuần tự. ≥ 4 nhánh trên cùng 1 biến → chuyển sang match.
  • && / || short-circuit (lười, an toàn cho side effect) vs & / | eager (luôn evaluate cả 2 vế). Dùng && / || trong condition if gần như luôn đúng.
  • if let — preview pattern match, shortcut cho match vớ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 lint bool_comparison, comparison_to_empty, needless_bool. Fix: viết thẳng if x, dùng !is_empty().
  • Nested if sâu ≥ 3 tầng — flatten bằng early return (Bài 47), let else (Bài 105), hoặc chuyển sang match (Group 14).
11

Bài Tập Củng Cố

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

  1. Đ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ết if x trực tiếp.
  2. Viết function fn weather(temp: i32) -> &'static str trả: "lạnh" khi temp < 15, "mát" khi 15 ≤ temp < 25, "nóng" khi temp ≥ 25. Dùng if / else if / else.
  3. 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"); }
    }
  4. Đ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ý");
            }
        }
    }
  5. 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
  1. (a) if x != 0 { println!("ok"); }. (b) Nếu x: Option<i32>if x.is_some() { ... } hoặc if let Some(_) = x { ... }. Rust không có truthy/falsy: condition phải có type bool để 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".
  2. fn weather(temp: i32) -> &'static str {
        if temp < 15 {
            "lạnh"
        } else if temp < 25 {
            "mát"
        } else {
            "nóng"
        }
    }
    Không cần điều kiện temp >= 15 ở nhánh giữa vì else if đã đảm bảo temp >= 15 (nhánh trên đã false).
  3. Output:
    [eval A]
    OR ok
    [eval C]
    Giải thích: với ||, log("A", true) trả true ngay nên short-circuit bỏ qua log("B", ...) → in "OR ok". Với &&, log("C", false) trả false → short-circuit bỏ qua log("D", ...), "AND ok" không in.
  4. 2 lint: clippy::len_zero (.len() > 0!is_empty()) và clippy::bool_comparison (ready == trueready). Sửa:
    fn check(items: &[i32], ready: bool) {
        if !items.is_empty() && ready {
            println!("xử lý");
        }
    }
    Bonus: đổi &Vec<i32>&[i32] (slice) cho idiomatic — lint clippy::ptr_arg.
  5. fn 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)
    }
    Flatten từ 3 tầng nest xuống tuyến tính: 1 let else + 2 guard clause + happy path. Mỗi điều kiện đảo ngược logic gốc và return Err sớm.
12

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.