โครงสร้างควบคุมการทำงาน
ความสามารถในการรันโค้ดบางส่วนโดยขึ้นอยู่กับว่าเงื่อนไขเป็นจริง (true) หรือไม่ และความสามารถในการรันโค้ดบางส่วนซ้ำๆ ในขณะที่เงื่อนไขเป็นจริงอยู่ ล้วนเป็นโครงสร้างพื้นฐานทั่วไปในภาษาโปรแกรมส่วนใหญ่ โดยโครงสร้างที่นิยมใช้งานกันมากที่สุดในการใช้ควบคุมทิศทางและลำดับการทำงาน (control flow) ของโค้ด Rust ได้แก่ นิพจน์ if และโครงสร้างการวนซ้ำ (loops)
นิพจน์ if
นิพจน์ if ช่วยให้คุณสามารถสร้างทางแยกสำหรับรันโค้ดโดยอิงตามเงื่อนไขที่กำหนด โดยคุณจะระบุเงื่อนไขแล้วสั่งการว่า “หากตรงตามเงื่อนไขนี้ ให้รันบล็อกโค้ดส่วนนี้ แต่หากไม่ตรงตามเงื่อนไขดังกล่าว ก็ไม่ต้องประมวลผลโค้ดบล็อกส่วนนี้”
ให้ทดลองสร้างโปรเจกต์ใหม่ชื่อว่า branches ในไดเรกทอรี projects ของคุณ เพื่อร่วมสำรวจตรวจสอบนิพจน์ if โดยในไฟล์ 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 ทั้งหมดจะเริ่มต้นด้วยคำสำคัญ if ตามด้วยตัวเงื่อนไข ซึ่งในกรณีตัวอย่างนี้ เงื่อนไขคือการตรวจสอบว่าตัวแปร number มีค่าน้อยกว่า 5 หรือไม่ เราจะนำบล็อกโค้ดที่ต้องการประมวลผลเมื่อเงื่อนไขได้รับการประเมินเป็นจริง (true) มาวางไว้ต่อท้ายข้อความเงื่อนไขทันทีโดยครอบภายในวงเล็บปีกกา บล็อกโค้ดที่เชื่อมโยงอยู่กับเงื่อนไขในนิพจน์ if บางครั้งจะเรียกว่า กิ่งการทำงาน (arms) เช่นเดียวกับกิ่งตัวเลือกในนิพจน์ match ที่เราคุยกันไว้ในหัวข้อ “การเปรียบเทียบค่าที่ทายกับตัวเลขปริศนา” ในบทที่ 2
นอกจากนี้เรายังสามารถเขียนเพิ่มนิพจน์ else เข้ามาร่วมด้วยได้ตามความเหมาะสม เช่นที่เราเลือกทำในตัวอย่างนี้ เพื่อกำหนดบล็อกโค้ดทางเลือกให้โปรแกรมรันทำงานในกรณีที่เงื่อนไขถูกประเมินค่าออกมาเป็นเท็จ (false) หากคุณไม่ได้ใส่นิพจน์ else ประกอบไว้ และเงื่อนไขประเมินได้เป็นเท็จ ตัวโปรแกรมจะทำการข้ามบล็อกการทำงานของ if นั้นไปเลยและเปลี่ยนไปรันโค้ดส่วนถัดไปแทน
ลองสั่งรันโค้ดส่วนนี้ดู คุณควรจะพบรายงานผลลัพธ์ปรากฏดังนี้:
$ 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 ให้แสดงเงื่อนไขประเมินค่าเป็นเท็จ (false) เพื่อตรวจสอบพฤติกรรมดู:
fn main() {
let number = 7;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
ให้สั่งรันโปรแกรมใหม่อีกครั้ง แล้วมาตรวจผลลัพธ์กัน:
$ 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
ประเด็นสำคัญที่สมควรบันทึกจำไว้คือ เงื่อนไขภายในโครงสร้างประโยคนี้ จำเป็นต้อง มีชนิดข้อมูลเป็นประเภท bool เท่านั้น หากเงื่อนไขไม่ได้รายงานประเภทเป็น bool ระบบจะส่งคืนข้อผิดพลาดแจ้งเตือน ตัวอย่างเช่น ลองรันโค้ดต่อไปนี้ดู:
Filename: src/main.rs
fn main() {
let number = 3;
if number {
println!("number was three");
}
}
เงื่อนไขของ if ในรอบนี้ประเมินมูลค่าออกมาได้เป็นตัวเลข 3 ซึ่งทำให้ Rust ส่งข้อความแจ้งเตือนความล้มเหลวขึ้นมาทันที:
$ 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
ข้อผิดพลาดบ่งชี้ว่า Rust คาดหวังจะได้รับข้อมูลชนิด bool แต่กลับได้รับค่าส่งมาเป็นเลขจำนวนเต็ม ซึ่งจุดนี้จะต่างไปจากภาษาโปรแกรมอื่นๆ อย่างเช่น Ruby หรือ JavaScript ที่ระบบเบื้องหลังจะช่วยอำนวยความสะดวกแปลงข้อมูลชนิดที่ไม่ใช่ Boolean ให้แปรสภาพเป็นค่าความจริงโดยอัตโนมัติ ใน Rust คุณจำเป็นต้องเขียนชี้แนะชัดเจนตรงไปตรงมาและป้อนเงื่อนไขของ if เป็นค่าความจริง Boolean เสมอ ตัวอย่างเช่น หากเราต้องการให้บล็อกคำสั่งของ if ทำงานเฉพาะเมื่อจำนวนตัวเลขมีมูลค่าไม่เท่ากับ 0 เท่านั้น เราสามารถเปลี่ยนรูปนิพจน์ if ให้เป็นรูปแบบดังนี้:
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
การรับมือกับเงื่อนไขที่หลากหลายด้วย else if
คุณสามารถประกาศระบุเงื่อนไขสำหรับดักตรวจจับเหตุการณ์ได้มากกว่าหนึ่งทางแยกโดยการผสานกำลังคำสั่งของ if และ else เข้ามาร่วมทำงานในรูปนิพจน์ else if ตัวอย่างเช่น:
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");
}
}
โปรแกรมตัวอย่างนี้จะมีเส้นทางเลือกการเดินทางที่เป็นไปได้ทั้งหมด 4 รูปแบบ ภายหลังสั่งรันทำงานเรียบร้อยแล้ว คุณควรจะได้รับรายงานผลลัพธ์ปรากฏดังนี้:
$ 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
ในระหว่างโปรแกรมประมวลผลคำสั่ง มันจะไล่ตรวจทานนิพจน์เงื่อนไขของแต่ละกิ่งไปทีละรายการตามลำดับก่อนหลัง และจะตกลงยอมประมวลผลรันเฉพาะบล็อกเนื้อความแรกสุดที่ระบบประเมินค่าเงื่อนไขออกมาได้ผลเป็นจริง (true) เท่านั้น พึงสังเกตว่าแม้เลข 6 จะสามารถนำมาหารด้วย 2 ลงตัวได้เช่นกัน แต่เราจะไม่พบเห็นข้อความแสดงผล number is divisible by 2 ปรากฏขึ้นมาบนหน้าจอเลย และก็ไม่เห็นข้อความจากกิ่ง else เช่นกัน เหตุผลก็เนื่องมาจาก Rust จะเลือกทำบล็อกการทำงานแรกสุดที่สอดคล้องกับเงื่อนไขที่เป็นจริงเท่านั้น และเมื่อตรวจพบเป้าหมายสำเร็จแล้ว ตัวคอมไพเลอร์จะไม่ลงไปไล่ตรวจสอบเงื่อนไขที่เหลืออยู่ด้านล่างอีกเลย
การเขียนเรียกใช้งานนิพจน์ else if ปริมาณที่หนาแน่นเกินไปมักส่งผลให้โครงสร้างโค้ดของคุณดูรกรุงรังและจัดการแก้ไขปรับปรุงยาก ดังนั้นหากคุณเริ่มมีกิ่งเงื่อนไขจำนวนมาก แนะนำให้มองหาแนวทางปรับปรุงจัดระเบียบโครงสร้างโค้ด (refactoring) ในบทที่ 6 จะอธิบายถึงเครื่องมือควบคุมเส้นทางแยกที่มีความยืดหยุ่นและเปี่ยมประสิทธิภาพของ Rust ที่เรียกว่า match สำหรับใช้จัดการกับเงื่อนไขในกรณีเหล่านี้
การใช้งาน if ภายในประโยคคำสั่ง let
เนื่องจากโครงสร้าง if ถูกวิเคราะห์นับเป็นประโยคนิพจน์ชนิดหนึ่ง เราจึงสามารถเขียนระบุประยุกต์ใช้งานมันไว้ที่ส่วนทางขวาของประโยคคำสั่ง let เพื่อนำเอาผลสรุปขากลับจากการคำนวณมาใช้กำหนดมูลค่าเก็บไว้ในตัวแปรได้ ดังแสดงในรายการที่ 3-2
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
println!("The value of number is: {number}");
}
ตัวแปร number จะได้รับการผูกมูลค่าข้อมูลเข้ากับผลลัพธ์ที่คำนวณสรุปได้จากประโยคนิพจน์ if ให้ลองรันโค้ดส่วนนี้เพื่อตรวจทานผลการประมวลผลดู:
$ 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
จำได้ไหมว่าบล็อกคำสั่งจะประเมินค่าออกมาได้เท่ากับมูลค่านิพจน์ขอบเขตแถวสุดท้ายภายในบล็อกนั้นๆ และมูลค่าสเกลาร์ตัวเลขเดี่ยวก็จัดเป็นรูปนิพจน์ด้วยเช่นกัน ดังนั้นในกรณีนี้ ข้อมูลขากลับของนิพจน์ if ทั้งหมดจะผันเปลี่ยนไปตามชนิดการประมวลผลของบล็อกโค้ดกิ่งที่ได้ทำงาน ซึ่งมีผลให้ค่าข้อมูลที่เป็นไปได้ทั้งหมดจากการคำนวณของแต่ละกิ่งในโครงสร้าง if จำเป็นต้อง มีประเภทชนิดข้อมูลขากลับที่ตรงกันอย่างสมบูรณ์ โดยในรายการที่ 3-2 ผลลัพธ์ขากลับจากทั้งกิ่ง if และกิ่ง else ต่างคืนค่ามาเป็นตัวเลขจำนวนเต็มชนิด i32 ทั้งคู่ หากชนิดข้อมูลขากลับไม่ตรงกันดังเช่นที่แสดงในตัวอย่างถัดไปด้านล่างนี้ ระบบจะเกิดข้อผิดพลาดขึ้นกะทันหัน:
Filename: src/main.rs
fn main() {
let condition = true;
let number = if condition { 5 } else { "six" };
println!("The value of number is: {number}");
}
เมื่อเราพยายามประมวลผลเพื่อคอมไพล์โค้ดชุดดังกล่าว ระบบจะแจ้งเตือนข้อผิดพลาดทันที โดยกิ่ง if และกิ่ง else จะคืนค่าชนิดข้อมูลที่ขัดแย้งใช้งานร่วมกันไม่ได้ และ 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
นิพจน์ภายในบล็อกงานของ if ประเมินผลได้เป็นตัวเลขจำนวนเต็ม แต่นิพจน์ในกิ่ง else กลับประเมินผลได้เป็นข้อมูลชนิดข้อความ (string) พฤติกรรมนี้จะไม่ได้รับอนุญาตเนื่องจากตัวแปรใน Rust จะต้องมีชนิดข้อมูลประเภทเดียวที่แน่นอน และ Rust จำเป็นต้องรับทราบอย่างแน่ชัดตั้งแต่ขั้นตอนคอมไพล์ว่าตัวแปร number มีประเภทข้อมูลเป็นอะไร การที่คอมไพเลอร์ทราบชนิดข้อมูลที่ตายตัวของ number จะเปิดโอกาสให้ระบบสแกนคอยตรวจสอบประเมินได้ครอบคลุมในทุกจุดที่ตัวแปร number นี้ถูกเรียกใช้งาน Rust จะสูญเสียความปลอดภัยนี้ไปทันทีหากยอมปล่อยให้ตัวแปร number เปลี่ยนชนิดข้อมูลไปตามช่วงโปรแกรมทำงาน (runtime) ยิ่งไปกว่านั้นตัวโครงสร้างคอมไพเลอร์จะต้องเขียนขึ้นมาอย่างซับซ้อนมาก และมีผลให้การันตีการคุ้มกันความปลอดภัยในส่วนของตัวแปรมีประสิทธิภาพลดลงหากระบบต้องมาคอยไล่ตามสืบประวัติชนิดข้อมูลสมมติที่อาจเปลี่ยนโฉมไปมา
การทำงานวนซ้ำด้วยลูป (Repetition with Loops)
ในขั้นตอนเขียนโปรแกรม มักมีสถานการณ์ที่คุณต้องการรันบล็อกคำสั่งใดๆ ซ้ำมากกว่าหนึ่งครั้ง ภาษา Rust ได้เตรียมคำสั่งสำหรับจัดการการวนซ้ำ (loops) ไว้ให้ใช้งานหลากหลายรูปแบบ ซึ่งจะคอยรันโค้ดต่างๆ ภายในลูปไปจนจบแถวแล้ววนระบบกระโดดกลับมาเริ่มต้นประมวลผลที่จุดแรกสุดของลูปทันที เพื่อทดลองใช้งานเกี่ยวกับลูป มาทดลองตั้งโปรเจกต์ใหม่ชื่อว่า loops กัน
ภาษา Rust มีกลุ่มลูปการทำงานหลักอยู่ 3 ประเภท ได้แก่: loop, while และ for มาลองศึกษาวิธีใช้งานแต่ละแบบไปด้วยกัน
การวนซ้ำโค้ดไปเรื่อยๆ ด้วย loop
คำสำคัญ loop จะเป็นตัวบอก Rust ให้ทำการประมวลผลรันบล็อกคำสั่งวนไปวนมาเรื่อยๆ ตลอดกาลจนกว่าจะสั่งการอย่างเป็นทางการจากโค้ดภายในให้สั่งยุติการทำงาน
มาลองดูตัวอย่างกัน ให้เขียนปรับโครงสร้างโค้ดในไฟล์ src/main.rs ภายในไดเรกทอรี loops ของคุณให้แสดงพฤติกรรมดังต่อไปนี้:
Filename: src/main.rs
fn main() {
loop {
println!("again!");
}
}
เมื่อสั่งรันประมวลผลโปรแกรมนี้ เราจะพบคำว่า again! แสดงผลทับซ้ำๆ วนเวียนอยู่บนหน้าจอไปเรื่อยๆ ไม่มีวันสิ้นสุดจนกว่าเราจะกดสั่งหยุดโปรแกรมด้วยตนเอง โดยส่วนใหญ่แล้วบนหน้าต่างเทอร์มินัลเกือบทุกระบบจะรองรับการกดแป้นพิมพ์คีย์ลัด ctrl ร่วมกับ C เพื่อเข้าไปขัดขวางหยุดการทำงานโปรแกรมที่ติดขัดวนลูปไม่รู้จบอยู่ ให้ลองกดทดสอบรันดู:
$ 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 หรือไม่อยู่บ้างเล็กน้อย ซึ่งพฤติกรรมดังกล่าวจะแปรผันตามตำแหน่งประมวลผลโค้ดในลูปตรงช่วงเวลาที่สัญญาณขัดจังหวะโปรแกรม (interrupt signal) ถูกส่งเข้ามาประชิดตัวพอดี
โชคดีที่ภาษา Rust ได้เตรียมวิธีสั่งยุติการวนลูปจากชุดคำสั่งโค้ดไว้ให้ด้วยเช่นกัน โดยคุณสามารถระบุเติมคำสำคัญ break ไว้ภายในตำแหน่งลูปเพื่อแจ้งเตือนให้โปรแกรมยุติทำงานและออกจากลูปทันที จำได้ใช่ไหมว่าเราประยุกต์ใช้งานแนวทางนี้ไปแล้วในตัวเกมทายตัวเลขบทที่ 2 หัวข้อ “การหยุดการทำงานทันทีหลังจากทายถูก” เพื่อสั่งข้ามออกนอกโครงสร้างเกมเมื่อทายเลขสุ่มได้ถูกต้องสำเร็จ
และเรายังเคยหยิบใช้คำสั่ง continue ในตัวเกมทายตัวเลขด้วยเช่นกัน ซึ่งฟังก์ชันนี้จะคอยข้ามประโยคโค้ดที่เหลืออยู่ในการประมวลผลรอบปัจจุบันและสั่งกระโดดกลับขึ้นไปเริ่มทำงานรอบถัดไปของลูปทันที
การส่งคืนมูลค่าข้อมูลออกจากลูป
หนึ่งในประโยชน์หลักของโครงสร้าง loop คือการนำมาใช้เขียนคำสั่งขอรันพฤติกรรมซ้ำๆ ที่มีแนวโน้มความเสี่ยงอาจผิดพลาดได้ เช่น การช่วยตรวจสอบสแกนดูว่าเธรด (thread) คำนวณเบื้องหลังดำเนินการเสร็จสมบูรณ์หรือยัง นอกจากนี้คุณอาจมีความจำเป็นต้องนำเอาตัวเลขข้อมูลสรุปผลลัพธ์จากการทำงานภายในลูปออกมามอบให้แก่โค้ดภายนอกใช้งานต่อ เพื่อเป้าหมายดังกล่าวคุณสามารถเขียนระบุค่าตัวแปรผลลัพธ์ที่คุณประสงค์จะส่งข้ามขอบเขตออกมาต่อท้ายประโยคคำสั่ง break ที่ใช้คุมลูปได้เลย ซึ่งค่าความสัมพันธ์ชิ้นนั้นจะถูกโยนออกมาด้านนอกลูปให้คุณจับจองใช้งานได้ทันที ดังตัวอย่างนี้:
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {result}");
}
ก่อนเริ่มสั่งงานลูป เราได้ทำสร้างตัวแปรตัวหนึ่งชื่อว่า counter ตั้งต้นเก็บค่าเป็น 0 จากนั้นเราประกาศจัดตั้งตัวแปรชื่อว่า result คอยเก็บข้อมูลส่งกลับขากลับจากลูป ภายในลูปในแต่ละรอบการทำงาน ตัวโปรแกรมจะประมวลผลเพิ่มค่าตัวเลขสะสม 1 ให้แก่ counter แล้วตามด้วยตรวจสอบว่าค่า counter มีขนาดเท่ากับ 10 พอดีหรือยัง และเมื่อสะสมไปถึงพิกัดสำเร็จ เราสั่งเรียกคำสั่ง break แนบพ่วงค่าที่ต้องการส่งกลับคำนวณย่อยเป็น counter * 2 ออกมาภายนอก ภายหลังเสร็จสิ้นกระบวนการของลูป เราใส่เครื่องหมายอัฒภาคลงท้ายเพื่อจบร่างประโยคคำสั่งที่กำหนดค่ามอบให้แก่ result และบรรทัดท้ายสุดเราแสดงผลค่าของ result ซึ่งประมวลผลได้ค่าเป็น 20 พอดี
คุณยังสามารถสั่งเรียก return จากด้านในลูปได้เช่นกัน พึงระลึกไว้ว่าในขณะที่คำสั่ง break จะมีผลเฉพาะเจาะจงให้ยุติทำงานข้ามออกมานอกลูปปัจจุบันเท่านั้น แต่คำสั่ง return จะมีผลสูงสุดคือสั่งการหยุดรันข้ามออกไปนอกฟังก์ชันปัจจุบันทันที
การจำแนกความสัมพันธ์ด้วยป้ายกำกับลูป (Loop Labels)
หากคุณมีลูปซ้อนลูป (nested loops) คำสั่ง break และ continue จะมีผลกับลูปในสุด ณ จุดนั้นเท่านั้น คุณสามารถระบุ ป้ายกำกับลูป (loop label) บนลูปได้ จากนั้นคุณสามารถนำป้ายกำกับไปใช้ร่วมกับ break หรือ continue เพื่อระบุให้คำสำคัญเหล่านั้นทำงานกับลูปที่มีป้ายกำกับแทนที่จะเป็นลูปในสุด ป้ายกำกับลูปจำเป็นต้องขึ้นต้นด้วยเครื่องหมายอัญประกาศเดี่ยว (single quote) เสมอ ต่อไปนี้เป็นตัวอย่างที่มีลูปซ้อนกันสองชั้น:
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}");
}
ลูปชั้นนอกมีป้ายกำกับว่า 'counting_up และจะนับเลขขึ้นจาก 0 ถึง 2 ลูปชั้นในที่ไม่มีป้ายกำกับจะนับถอยหลังลงจาก 10 ถึง 9 คำสั่ง break บรรทัดแรกที่ไม่ได้ระบุป้ายกำกับจะยุติการทำงานเฉพาะลูปชั้นในเท่านั้น ส่วนประโยคคำสั่ง break 'counting_up; จะมีผลให้ยุติการทำงานและออกจากลูปชั้นนอก โค้ดนี้จะแสดงผลลัพธ์ดังนี้:
$ 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
การเขียนวนซ้ำแบบมีเงื่อนไขด้วย while
บ่อยครั้งที่การวนซ้ำในโปรแกรมจำเป็นต้องประเมินเงื่อนไขประกอบไปด้วย ในระหว่างที่เงื่อนไขเป็นจริง (true) ลูปจะทำงานต่อไป และเมื่อใดที่เงื่อนไขเปลี่ยนเป็นเท็จ โปรแกรมจะเรียกใช้ break เพื่อหยุดลูป เราสามารถสร้างพฤติกรรมนี้ได้โดยใช้ loop, if, else และ break ร่วมกัน แต่เนื่องจากรูปแบบนี้พบบ่อยมาก Rust จึงมีโครงสร้างเฉพาะเตรียมไว้ให้ในตัวภาษา นั่นคือลูปแบบ while ในรายการที่ 3-3 เราจะใช้ while ในการรันโค้ดซ้ำ 3 ครั้ง โดยนับถอยหลังในแต่ละรอบ และแสดงข้อความจบลูปก่อนจะสิ้นสุดการทำงาน
fn main() {
let mut number = 3;
while number != 0 {
println!("{number}!");
number -= 1;
}
println!("LIFTOFF!!!");
}
โครงสร้างนี้ช่วยลดการเขียนโค้ดซ้อนกันหลายชั้น (nesting) ที่อาจเกิดขึ้นหากเลือกใช้ loop, if, else และ break ส่งผลให้โค้ดมีความชัดเจนและอ่านง่ายขึ้น ในขณะที่เงื่อนไขยังคงเป็นจริง โค้ดภายในบล็อกจะถูกรันซ้ำ หากเงื่อนไขเป็นเท็จ โปรแกรมจะออกจากลูปทันที
การวนซ้ำเพื่ออ่านข้อมูลในคอลเลกชันด้วย for
คุณสามารถเลือกใช้โครงสร้าง while เพื่อวนลูปอ่านข้อมูลของสมาชิกแต่ละชิ้นในคอลเลกชัน เช่น อาร์เรย์ ได้ ตัวอย่างเช่น ลูปในรายการที่ 3-4 จะพิมพ์ค่าสมาชิกแต่ละตัวในอาร์เรย์ 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;
}
}
ในส่วนนี้ โค้ดจะคอยนับดัชนีสะสมเพิ่มขึ้นไปตามจำนวนสมาชิกในอาร์เรย์ โดยเริ่มต้นทำงานที่ตำแหน่งดัชนีชี้ 0 แล้วรันวนซ้ำไปจนกระทั่งดัชนีชี้พิกัดขอบเขตสูงสุดของอาร์เรย์ (นั่นคือเมื่อเงื่อนไขประเมินค่า index < 5 มีสภาวะไม่เป็นจริงอีกต่อไป) การสั่งประมวลผลโค้ดส่วนนี้จะแสดงรายงานผลลัพธ์สมาชิกทุกตำแหน่งออกมาดังนี้:
$ 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
ผลลัพธ์ตัวเลขทั้งห้าค่าของอาร์เรย์จะปรากฏแสดงบนเทอร์มินัลตามเป้าหมาย แม้ว่าตัวแปรดัชนี index จะสะสมค่าไปจนถึงพิกัด 5 ณ ท้ายกระบวนการก็ตาม แต่ลูปจะได้รับการสั่งการให้ยุติทำงานลงก่อนขั้นตอนการเข้าถึงเรียกหาตำแหน่งชิ้นข้อมูลตัวที่หกในอาร์เรย์
อย่างไรก็ตาม วิธีการนี้ค่อนข้างมีความเสี่ยงที่จะเกิดข้อผิดพลาดได้ง่าย (error-prone) เนื่องจากเราอาจทำให้โปรแกรมเกิดการแครชหยุดชะงัก (panic) ได้หากการตั้งค่าดัชนีหรือตัวดักกรองตรวจเงื่อนไขเกิดผิดพลาด ตัวอย่างเช่น หากคุณแก้ไขขนาดโครงสร้างอาร์เรย์ a ให้มีสมาชิกเหลือเพียง 4 ตัว แต่ดันลืมเข้าไปแก้ไขตัวตรวจเงื่อนไขลูปเป็น while index < 4 ตัวโปรแกรมจะแครชในที่สุด ยิ่งไปกว่านั้น วิธีการนี้ทำงานค่อนข้างช้าเนื่องจากคอมไพเลอร์จำเป็นต้องเพิ่มชุดคำสั่งเบื้องหลังเข้ามาคอยสแกนตรวจสอบความถูกต้องว่าดัชนีที่ป้อนนั้นอยู่ในขอบเขตขนาดของอาร์เรย์จริงหรือไม่ในทุกๆ รอบที่มีการรันทำงานวนลูป
เพื่อแนวทางแก้ไขที่สั้นกระชับและปลอดภัยยิ่งขึ้น คุณสามารถเลือกเรียกใช้งานลูปแบบ for เพื่อสั่งการรันประมวลผลโค้ดสำหรับสมาชิกแต่ละชิ้นในคอลเลกชันได้โดยตรง โครงสร้างลูป for แสดงตัวอย่างดังในรายการที่ 3-5
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {element}");
}
}
เมื่อสั่งรันรหัสโค้ดชุดนี้ เราจะได้ผลรายงานผลลัพธ์เดียวกับข้อมูลที่ได้รับไปในรายการที่ 3-4 แต่ประเด็นที่สำคัญยิ่งกว่าคือ ตอนนี้เราได้ยกระดับความคุ้มกันความปลอดภัยให้แก่โค้ดและป้องกันสภาวะการเกิดบั๊กที่อาจจะมาจากการเข้าถึงดัชนีชี้ตำแหน่งเลยความจุของอาร์เรย์ หรืออ่านข้อมูลไม่ครบทุกตำแหน่งได้อย่างถาวร ยิ่งไปกว่านั้น โครงสร้างโค้ดเครื่องประมวลผล (machine code) ที่สร้างขึ้นมาจากการคอมไพเลอร์ลูป for จะมีระดับการประมวลผลที่มีประสิทธิภาพสูงกว่าด้วย เนื่องจากระบบไม่จำเป็นต้องมาคอยชั่งวัดค่าตัวแปรดัชนีเปรียบเทียบกับขนาดความยาวอาร์เรย์ซ้ำๆ ในทุกรอบวนลูป
ด้วยประโยชน์จากการประยุกต์ลูป for นี้ คุณไม่จำเป็นต้องกังวลที่จะต้องคอยสลับกลับมาคอยอัปเดตเปลี่ยนค่าตัวเลขตำแหน่งเงื่อนไขดักตรวจจับตัวเลขใดๆ เลยหากในภายหลังคุณมีการแก้ไขปริมาณความจุรายการสมาชิกในอาร์เรย์ ซึ่งจะแตกต่างไปจากวิธีดั้งเดิมที่ใช้ในตารางที่ 3-4
ข้อเด่นในเรื่องความปลอดภัยและความสั้นกระชับของการใช้งานลูป for ส่งผลให้มันกลายเป็นลูปควบคุมการวนซ้ำที่ถูกเลือกใช้งานอย่างแพร่หลายที่สุดใน Rust แม้แต่สภาวะที่คุณมีความประสงค์ต้องการสั่งการให้โปรแกรมทำงานวนซ้ำๆ ตามจำนวนรอบที่ระบุกำหนดชัดเจน เช่น การนับถอยหลังด้วยลูป while ในรายการที่ 3-3 สมาชิกส่วนใหญ่ชาว Rustacean ก็นิยมเลือกประยุกต์ใช้งานลูป for แทนที่อยู่ดี วิธีการประยุกต์งานลักษณะนี้จะใช้อุปกรณ์ช่วงขอบเขต Range ที่จัดหาให้ในระบบไลบรารีมาตรฐาน ซึ่งจะช่วยสร้างกลุ่มตัวเลขเรียงตามลำดับสะสมจากค่าตัวแรกไปจนจบพิกัดถัดก่อนหน้าเลขปลายทาง
ตัวอย่างภาพการนับถอยหลังด้วยลูป for และการเรียกใช้งานร่วมกับอีกเครื่องมือที่เรายังไม่มีโอกาสพูดถึงอย่าง rev สำหรับสลับกลับด้านช่วงตัวเลขขอบเขต มีดังนี้:
Filename: src/main.rs
fn main() {
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!!!");
}
โค้ดส่วนนี้ดูสวยงามและสะอาดตากว่าเดิมมากเลยใช่ไหมครับ?
สรุป
คุณทำสำเร็จแล้ว! บทนี้นับเป็นบทเรียนที่มีขนาดข้อมูลและเนื้อความที่แน่นและจุใจมาก: คุณได้เรียนรู้เกี่ยวกับตัวแปร, ชนิดข้อมูลประเภทสเกลาร์และเชิงประกอบ, ฟังก์ชัน, คำอธิบายโค้ด, นิพจน์เงื่อนไข if ตลอดจนโครงสร้างการวนลูป! เพื่อทบทวนทักษะฝีมือความรู้เกี่ยวกับแนวคิดต่างๆ ที่เราคุยกันในบทนี้ ให้ทดลองสร้างโปรแกรมจัดการแก้ไขโจทย์เหล่านี้เล่นกันดูครับ:
- ตัวแปลงค่าหน่วยอุณหภูมิสลับไปมาระหว่าง Fahrenheit และ Celsius
- การคำนวณสุ่มหาลำดับเลขฟีโบนัชชี (Fibonacci number) ตำแหน่งที่ n
- การสั่งพิมพ์เนื้อเพลงคริสต์มาสคลาสสิก “The Twelve Days of Christmas” โดยประยุกต์ใช้ประโยชน์จากการรันลูปพิมพ์ทวนทับซ้ำๆ ของเพลง
และเมื่อคุณมีความพร้อมที่จะก้าวเดินหน้าต่อ หัวข้อถัดไปเราจะมาร่วมศึกษากับแนวคิดหลักของ Rust ซึ่งเป็นคุณลักษณะเด่นเฉพาะตัวที่ไม่ค่อยมีให้เห็นในภาษาโปรแกรมอื่น นั่นคือระบบสิทธิ์การครอบครองหน่วยความจำ (ownership) ครับ