Luồng điều khiển
Khả năng chạy một đoạn mã tùy thuộc vào việc một điều kiện là true
và chạy lặp đi lặp lại khi điều kiện là true
là những khối xây dựng cơ bản trong hầu hết các ngôn ngữ lập trình. Các cấu trúc phổ biến nhất giúp bạn điều khiển luồng thực thi của mã Rust là các biểu thức if
và các vòng lặp.
Biểu thức if
Một biểu thức if
cho phép bạn rẽ nhánh mã tùy theo điều kiện. Bạn cung cấp một điều kiện rồi nêu: “Nếu điều kiện này thỏa, chạy khối mã này. Nếu điều kiện không thỏa, đừng chạy khối mã này.”
Tạo một dự án mới tên là branches trong thư mục projects của bạn để khám phá biểu thức if
. Trong tệp src/main.rs, nhập nội dung sau:
Tên tệp: src/main.rs
fn main() { let number = 3; if number < 5 { println!("condition was true"); } else { println!("condition was false"); } }
Tất cả cá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 kiểm tra xem biến number
có giá trị nhỏ hơn 5 hay không. Chúng ta đặt khối mã cần thực thi nếu điều kiện là true
ngay sau điều kiện, bên trong dấu ngoặc nhọn. Các khối mã gắn với điều kiện trong biểu thức if
đôi khi được gọi là arm, tương tự như các arm trong biểu thức match
mà chúng ta đã bàn trong phần “So sánh lần đoán với số bí mật” của Chương 2.
Tùy chọn, chúng ta cũng có thể thêm một biểu thức else
, như ở đây, để cung cấp cho chương trình một khối mã thay thế để thực thi nếu điều kiện đánh giá là false
. Nếu bạn không cung cấp else
và điều kiện là false
, chương trình sẽ chỉ bỏ qua khối if
và tiếp tục phần mã tiếp theo.
Hãy chạy đoạn mã này; bạn sẽ thấy kết quả sau:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
condition was true
Hãy thử đổi giá trị của number
sang một giá trị khiến điều kiện false
để 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 lại chương trình và xem kết quả:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
condition was false
Cũng đáng lưu ý là điều kiện trong đoạn mã này bắt buộc phải là bool
. Nếu điều kiện không phải là bool
, chúng ta sẽ nhận lỗi. Ví dụ, thử chạy đoạn mã sau:
Tên tệp: src/main.rs
fn main() {
let number = 3;
if number {
println!("number was three");
}
}
Lần này điều kiện if
đánh giá ra giá trị 3
, và Rust báo 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` (bin "branches") due to 1 previous error
Lỗi cho biết Rust mong đợi bool
nhưng nhận được một số nguyên. Không giống các ngôn ngữ như Ruby và JavaScript, Rust sẽ không tự động cố chuyển các kiểu không phải Boolean thành Boolean. Bạn phải rõ ràng và luôn cung cấp một giá trị Boolean làm điều kiện cho if
. Nếu chúng ta muốn khối if
chỉ chạy khi một số khác 0
, chẳng hạn, ta có thể đổi biểu thức if
như sau:
Tên tệp: src/main.rs
fn main() { let number = 3; if number != 0 { println!("number was something other than zero"); } }
Chạy đoạn mã này sẽ in number was something other than zero
.
Xử lý nhiều điều kiện với else if
Bạn có thể dùng nhiều điều kiện bằng cách kết hợp if
và else
trong một biểu thức else if
. Ví dụ:
Tên tệp: 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ó bốn đường đi có thể thực hiện. Sau khi chạy, bạn sẽ thấy kết quả sau:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
number is divisible by 3
Khi chương trình thực thi, nó kiểm tra từng biểu thức if
theo thứ tự và thực thi khối đầu tiên có điều kiện đánh giá là true
. Lưu ý rằng mặc dù 6 chia hết cho 2, chúng ta không thấy dòng number is divisible by 2
, cũng không thấy dòng number is not divisible by 4, 3, or 2
từ khối else
. Đó là vì Rust chỉ thực thi khối cho điều kiện true
đầu tiên, và khi đã tìm thấy, nó sẽ không kiểm tra các điều kiện còn lại.
Dùng quá nhiều else if
có thể khiến mã rối rắm, nên nếu bạn có nhiều hơn một else if
, bạn có thể muốn tái cấu trúc. Chương 6 mô tả một cấu trúc rẽ nhánh mạnh mẽ trong Rust gọi là match
cho những trường hợp như vậy.
Dùng if
trong câu lệnh let
Vì if
là một biểu thức, ta có thể dùng nó ở vế phải của câu lệnh let
để gán kết quả cho một biến, như trong Liệt kê 3-2.
fn main() { let condition = true; let number = if condition { 5 } else { 6 }; println!("The value of number is: {number}"); }
if
cho biếnBiến number
sẽ được gán một giá trị dựa trên kết quả của biểu thức if
. Hãy chạy đoạn mã này để xem điều gì xảy ra:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/branches`
The value of number is: 5
Hãy nhớ rằng các khối mã được đánh giá thành biểu thức cuối cùng trong khối, và các con số tự chúng 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 khối nào được thực thi. Điều này có nghĩa là các giá trị có thể trở thành kết quả từ mỗi arm của if
phải cùng kiểu; trong Liệt kê 3-2, kết quả của cả arm if
và arm else
đều là số nguyên i32
. Nếu các kiểu không trùng khớp, như trong ví dụ sau, chúng ta sẽ nhận lỗi:
Tên tệp: src/main.rs
fn main() {
let condition = true;
let number = if condition { 5 } else { "six" };
println!("The value of number is: {number}");
}
Khi cố biên dịch đoạn mã này, chúng ta sẽ nhận lỗi. Các arm if
và else
có kiểu giá trị không tương thích, và Rust chỉ ra chính xác vị trí có 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` (bin "branches") due to 1 previous error
Biểu thức trong khối if
đánh giá thành một số nguyên, còn biểu thức trong khối else
đánh giá thành một chuỗi. Điều này sẽ không hoạt động vì biến phải có một kiểu duy nhất, và Rust cần biết kiểu của biến number
ngay tại thời điểm biên dịch, một cách dứt khoát. Biết kiểu của number
cho phép trình biên dịch xác minh kiểu đó là hợp lệ ở mọi nơi ta dùng number
. Rust sẽ không thể làm điều đó nếu kiểu của number
chỉ được xác định lúc chạy; trình biên dịch sẽ phức tạp hơn và đưa ra ít đảm bảo hơn về mã 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
Thường thì chúng ta cần thực thi một khối mã nhiều hơn một lần. Để làm việc này, Rust cung cấp nhiều vòng lặp, chúng sẽ chạy qua đoạn mã bên trong thân vòng lặp đến cuối rồi ngay lập tức bắt đầu lại từ đầu. Để thử nghiệm với vòng lặp, hãy tạo một dự án mới tên loops.
Rust có ba loại vòng lặp: loop
, while
, và for
. Hãy thử từng loại.
Lặp lại mã với loop
Từ khóa loop
bảo Rust thực thi một khối mã lặp đi lặp lại mãi mãi hoặc cho đến khi bạn bảo nó dừng lại một cách tường minh.
Ví dụ, hãy đổi tệp src/main.rs trong thư mục loops của bạn thành như sau:
Tên tệp: src/main.rs
fn main() {
loop {
println!("again!");
}
}
Khi chạy chương trình này, chúng ta sẽ thấy again!
được in ra liên tục cho đến khi chúng ta dừng chương trình 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ị kẹt trong vòng lặp vô tận. Hãy thử nhé:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!
Ký hiệu ^C
biểu thị nơi bạn đã nhấn ctrl-c.
Bạn có thể thấy hoặc không thấy chữ again!
được in sau ^C
, tùy thuộc vào việc mã đang ở đâu trong vòng lặp khi nhận tín hiệu ngắt.
May mắn thay, Rust cũng cung cấp cách thoát khỏi vòng lặp bằng mã. Bạn có thể đặt từ khóa break
bên trong vòng lặp để cho chương trình biết khi nào dừng thực thi vòng lặp. Hãy nhớ rằng chúng ta đã làm điều này trong trò chơi đoán số ở phần “Thoát sau khi đoán đúng” của Chương 2 để thoát chương trình khi người dùng thắng trò chơi bằng cách đoán đúng số.
Chúng ta cũng đã dùng continue
trong trò chơi đoán số, thứ mà trong vòng lặp sẽ bảo chương trình bỏ qua phần mã còn lại của vòng lặp hiện tại và chuyển sang vòng lặp kế tiếp.
Trả về giá trị từ vòng lặp
Một trong những cách dùng của loop
là thử lại một thao tác mà bạn biết có thể thất bại, chẳng hạn như kiểm tra xem một thread đã hoàn thành công việc hay chưa. Bạn cũng có thể cần truyền kết quả của thao tác đó ra khỏi vòng lặp cho phần mã còn lại. Để làm điều này, bạn có thể thêm giá trị muốn trả về sau biểu thức break
bạn dùng để dừng vòng lặp; giá trị đó sẽ được trả về từ vòng lặp để bạn dùng, như sau:
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 tên counter
và khởi tạo nó bằng 0
. Sau đó khai báo một biến tên result
để giữ giá trị trả về từ vòng lặp. Ở mỗi lần lặp, chúng ta cộng 1
vào counter
, rồi kiểm tra xem counter
có bằng 10
hay không. Khi bằng, chúng ta dùng từ khóa break
với giá trị counter * 2
. Sau vòng lặp, chúng ta dùng dấu chấm phẩy để kết thúc câu lệnh gán giá trị cho result
. Cuối cùng, chúng ta in giá trị trong result
, trong trường hợp này là 20
.
Bạn cũng có thể return
từ bên trong một vòng lặp. Trong khi break
chỉ thoát vòng lặp hiện tại, return
luôn thoát khỏi hàm hiện tại.
Nhãn vòng lặp để phân biệt giữa nhiều vòng lặp
Nếu bạn có các vòng lặp lồng nhau, break
và continue
áp dụng cho vòng lặp trong cùng tại thời điểm đó. Bạn có thể tùy chọn chỉ định một nhãn vòng lặp cho một vòng lặp, rồi dùng với break
hoặc continue
để chỉ định rằng các từ khóa đó áp dụng cho vòng lặp được gắn nhãn thay vì vòng lặp trong cùng. Nhãn vòng lặp phải bắt đầu bằng một dấu nháy đơn. Đây là một ví dụ với 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 ngoài có nhãn 'counting_up
, và nó sẽ đếm tăng từ 0 đến 2. Vòng lặp trong không có nhãn đếm giảm từ 10 xuống 9. Lệnh break
đầu tiên không chỉ rõ nhãn sẽ chỉ thoát vòng lặp trong. Câu lệnh break 'counting_up;
sẽ thoát vòng lặp ngoài. Đoạn mã này in ra:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [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
Vòng lặp có điều kiện với while
Một chương trình thường cần đánh giá một điều kiện bên trong vòng lặp. Khi điều kiện là true
, vòng lặp chạy. Khi điều kiện không còn true
, chương trình gọi break
, dừng vòng lặp. Có thể hiện thực hành vi như vậy bằng cách kết hợp loop
, if
, else
, và break
; bạn có thể thử tự làm nếu muốn. Tuy nhiên, mẫu này phổ biến đến mức Rust có một cấu trúc ngôn ngữ dựng sẵn cho nó, gọi là vòng lặp while
. Trong Liệt kê 3-3, chúng ta dùng while
để lặp chương trình ba lần, đếm ngược mỗi lần, rồi sau vòng lặp in một thông điệp và thoát.
fn main() { let mut number = 3; while number != 0 { println!("{number}!"); number -= 1; } println!("LIFTOFF!!!"); }
while
để chạy mã khi một điều kiện đánh giá là true
Cấu trúc này loại bỏ nhiều tầng lồng nhau nếu bạn dùng loop
, if
, else
, và break
, và nó rõ ràng hơn. Chừng nào điều kiện đánh giá là true
, mã sẽ chạy; nếu không, nó thoát vòng lặp.
Lặp qua một tập hợp với for
Bạn có thể chọn dùng while
để lặp qua các phần tử của một tập hợp, như một mảng. Ví dụ, vòng lặp trong Liệt kê 3-4 in từng phần tử trong mảng a
.
fn main() { let a = [10, 20, 30, 40, 50]; let mut index = 0; while index < 5 { println!("the value is: {}", a[index]); index += 1; } }
while
Ở đây, mã đếm tăng qua các phần tử trong mảng. Nó bắt đầu tại chỉ mục 0
, rồi lặp cho đến khi đạt đến chỉ mục cuối cùng trong mảng (nghĩa là khi index < 5
không còn true
). Chạy đoạn mã này sẽ in mọi phần tử trong mảng:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [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
Cả năm giá trị của mảng xuất hiện trong terminal, như mong đợi. Mặc dù index
sẽ đạt giá trị 5
tại một thời điểm, vòng lặp dừng thực thi trước khi cố lấy phần tử thứ sáu từ mảng.
Tuy nhiên, cách tiếp cận này dễ lỗi; chúng ta có thể khiến 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 đổi định nghĩa mảng a
thành có bốn phần tử nhưng quên cập nhật điều kiện thành while index < 4
, mã sẽ panic. Nó cũng chậm, vì trình biên dịch thêm mã chạy lúc thực thi để kiểm tra xem chỉ mục có nằm trong phạm vi mảng ở mỗi vòng lặp hay không.
Là một cách gọn gàng hơn, bạn có thể dùng vòng lặp for
để thực thi một đoạn mã cho từng phần tử trong tập hợp. Vòng lặp for
trông như mã trong Liệt kê 3-5.
fn main() { let a = [10, 20, 30, 40, 50]; for element in a { println!("the value is: {element}"); } }
for
Khi chạy đoạn mã này, chúng ta sẽ thấy đầu ra giống Liệt kê 3-4. Quan trọng hơn, chúng ta đã tăng độ an toàn cho mã và loại bỏ khả năng lỗi có thể xảy ra do vượt quá cuối mảng hoặc không đi đủ xa và bỏ sót một số phần tử. Mã máy được tạo từ vòng lặp for
cũng có thể hiệu quả hơn, vì không cần so sánh chỉ mục với độ dài mảng ở mỗi vòng lặp.
Dùng vòng lặp for
, bạn sẽ không phải nhớ đổi bất kỳ đoạn mã nào khác nếu bạn thay đổi số lượng phần tử trong mảng, như bạn phải làm với cách ở Liệt kê 3-4.
Tính an toàn và ngắn gọn của vòng lặp for
khiến nó trở thành cấu trúc vòng lặp được dùng nhiều nhất trong Rust. Ngay cả trong những tình huống bạn muốn chạy một đoạn mã một số lần nhất định, như ví dụ đếm ngược dùng vòng lặp while
trong Liệt kê 3-3, hầu hết Rustacean sẽ dùng vòng lặp for
. Cách làm là dùng một Range
do thư viện tiêu chuẩn cung cấp, 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.
Đoạn đếm ngược sẽ trông như thế này khi dùng vòng lặp for
và một phương thức khác mà ta chưa bàn, rev
, để đảo ngược range:
Tên tệp: src/main.rs
fn main() { for number in (1..4).rev() { println!("{number}!"); } println!("LIFTOFF!!!"); }
Đoạn mã này dễ chịu hơn một chút, phải không?
Tóm tắt
Tuyệt vời! Đây là một chương khá dài: bạn đã học về biến, kiểu dữ liệu vô hướng và tổng hợp, hàm, chú thích, biểu thức if
, và các vòng lặp! Để luyện tập các khái niệm trong chương này, hãy thử xây dựng các chương trình sau:
- Chuyển đổi nhiệt độ giữa Fahrenheit và Celsius.
- Tạo số Fibonacci thứ n.
- In lời bài hát Giáng Sinh “The Twelve Days of Christmas,” tận dụng sự lặp lại trong bài.
Khi sẵn sàng tiếp tục, chúng ta sẽ nói về một khái niệm trong Rust mà thường không tồn tại ở các ngôn ngữ lập trình khác: ownership.