Luồng điều khiển (Control Flow)

Luồng điều khiển sẽ quyết định cách code của chúng ta được hoạt động như thế nào. Các cấu trúc phổ biến nhất cho phép bạn kiểm soát luồng thực thi code của Rust là biểu thức if và vòng lặp (loops).

Biểu thức if

Biểu thức if cho phép bạn phân nhánh code phụ thuộc vào các điều kiện. Bạn đưa ra một điều kiện, sau đó yêu cầu: “Nếu thỏa mãn điều kiện này, hãy chạy code này. Nếu điều kiện này không thỏa mãn, không chạy code này.”

Khởi tạo một dự án mới có tên branches trong thư mực projects của bạn để tìm hiểu về biểu thức if. Trong file src/main.rs, hãy nhập code như bên dưới:

Filename: src/main.rs

fn main() {
    let number = 3;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

Tất cả biểu thức if bắt đầu với từ khóa if, theo sau là một điều kiện. Trong trường hợp này, điều kiện sẽ kiểm tra xem liệu biến number có giá trị nhỏ hơn 5 hay không. Ngay sau điều kiện, bên trong cặp dấu ngoặc nhọn, chúng ta viết code cần thực thi nếu điều kiện đúng. Đoạn code liên kết với điều kiện trong biểu thức if thường được gọi là nhánh (arms), giống như arms trong biểu thức match mà chúng ta đã đề cập trong phần “So sánh số dự đoán với số bí mật” ở Chương 2.

Chúng ta cũng có thể tùy ý bao gồm thêm biểu thức else, như chúng ta đã làm ở đây, để cung cấp cho chương trình một đoạn code thay thế khác để thực thi nếu điều kiện sai. Nếu bạn không cung cấp biểu thức else và điều kiện không thỏa mãn, chương trình sẽ bỏ qua đoạn code chứa if và chuyển sang đoạn code tiếp theo.

Hãy thử chạy code này; bạn sẽ nhận được kết quả như sau:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
condition was true

Hãy thử thay đổi giá trị của biến number thành một giá trị làm cho điều kiện sai để xem điều gì xảy ra:

fn main() {
    let number = 7;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

Chạy chương trình một lần nữa và xem kết quả:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
condition was false

Cũng cần lưu ý rằng điều kiện trong code phảibool. Nếu điều kiện không phải là bool, chúng ta sẽ gặp lỗi. Ví dụ, bạn hãy thử chạy đoạn code sau đây:

Filename: src/main.rs

fn main() {
    let number = 3;

    if number {
        println!("number was three");
    }
}

Lần này điều kiện if có giá trị là 3 và Rust đưa ra lỗi:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: mismatched types
 --> src/main.rs:4:8
  |
4 |     if number {
  |        ^^^^^^ expected `bool`, found integer

For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` due to previous error

Lỗi chỉ ra rằng Rust mong đợi bool nhưng lại nhận được số nguyên. Không giống như các ngôn ngữ khác như Ruby và JavaScript, Rust sẽ không tự động chuyển đổi các kiểu dữ liệu không phải Boolean sang Boolean. Bạn phải rõ ràng và luôn luôn cung cấp Boolean cho câu điều kiện if. Ví dụ, nếu bạn muốn đoạn code if chỉ chạy khi một số khác 0, chúng ta có thể thay đổi biểu thức if như sau:

Filename: src/main.rs

fn main() {
    let number = 3;

    if number != 0 {
        println!("number was something other than zero");
    }
}

Khởi chạy đoạn code này sẽ in ra number was something other than zero.

Xử lý nhiều điều kiện với else if

Bạn có thể so sánh nhiều điều kiện bằng cách kết hợp ifelse trong biểu thức else if Ví dụ như:

Filename: src/main.rs

fn main() {
    let number = 6;

    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }
}

Chương trình này có 4 điều kiện có thể thực hiện. Sau khi chạy code, bạn sẽ thấy kết quả như bên dưới:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
number is divisible by 3

Khi thực thi chương trình, Rust sẽ kiểm tra lần lượt mỗi biểu thức if và thực thi đoạn code đầu tiên mà điều kiện thỏa mãn. Lưu ý rằng 6 chia hết cho 2, chúng ta không thấy kết quả đầu ra là đoạn text number is divisible by 2 cũng như đoạn text number is not divisible by 4, 3, or 2 từ else. Bởi vì Rust chỉ thực thi đoạn code cho điều kiện đúng đầu tiên và nó sẽ bỏ qua phần code còn lại.

Sử dụng quá nhiều biểu thức else if có thể làm lộn xộn code của bạn, vì vậy nếu bạn có nhiều hơn một biểu thức, bạn có thể cần tái cấu trúc lại code của mình. Chương 6 giới thiệu một cấu trúc phân nhánh mạnh mẽ, match, cho các trường hợp này.

Sử dụng if trong câu lệnh let

Bởi vì if là một biểu thức, chúng ta có thể sử dụng nó bên phải câu lệnh let để giá kết quả cho biến, như trong Listing 3-2.

Filename: src/main.rs

fn main() {
    let condition = true;
    let number = if condition { 5 } else { 6 };

    println!("The value of number is: {}", number);
}

Listing 3-2: Gán kết quả của biểu thức if cho một biến

Biến number sẽ được gán với giá trị dựa vào kết quả của biểu thức if. Hãy chạy đoạn code này để xem điều gì xảy ra:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/branches`
The value of number is: 5

Hãy nhớ rằng đoạn code đánh giá đến biểu thức cuối cùng và các số cũng bản thân nó
cũng là biểu thức. Trong trường hợp này, giá trị của toàn bộ biểu thức if phụ thuộc vào đoạn code mà nó thực thi. Điều này có nghĩa là các giá trị của mỗi nhánh if phải có kiểu dữ liệu giống nhau; trong Listing 3-2, kết quả từ cả nhánh if và nhánh else đều là số nguyên i32. Nếu kiểu dữ liệu không khớp, bạn sẽ gặp lỗi như trong ví dụ dưới đây:

Filename: src/main.rs

fn main() {
    let condition = true;

    let number = if condition { 5 } else { "six" };

    println!("The value of number is: {}", number);
}

Khi bạn biên dịch code này, bạn sẽ gặp lỗi. Nhánh ifelse có kiểu giá trị không tương thích và Rust chỉ ra chính xác vị trí vấn đề trong chương trình:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: `if` and `else` have incompatible types
 --> src/main.rs:4:44
  |
4 |     let number = if condition { 5 } else { "six" };
  |                                 -          ^^^^^ expected integer, found `&str`
  |                                 |
  |                                 expected because of this

For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` due to previous error

Biểu thức trong khối if đưa ra một số nguyên và biểu thức trong khối else đưa ra một string. Điều này sẽ không thực hiện được bởi vì các biến phải có một kiểu duy nhất và Rust cần biết tại thời điểm biên dịch biến number có kiểu gì. Biết được kiểu của number cho phép trình biên dịch xác minh kiểu đó hợp lệ ở mọi chỗ khi chúng ta sử dụng number. Rust không thể làm điều đó nếu kiểu của number chỉ được xác định tại thời điểm runtime; trình biên dịch sẽ phức tạp hơn và sẽ ít đảm bảo hơn về code nếu nó phải theo dõi nhiều kiểu giả định cho bất kỳ biến nào.

Lặp lại với các vòng lặp (Loops)

Việc thực thi một đoạn code nhiều lần sẽ rất hữu ích. Để làm điều này, Rust cung cấp một số vòng lặp (loop), vòng lặp này sẽ chạy code bên trong thân của vòng lặp từ đầu đến cuối và sau đó ngay lập tức bắt đầu lại từ đầu. Để trải nghiệm với vòng lặp, hãy khởi tạo một dự án mới có tên loops.

Rust có 3 loại vòng lặp: loop, while, và for. Hãy thử từng loại.

Lặp lại code với loop

Từ khóa loop yêu cầu Rust thực thi một đoạn code lặp đi lặp lại mãi mãi hoặc cho đến khi bạn yêu cầu nó dừng lại.

Ví dụ, thay đổi file src/main.rs trong thư mục loops như bên dưới:

Filename: src/main.rs

fn main() {
    loop {
        println!("again!");
    }
}

Khi bạn chạy chương trình này, bạn sẽ thấy kết quả again! được in liên tục cho đến khi chúng ta dừng chương trình một cách thủ công. Hầu hết terminal hỗ trợ phím tắt ctrl-c để ngắt một chương trình bị mắc kẹt trong một vòng lặp liên tục. Hãy thử một lần:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished dev [unoptimized + debuginfo] target(s) in 0.29s
     Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!

Ký hiệu ^C thể hiện vị trí bạn nhấn ctrl-c . Bạn có thể gặp lại từ again! này hoặc không được in ra sau ^C, phụ thuộc vào vị trí của code trong vòng lặp khi nó nhận được tín hiệu ngắt.

May mắn thay, Rust cũng cung cấp một cách để thoát khỏi vòng lặp bằng cách sử dụng code. Bạn có thể đặt từ khóa break trong vòng lặp để cho chương trình biết khi nào nên dừng thực hiện vòng lặp. Chúng ta đã làm điều này trong trò chơi đoán số trong phần “Thoát ra sau khi đoán đúng” ở Chương 2 để thoát khỏi chương trình khi người dùng đoán đúng con số bí mật.

Chúng ta cũng đã dùng continue trong trò chơi đoán số, trong vòng lặp continue sẽ yêu cầu chương trình bỏ qua đoạn code còn lại trong lần lặp này của vòng lặp và đi đến vòng lặp tiếp theo.

Nếu bạn có vòng lặp lồng bên trong vòng lặp, breakcontinue áp dụng cho vòng lặp trong cùng tại điểm đó. Bạn có thể tùy chọn chỉ định gắn nhãn cho vòng lặp, chúng ta có thể sử dụng break hoặc continue để chỉ định rằng các từ khóa đó sẽ áp dụng cho vòng lặp nào được gắn nhãn thay vì vòng lặp trong cùng. Dưới đây là một ví dụ về hai vòng lặp lồng nhau:

fn main() {
    let mut count = 0;
    'counting_up: loop {
        println!("count = {}", count);
        let mut remaining = 10;

        loop {
            println!("remaining = {}", remaining);
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }

        count += 1;
    }
    println!("End count = {}", count);
}

Vòng lặp bên ngoài có nhãn 'counting_up và nó sẽ đếm từ 0 đến 2. Vòng lặp bên trong không có nhãn đếm ngược từ 10 đến 9. Câu lệnhbreak đầu tiên không chỉ định nhãn của vòng lặp nên sẽ chỉ thoát khỏi vòng lặp bên trong. Câu lệnh break 'counting_up; sẽ thoát khỏi vòng lặp ngoài. Code này sẽ in ra:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished dev [unoptimized + debuginfo] target(s) in 0.58s
     Running `target/debug/loops`
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2

Trả về giá trị từ vòng lặp

Một trong các cách sử dụng vòng lặp là thử một phép toán mà bạn biết có thể nó sẽ thất bại, chẳng hạn như kiểm tra xem một luồng có hoàn thành công việc hay không. Bạn cũng có thể cần chuyển kết quả của phép toán đó ra khỏi vòng lặp đến phần còn lại của code. Để làm điều này, bạn có thể thêm giá trị bạn muốn trả về sau biểu thức break mà bạn sử dụng để dừng vòng lặp; giá trị đó sẽ được trả về khỏi vòng lặp và do đó bạn có thể sử dụng giá trị đó như được hiển thị ở đây:

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {}", result);
}

Trước vòng lặp, chúng ta khai báo một biến có tên counter và gán cho nó giá trị 0. Sau đó chúng ta khai báo một biến có tên result để chứa giá trị mà vòng lặp trả về. Trên mỗi lần lặp của vòng lặp, chúng ta cộng 1 vào biến counter và sau đó kiểm tra xem liệu counter bằng 10 hay không. Khi nó thỏa mãn điều kiện, chúng ta sử dụng từ khóa break với counter * 2. Sau vòng lặp, chúng ta sử dụng dấu chấm phẩy để kết thúc câu lệnh gán giá trị vào biến result. Cuối cùng, chúng ta in ra giá trị của result, trong trường hợp này là 20.

Vòng lặp có điều kiện với while

Một chương trình sẽ thường cần đánh giá một điều kiện bên trong vòng lặp. Trong khi điều kiện là đúng, vòng lặp sẽ chạy. Khi điều kiện không còn đúng nữa, chương trình sẽ gọi break để dừng vòng lặp. Có thể triển khai hành vi như vậy bằng cách sử dụng kết hợp loop, if, elsebreak; bạn có thể thử điều đó trong một chương trình ngay nếu bạn muốn. Tùy nhiên, mẫu này phổ biến đến mức Rust đã xây dựng một cấu trúc cho nó gọi là vòng lặp while. Trong Listing 3-3, chúng ta sử dụng while để lặp lại chương trình ba lần, mỗi lần lặp đếm ngược, in ra một thông báo và thoát

Filename: src/main.rs

fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{}!", number);

        number -= 1;
    }

    println!("LIFTOFF!!!");
}

Listing 3-3: Sử dụng vòng lặp while để chạy code trong khi điều kiện đúng

Cấu trúc này loại bỏ nhiều lồng ghép cần thiết nếu bạn sử dụng loop, if, elsebreak và nó rõ ràng hơn nhiều. Trong khi một điều kiện đúng, code sẽ hoạt động; nếu không nó sẽ thoát vòng lặp.

Vòng lặp cho Collection với for

Bạn có thể sử dụng cấu trúc while để lặp qua các phần tử của một collection, chẳng hạn như array. Ví dụ, vòng lặp trong Listing 3-4 in ra từng phần tử trong array a.

Filename: src/main.rs

fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

    while index < 5 {
        println!("the value is: {}", a[index]);

        index += 1;
    }
}

Listing 3-4: Lặp qua từng phần tử của một collection sử dụng vòng lặp while

Ở đây, code sẽ đếm lên thông qua các phần tử trong array. Nó bắt đầu ở chỉ mục 0 và sau đó lặp lại cho đến chỉ mục cuối cùng trong array (đó là khi index < 5 không còn đúng nữa). Khởi chạy code này, nó sẽ in ra mọi phần tử trong array:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished dev [unoptimized + debuginfo] target(s) in 0.32s
     Running `target/debug/loops`
the value is: 10
the value is: 20
the value is: 30
the value is: 40
the value is: 50

Tất cả năm giá trị của array đều xuất hiện trong terminal như mong đợi. Mặc dù index sẽ đạt giá trị 5 tại một số thời điểm , vòng lặp dừng thực thi trước khi cố gắng tìm nạp giá trị thứ sáu từ array.

Tuy nhiên, các tiếp cận này dễ xảy ra lỗi; chúng ta có thể làm chương trình panic nếu giá trị chỉ mục hoặc điều kiện kiểm tra không đúng. Ví dụ nếu bạn thay đổi định nghĩa của array a chỉ có bốn phần tử nhưng quên cập nhật điều kiện thành while index < 4, code khi đó sẽ panic. Nó cũng chậm bởi vì trình biên dịch thêm runtime code để thực hiện kiểm tra có điều kiện liệu chỉ mục có nằm trong giới hạn của array trên mỗi lần lặp qua vòng lặp hay không.

Để thay thế ngắn gọn hơn, bạn có thể sử dụng vòng lặp for và thực thi code cho mỗi item trong collection. Vòng lặp for trong giống như code trong Listing 3-5.

Filename: src/main.rs

fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a {
        println!("the value is: {}", element);
    }
}

Listing 3-5: Lặp qua từng phần tử của collection sử dụng vòng lặp for

Khi chúng ta sử dụng đoạn code này, chúng ta sẽ nhận được kết quả giống hệt như trong Listing 3-4. Quan trọng hơn, chúng ta có thể tăng tính an toàn của code và loại bỏ khả năng lỗi từ việc vượt qua phần tử cuối của array hoặc lặp không hết và thiếu sót một vài item.

Sử dụng vòng lặp for, bạn sẽ không cần phải thay đổi code nếu bạn thay đổi số lượng phần tử của array như với phương pháp mà bạn sử dụng trong Listing 3-4.

Tính an toàn và độ ngắn gọn của vòng lặp for khiến chúng trở thành cấu trúc vòng lặp được sử dụng phổ biến trong Rust. Ngay cả trong các tình huống bạn muốn chạy code một số lần nhất định, như ví dụ đếm ngược đã sử dụng vòng lặp while trong Listing 3-3, phần lớn Rustaceans sẽ sử dụng vòng lặp for. Các để làm điều đó là sử dụng phạm vi (Range), được cung cấp bởi thư viện chuẩn, tạo ra tất cả các số theo thứ tự bắt đầu từ một số và kết thúc trước một số khác.

Dưới đây là cách đếm ngược sử dụng vòng lặp for và một phương thức khác mà chúng ta chưa đề cập đến, rev, để đảo ngược phạm vi:

Filename: src/main.rs

fn main() {
    for number in (1..4).rev() {
        println!("{}!", number);
    }
    println!("LIFTOFF!!!");
}

Code này đẹp hơn một chút rồi phải không?

Tóm tắt

Bạn đã làm được! Đây là một chương khá lớn: bạn đã học về biến, kiểu dữ liệu vô hướng và kết hợp, hàm, comments, biểu thức if và vòng lặp! Để thực hành các khái niệm này, hãy thử xây dựng chương trình để thực hiện những điều sau:

  • Chuyển đổi nhiệt độ giữa Fahrenheit và Celsius.
  • Tạo ra số Fibonacci thứ n.
  • In lời bài hát mừng Giáng sinh “The Twelve Days of Christmas,” tận dụng sự lặp lại trong bài hát.

Khi bạn đã sẵn sàng để tiếp tục, chúng ta sẽ nói về một khái niệm trong Rust không thường tồn tại trong các ngôn ngữ lập trình khác: quyền sở hữu (ownership).