Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

โครงสร้างควบคุมการทำงาน

ความสามารถในการรันโค้ดบางส่วนโดยขึ้นอยู่กับว่าเงื่อนไขเป็นจริง (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) ครับ