Control Flow
ความสามารถในการรันโค้ดบางอย่างขึ้นกับว่า condition เป็น true หรือไม่
และความสามารถในการรันโค้ดบางอย่างซ้ำ ๆ ขณะที่ condition เป็น true เป็น
building block พื้นฐานในภาษาโปรแกรมส่วนใหญ่ โครงสร้างที่ใช้บ่อยที่สุดที่
ให้คุณควบคุม flow ของการ execute โค้ด Rust คือ if expression และ loop
if Expression
if expression ให้คุณ branch โค้ดขึ้นกับ condition คุณให้ condition แล้ว
บอกว่า “ถ้า condition นี้ตรง รัน block โค้ดนี้ ถ้า condition ไม่ตรง อย่า
รัน block โค้ดนี้”
สร้างโปรเจกต์ใหม่ชื่อ branches ใน directory projects ของคุณ เพื่อ
สำรวจ if expression ในไฟล์ src/main.rs ป้อนต่อไปนี้:
Filename: src/main.rs
fn main() {
let number = 3;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
if expression ทั้งหมดเริ่มด้วย keyword if ตามด้วย condition ในกรณีนี้
condition เช็คว่าตัวแปร number มีค่าน้อยกว่า 5 หรือไม่ เราวาง block ของ
โค้ดที่จะ execute ถ้า condition เป็น true ทันทีหลัง condition ภายใน
curly bracket Block ของโค้ดที่ผูกกับ condition ใน if expression บางครั้ง
เรียกว่า arm เหมือน arm ใน match expression ที่เราพูดถึงในส่วน
“เปรียบเทียบคำตอบกับตัวเลขลับ”
ของบทที่ 2
เราสามารถใส่ else expression แบบ optional ได้ด้วย ซึ่งเราเลือกทำที่นี่
เพื่อให้โปรแกรมมี block โค้ดทางเลือกให้ execute หาก condition ประเมินเป็น
false ถ้าคุณไม่ให้ else expression และ condition เป็น false
โปรแกรมก็จะข้าม block if ไปและไปที่โค้ดถัดไป
ลองรันโค้ดนี้ คุณควรเห็น output ต่อไปนี้:
$ 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
ลองเปลี่ยนค่าของ number ให้เป็นค่าที่ทำให้ condition เป็น false เพื่อ
ดูว่าเกิดอะไรขึ้น:
fn main() {
let number = 7;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
รันโปรแกรมอีกครั้งและดู output:
$ 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
ที่ควรหมายเหตุไว้คือ condition ในโค้ดนี้ ต้อง เป็น bool ถ้า condition
ไม่ใช่ bool เราจะได้ error เช่น ลองรันโค้ดต่อไปนี้:
Filename: src/main.rs
fn main() {
let number = 3;
if number {
println!("number was three");
}
}
condition if ประเมินเป็นค่า 3 ครั้งนี้ และ Rust โยน error:
$ 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
Error บ่งบอกว่า Rust คาด bool แต่ได้ integer ต่างจากภาษาอย่าง Ruby และ
JavaScript Rust จะไม่พยายามแปลง type ที่ไม่ใช่ Boolean เป็น Boolean
อัตโนมัติ คุณต้อง explicit และให้ if พร้อม Boolean เป็น condition เสมอ
ถ้าเราอยากให้ block โค้ด if รันเฉพาะเมื่อตัวเลขไม่เท่ากับ 0 เช่น เรา
เปลี่ยน if expression เป็นต่อไปนี้ได้:
Filename: src/main.rs
fn main() {
let number = 3;
if number != 0 {
println!("number was something other than zero");
}
}
การรันโค้ดนี้จะพิมพ์ number was something other than zero
จัดการ Condition หลายตัวด้วย else if
คุณใช้ condition หลายตัวได้โดยรวม if และ else ใน else if expression
เช่น:
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");
}
}
โปรแกรมนี้มี path เป็นไปได้ 4 ทาง หลังรันมันแล้ว คุณควรเห็น output ต่อไปนี้:
$ 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
เมื่อโปรแกรมนี้ execute มันเช็ค if expression แต่ละตัวตามลำดับ และ
execute body แรกที่ condition ประเมินเป็น true หมายเหตุว่าแม้ 6 หาร 2
ลงตัว เราไม่เห็น output number is divisible by 2 และเราก็ไม่เห็นข้อความ
number is not divisible by 4, 3, or 2 จาก block else นั่นเป็นเพราะ
Rust execute แค่ block ของ condition true ตัวแรก และเมื่อมันเจอตัวหนึ่ง
มันก็ไม่เช็คตัวที่เหลือ
การใช้ else if expression มากเกินไปทำให้โค้ดเลอะเทอะได้ ดังนั้นถ้าคุณมี
มากกว่าหนึ่งตัว คุณอาจอยาก refactor โค้ด บทที่ 6 อธิบายโครงสร้าง branching
ที่ทรงพลังของ Rust ชื่อ match สำหรับกรณีเหล่านี้
ใช้ if ใน Statement let
เพราะ if เป็น expression เราใช้มันทางขวาของ statement let เพื่อ assign
ผลให้ตัวแปรได้ ดังใน Listing 3-2
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
println!("The value of number is: {number}");
}
if expression ให้ตัวแปรตัวแปร number จะถูก bind กับค่าตามผลของ if expression รันโค้ดนี้เพื่อ
ดูว่าเกิดอะไรขึ้น:
$ 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
จำไว้ว่า block ของโค้ดประเมินเป็น expression สุดท้ายในนั้น และตัวเลขใน
ตัวเองก็เป็น expression ด้วย ในกรณีนี้ ค่าของ if expression ทั้งหมดขึ้น
กับว่า block ของโค้ดไหน execute สิ่งนี้หมายความว่าค่าที่มีศักยภาพเป็นผล
จาก arm แต่ละตัวของ if ต้องมี type เดียวกัน ใน Listing 3-2 ผลของทั้ง
arm if และ arm else คือ integer i32 ถ้า type mismatch ดังตัวอย่าง
ต่อไปนี้ เราจะได้ error:
Filename: src/main.rs
fn main() {
let condition = true;
let number = if condition { 5 } else { "six" };
println!("The value of number is: {number}");
}
เมื่อเราพยายาม compile โค้ดนี้ เราจะได้ error arm if และ else มี type
ของค่าที่เข้ากันไม่ได้ และ Rust ระบุตำแหน่งที่จะหาปัญหาในโปรแกรมเป๊ะ ๆ:
$ 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
Expression ใน block if ประเมินเป็น integer และ expression ใน block
else ประเมินเป็น string มันจะไม่ทำงาน เพราะตัวแปรต้องมี type เดียว และ
Rust ต้องรู้แน่นอนตอน compile time ว่า type ของตัวแปร number คืออะไร
การรู้ type ของ number ให้ compiler ตรวจสอบว่า type valid ในทุกที่ที่
เราใช้ number Rust จะไม่สามารถทำแบบนั้นได้ ถ้า type ของ number ถูก
กำหนดแค่ตอน runtime compiler ก็จะซับซ้อนขึ้นและให้การรับประกันเกี่ยวกับ
โค้ดน้อยลง ถ้ามันต้องตามติด type สมมติฐานหลายตัวสำหรับตัวแปรใด ๆ
การทำซ้ำด้วย Loop
มันบ่อยครั้งที่มีประโยชน์ในการ execute block โค้ดมากกว่าหนึ่งครั้ง สำหรับ งานนี้ Rust มี loop หลายแบบ ซึ่งจะรันผ่านโค้ดภายใน body ของ loop จน สุด แล้วเริ่มกลับที่ต้นทันที เพื่อทดลอง loop มาสร้างโปรเจกต์ใหม่ชื่อ loops
Rust มี loop สามแบบ: loop, while และ for ลองดูแต่ละแบบ
ทำซ้ำโค้ดด้วย loop
keyword loop บอก Rust ให้ execute block ของโค้ดซ้ำ ๆ ตลอดไป หรือจน
กว่าคุณบอกให้หยุดแบบ explicit
ตัวอย่าง เปลี่ยนไฟล์ src/main.rs ใน directory loops ของคุณ ให้หน้า ตาเป็นแบบนี้:
Filename: src/main.rs
fn main() {
loop {
println!("again!");
}
}
เมื่อเรารันโปรแกรมนี้ เราจะเห็น again! พิมพ์ซ้ำ ๆ ต่อเนื่อง จนกว่าเรา
หยุดโปรแกรมเอง terminal ส่วนใหญ่รองรับ keyboard shortcut
ctrl-C เพื่อ interrupt โปรแกรมที่ติดอยู่ใน loop
ต่อเนื่อง ลองดู:
$ 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!
สัญลักษณ์ ^C แทนตำแหน่งที่คุณกด ctrl-C
คุณอาจเห็นหรือไม่เห็นคำ again! พิมพ์หลัง ^C ขึ้นกับว่าโค้ดอยู่ตรงไหน
ใน loop ตอนที่รับ interrupt signal
โชคดีที่ Rust ยังมีวิธี break ออกจาก loop ด้วยโค้ด คุณวาง keyword break
ภายใน loop เพื่อบอกโปรแกรมว่าจะหยุด execute loop เมื่อไหร่ จำได้ว่าเราทำ
แบบนี้ในเกมทายตัวเลขในส่วน
“ออกเมื่อทายถูก” ของ
บทที่ 2 เพื่อออกจากโปรแกรมเมื่อผู้ใช้ชนะเกมโดยทายตัวเลขถูก
เรายังใช้ continue ในเกมทายตัวเลขด้วย ซึ่งใน loop บอกโปรแกรมให้ข้ามโค้ด
ที่เหลือใน iteration นี้ของ loop และไปที่ iteration ถัดไป
Return ค่าจาก Loop
หนึ่งในการใช้ loop คือลอง operation ที่คุณรู้ว่าอาจล้มเหลว เช่น เช็ค
ว่า thread ทำงานเสร็จแล้วหรือไม่ คุณอาจยังต้องส่งผลของ operation นั้นออก
จาก loop ไปยังโค้ดที่เหลือ ในการทำสิ่งนี้ คุณเพิ่มค่าที่คุณอยากให้ return
หลัง expression break ที่ใช้หยุด loop ค่านั้นจะถูก return ออกจาก loop
เพื่อให้คุณใช้ได้ ดังที่แสดงที่นี่:
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {result}");
}
ก่อน loop เราประกาศตัวแปรชื่อ counter แล้ว initialize เป็น 0 จากนั้น
เราประกาศตัวแปรชื่อ result เพื่อเก็บค่าที่ return จาก loop ในทุก
iteration ของ loop เราเพิ่ม 1 ให้ตัวแปร counter แล้วเช็คว่า counter
เท่ากับ 10 หรือไม่ เมื่อเท่า เราใช้ keyword break พร้อมค่า
counter * 2 หลัง loop เราใช้ semicolon จบ statement ที่ assign ค่าให้
result สุดท้าย เราพิมพ์ค่าใน result ซึ่งในกรณีนี้คือ 20
คุณยัง return จากภายใน loop ได้ ขณะที่ break ออกแค่จาก loop ปัจจุบัน
return ออกจากฟังก์ชันปัจจุบันเสมอ
แยกความกำกวมด้วย Loop Label
ถ้าคุณมี loop ภายใน loop break และ continue ใช้กับ loop ในสุดที่จุด
นั้น คุณระบุ loop label บน loop แบบ optional ได้ ซึ่งจากนั้นใช้กับ
break หรือ continue เพื่อระบุว่า keyword เหล่านั้นใช้กับ loop ที่
label แทน loop ในสุด Loop label ต้องเริ่มด้วย single quote นี่คือตัวอย่าง
ที่มี loop ซ้อนสองตัว:
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}");
}
Loop ด้านนอกมี label 'counting_up และจะนับขึ้นจาก 0 ถึง 2 Loop ด้านใน
ที่ไม่มี label นับลงจาก 10 ถึง 9 break ตัวแรกที่ไม่ระบุ label จะออกจาก
loop ด้านในเท่านั้น statement break 'counting_up; จะออกจาก loop ด้าน
นอก โค้ดนี้พิมพ์:
$ 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
Conditional Loop ที่กระชับด้วย while
บ่อยครั้งโปรแกรมต้องประเมิน condition ภายใน loop ขณะที่ condition เป็น
true loop รัน เมื่อ condition หยุดเป็น true โปรแกรมเรียก break
หยุด loop เป็นไปได้ที่จะ implement พฤติกรรมแบบนี้ด้วยการรวม loop, if,
else และ break คุณลองทำในโปรแกรมตอนนี้ได้ถ้าต้องการ อย่างไรก็ตาม
pattern นี้ใช้บ่อยมาก Rust จึงมีโครงสร้างภาษา built-in สำหรับมัน เรียกว่า
while loop ใน Listing 3-3 เราใช้ while วน loop โปรแกรมสามครั้ง นับ
ถอยหลังทุกครั้ง แล้วหลัง loop พิมพ์ข้อความและออก
fn main() {
let mut number = 3;
while number != 0 {
println!("{number}!");
number -= 1;
}
println!("LIFTOFF!!!");
}
while loop รันโค้ดขณะที่ condition ประเมินเป็น trueโครงสร้างนี้ขจัด nesting หลายชั้นที่จำเป็นถ้าคุณใช้ loop, if, else
และ break และมันชัดเจนกว่า ขณะที่ condition ประเมินเป็น true โค้ดรัน
ไม่อย่างนั้นมันออกจาก loop
วน Loop ผ่าน Collection ด้วย for
คุณเลือกใช้โครงสร้าง while วน loop ผ่าน element ของ collection เช่น
array ได้ เช่น loop ใน Listing 3-4 พิมพ์แต่ละ element ใน array 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 loopที่นี่ โค้ดนับขึ้นผ่าน element ใน array มันเริ่มที่ index 0 แล้ววน
loop จนถึง index สุดท้ายใน array (นั่นคือเมื่อ index < 5 ไม่ใช่
true อีกต่อไป) การรันโค้ดนี้จะพิมพ์ทุก element ใน array:
$ 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
ค่า array ทั้งห้าค่าปรากฏใน terminal ตามที่คาด แม้ index จะถึงค่า 5
ในจุดหนึ่ง loop หยุด execute ก่อนพยายามดึงค่าที่หกจาก array
อย่างไรก็ตาม วิธีนี้เสี่ยง error — เราอาจทำให้โปรแกรม panic ถ้าค่า
index หรือ test condition ไม่ถูกต้อง เช่น ถ้าคุณเปลี่ยนการประกาศ array
a ให้มีสี่ element แต่ลืม update condition เป็น while index < 4
โค้ดจะ panic มันยังช้าด้วย เพราะ compiler เพิ่ม runtime code เพื่อทำการ
เช็ค conditional ว่า index อยู่ภายในขอบเขตของ array ในทุก iteration ของ
loop
ในฐานะทางเลือกที่กระชับกว่า คุณใช้ for loop และ execute โค้ดบางส่วน
สำหรับแต่ละ item ใน collection ได้ for loop หน้าตาเหมือนโค้ดใน
Listing 3-5
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {element}");
}
}
for loopเมื่อเรารันโค้ดนี้ เราจะเห็น output เดียวกับใน Listing 3-4 ที่สำคัญกว่า
คือ ตอนนี้เราเพิ่มความปลอดภัยของโค้ด และขจัดโอกาสของ bug ที่อาจเกิดจาก
การไปเกินท้าย array หรือไปไม่ไกลพอและพลาด item บางตัว Machine code ที่
generate จาก for loop ก็มีประสิทธิภาพมากกว่าได้ด้วย เพราะ index ไม่
ต้องเปรียบเทียบกับความยาว array ทุก iteration
การใช้ for loop คุณไม่ต้องจำเปลี่ยนโค้ดอื่นใด ถ้าคุณเปลี่ยนจำนวนค่าใน
array เหมือนที่คุณจะต้องทำกับวิธีที่ใช้ใน Listing 3-4
ความปลอดภัยและความกระชับของ for loop ทำให้มันเป็นโครงสร้าง loop ที่ใช้
บ่อยที่สุดใน Rust แม้ในสถานการณ์ที่คุณอยากรันโค้ดบางอย่างจำนวนครั้งหนึ่ง
อย่างในตัวอย่างนับถอยหลังที่ใช้ while loop ใน Listing 3-3 Rustacean
ส่วนใหญ่ก็จะใช้ for loop วิธีทำคือใช้ Range ที่ standard library
มีให้ ซึ่ง generate ตัวเลขทั้งหมดในลำดับ เริ่มจากตัวเลขหนึ่งและจบก่อน
อีกตัวเลขหนึ่ง
นี่คือสิ่งที่การนับถอยหลังจะหน้าตาเป็นแบบนี้โดยใช้ for loop และอีก
เมธอดที่เรายังไม่ได้พูดถึง rev เพื่อกลับด้าน range:
Filename: src/main.rs
fn main() {
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!!!");
}
โค้ดนี้ดีขึ้นนิดหน่อย ใช่ไหม?
สรุป
คุณทำได้! นี่เป็นบทขนาดใหญ่ — คุณได้เรียนเรื่องตัวแปร, ชนิดข้อมูล scalar
และ compound, ฟังก์ชัน, comment, if expression และ loop! ในการฝึกกับ
แนวคิดที่พูดถึงในบทนี้ ลอง build โปรแกรมเพื่อทำสิ่งต่อไปนี้:
- แปลงอุณหภูมิระหว่าง Fahrenheit และ Celsius
- Generate ตัวเลข Fibonacci ตัวที่ n
- พิมพ์เนื้อเพลง Christmas carol “The Twelve Days of Christmas” โดยใช้ ประโยชน์จากการทำซ้ำในเพลง
เมื่อคุณพร้อมไปต่อ เราจะพูดถึงแนวคิดใน Rust ที่ ไม่ ค่อยมีในภาษา โปรแกรมอื่น: ownership