ควรจะ panic! หรือไม่ควรดี? (To panic! or Not to panic!)
คำถามคือ คุณจะตัดสินใจอย่างไรว่าเมื่อใดควรเรียกใช้ panic! และเมื่อใดควรส่งคืนค่าเป็น Result? เมื่อโค้ดเกิดการ panic จะไม่มีทางกู้คืนการทำงานกลับมาได้เลย คุณสามารถเลือกที่จะเรียก panic! ได้ในทุกสถานการณ์ที่เกิดข้อผิดพลาด ไม่ว่าจะมีหนทางกู้คืนได้หรือไม่ก็ตาม แต่การทำเช่นนั้นคือการตัดสินใจแทนโค้ดผู้เรียกใช้งานไปแล้วว่าสถานการณ์นั้นไม่สามารถกู้คืนได้ แต่เมื่อคุณเลือกที่จะส่งคืนค่าแบบ Result คุณจะมอบทางเลือกให้กับโค้ดผู้เรียก โดยโค้ดผู้เรียกสามารถเลือกที่จะพยายามกู้คืนระบบในลักษณะที่เหมาะสมกับสถานการณ์ของตนเอง หรือตัดสินใจว่าออบเจกต์ Err ในกรณีนี้ไม่สามารถกู้คืนได้ แล้วจึงสั่งเรียกใช้ panic! เพื่อเปลี่ยนข้อผิดพลาดที่แก้ไขได้ของคุณให้กลายเป็นข้อผิดพลาดที่แก้ไขไม่ได้แทน ดังนั้น การส่งคืนค่าแบบ Result จึงเป็นตัวเลือกเริ่มต้นที่ดีเมื่อคุณกำลังนิยามฟังก์ชันที่มีโอกาสทำงานล้มเหลว
สำหรับสถานการณ์บางอย่าง เช่น โค้ดตัวอย่าง, โค้ดต้นแบบ (prototype), และการเขียนชุดการทดสอบ (tests) การเขียนโค้ดเพื่อให้เกิดการ panic จะมีความเหมาะสมมากกว่าการส่งคืนค่าแบบ Result เรามาศึกษากันว่าเพราะอะไร จากนั้นจึงมาอภิปรายถึงสถานการณ์ที่คอมไพเลอร์ไม่สามารถระบุได้ว่าความล้มเหลวนั้นเป็นไปไม่ได้ แต่คุณในฐานะมนุษย์สามารถบอกได้ และบทนี้จะปิดท้ายด้วยคำแนะนำทั่วไปเกี่ยวกับแนวทางการตัดสินใจว่าเมื่อใดควรใช้ panic ในโค้ดประเภทไลบรารี
โค้ดตัวอย่าง, โค้ดต้นแบบ, และชุดการทดสอบ (Examples, Prototype Code, and Tests)
เมื่อคุณเขียนโค้ดตัวอย่างเพื่ออธิบายแนวคิดบางประการ การใส่โค้ดจัดการข้อผิดพลาดที่แข็งแกร่ง (robust error-handling) เข้าไปด้วยอาจทำให้โค้ดตัวอย่างดูไม่ชัดเจนและเข้าใจยาก ในโค้ดตัวอย่าง เป็นที่เข้าใจกันว่าการเรียกใช้เมธอดอย่าง unwrap ซึ่งมีโอกาสเกิด panic ได้นั้น มีไว้เพื่อเป็นตัวสำรอง (placeholder) แทนวิธีการจัดการข้อผิดพลาดจริงที่คุณต้องการใช้ในแอปพลิเคชันของคุณ ซึ่งกระบวนการจัดการนี้อาจแตกต่างกันออกไปขึ้นอยู่กับว่าส่วนที่เหลือของโค้ดทำหน้าที่อะไร
ทำนองเดียวกัน เมธอด unwrap และ expect จะมีประโยชน์อย่างมากเมื่อคุณกำลังเขียนโค้ดต้นแบบ (prototyping) และยังไม่พร้อมที่จะตัดสินใจว่าจะจัดการกับข้อผิดพลาดอย่างไร พวกมันทำหน้าที่เป็นเครื่องหมายสัญลักษณ์ระบุตำแหน่งในโค้ดไว้อย่างชัดเจน เพื่อให้คุณกลับมาแก้ไขปรับโปรแกรมให้แข็งแกร่งยิ่งขึ้นเมื่อพร้อม
หากคำสั่งเรียกเมธอดล้มเหลวในการทดสอบ (test) คุณจะต้องการให้การทดสอบทั้งหมดมีสถานะล้มเหลว (fail) ทันที แม้ว่าเมธอดดังกล่าวจะไม่ได้เป็นฟังก์ชันการทำงานที่อยู่ภายใต้การทดสอบนั้นโดยตรงก็ตาม เนื่องจากระบบการทดสอบจะระบุว่าล้มเหลวผ่านทาง panic! ดังนั้นการเลือกเรียกใช้เมธอด unwrap หรือ expect จึงเป็นสิ่งที่สมควรดำเนินขั้นตอนในกรณีนี้อย่างถูกต้องที่สุด
เมื่อคุณมีข้อมูลรายละเอียดมากกว่าคอมไพเลอร์ (When You Have More Information Than the Compiler)
การเลือกเรียกใช้งาน expect จะมีความเหมาะสมเช่นเดียวกันเมื่อคุณมีตรรกะในส่วนอื่น ๆ ที่ช่วยยืนยันได้ว่าออบเจกต์ Result จะคืนค่าเป็น Ok แน่นอน แต่ตรรกะนั้นอยู่นอกเหนือความเข้าใจของคอมไพเลอร์ อย่างไรก็ดีคุณยังคงมีออบเจกต์ Result ที่ต้องคอยเขียนโค้ดจัดการอยู่: เพราะคำสั่งประมวลผลใด ๆ ที่คุณเรียกใช้ก็ยังคงมีโอกาสเกิดความล้มเหลวได้ในกรณีทั่วไป แม้ว่าในสถานการณ์เฉพาะเจาะจงของคุณมันจะเป็นไปไม่ได้ในทางตรรกะก็ตาม หากคุณตรวจสอบโค้ดด้วยตนเองจนมั่นใจเต็มร้อยว่าไม่มีวันเกิดกรณีตัวแปรย่อย Err แน่นอน คุณก็สามารถเลือกเรียกใช้ expect ได้อย่างไม่มีปัญหา และใส่บันทึกอธิบายเหตุผลที่คุณคิดว่ามันไม่มีวันเกิดข้อผิดพลาดแบบ Err ไว้ในข้อความอาร์กิวเมนต์ตัวแปรได้ ตัวอย่างเช่น:
fn main() {
use std::net::IpAddr;
let home: IpAddr = "127.0.0.1"
.parse()
.expect("Hardcoded IP address should be valid");
}
เราสร้างออบเจกต์ IpAddr ขึ้นมาจากกระบวนการแปลงค่า (parsing) ข้อความสตริงที่ถูกกำหนดไว้ตายตัว (hardcoded string) เราจะเห็นได้อย่างชัดเจนว่า "127.0.0.1" เป็นที่อยู่ IP ที่ถูกต้องสมบูรณ์ ดังนั้นการเลือกใช้เมธอด expect ที่นี่จึงยอมรับได้ อย่างไรก็ตาม การใช้สตริงที่ถูกต้องแบบ hardcode ไม่ได้ไปเปลี่ยนชนิดประเภทข้อมูลส่งคืนของเมธอด parse แต่อย่างใด: เรายังคงได้รับค่ากลับคืนมาเป็น Result และคอมไพเลอร์ก็ยังคงบังคับให้เราจัดการกับ Result นั้นเสมือนว่ามีโอกาสเกิดตัวแปรย่อย Err ได้อยู่ดี เนื่องจากคอมไพเลอร์ไม่ได้ฉลาดพอที่จะตรวจวิเคราะห์ได้ว่าข้อความสตริงนี้คือ IP address ที่ถูกต้องเสมอ หากข้อความ IP address นั้นมาจากผู้ใช้งานป้อนข้อมูลเข้ามา แทนที่จะเป็นการฝังค่าตายตัวลงไปในโปรแกรม และส่งผลให้มีโอกาสล้มเหลวได้จริง ในกรณีนั้นเราจำเป็นต้องจัดการกับ Result ด้วยแนวทางที่แข็งแกร่งเป็นสัดส่วนมากกว่านี้อย่างแน่นอน การเขียนบันทึกกำกับสมมติฐานว่า IP address นี้ถูก hardcode เอาไว้ จะช่วยเตือนใจให้เรากลับมาเปลี่ยนจากเมธอด expect เป็นโค้ดจัดการข้อผิดพลาดที่ดีขึ้นได้ในอนาคต หากเราจำเป็นต้องเปลี่ยนไปดึงข้อมูล IP จากแหล่งข้อมูลอื่นแทน
แนวทางปฏิบัติสำหรับการจัดการข้อผิดพลาด (Guidelines for Error Handling)
เป็นการสมควรที่จะกำหนดให้โค้ดเกิด panic เมื่อมีโอกาสที่ขั้นตอนการทำงานของโปรแกรมจะตกไปอยู่ในสถานะที่ย่ำแย่ (bad state) ในบริบทนี้ สถานะที่ย่ำแย่ หมายถึงสถานการณ์ที่สมมติฐาน การรับประกัน ข้อตกลงการทำงาน (contract) หรือกฎเกณฑ์ที่ไม่แปรเปลี่ยน (invariant) ได้ถูกฝ่าฝืนและทำลายลง เช่น การมีค่าที่ไม่ถูกต้อง, ค่าที่ขัดแย้งกันเอง หรือค่าที่ขาดหายไปถูกส่งเข้ามายังโค้ดของคุณ รวมถึงตรงตามเงื่อนไขข้อใดข้อหนึ่งหรือมากกว่าดังต่อไปนี้:
- สถานะที่ย่ำแย่นั้นเป็นสิ่งที่ไม่คาดคิด ต่างจากสิ่งที่มีโอกาสเกิดขึ้นได้เป็นครั้งคราวตามธรรมชาติ เช่น ผู้ใช้กรอกข้อมูลผิดรูปแบบ
- โค้ดของคุณหลังจากจุดนี้ไปจำเป็นต้องพึ่งพาสมมติฐานว่าระบบไม่ได้อยู่ในสถานะที่ย่ำแย่นี้ แทนที่จะต้องคอยเขียนโค้ดตรวจสอบเงื่อนไขปัญหาในทุก ๆ บรรทัดคำสั่ง
- ไม่มีแนวทางที่ดีในการแปลงข้อมูลรายละเอียดข้อจำกัดนี้ให้อยู่ในรูปของโครงสร้างประเภทข้อมูลที่คุณเลือกใช้งาน เราจะนำเสนอตัวอย่างของแนวคิดนี้ในหัวข้อ “การบันทึกสถานะและพฤติกรรมในรูปของประเภทข้อมูล (Encoding States and Behavior as Types)” ในบทที่ 18
หากมีคนเรียกใช้โค้ดของคุณและส่งค่าอาร์กิวเมนต์ที่ไม่มีเหตุผลเข้ามา วิธีที่ดีที่สุดคือการส่งคืนข้อผิดพลาดกลับไปเพื่อให้ผู้ใช้งานไลบรารีนำไปตัดสินใจเองว่าจะดำเนินการอย่างไรต่อในกรณีนั้น ทว่าในกรณีที่การประมวลผลขั้นตอนคำสั่งต่อไปอาจก่อให้เกิดความไม่ปลอดภัยหรือความเสียหายต่อระบบ ทางเลือกที่ดีที่สุดอาจเป็นการเรียกใช้คำสั่ง panic! เพื่อแจ้งเตือนนักพัฒนาที่เป็นคนเรียกใช้งานไลบรารีของคุณว่ามีบั๊กในโค้ดฝั่งของเขา เพื่อที่เขาจะได้ทำการแก้ไขปัญหานั้นเสียตั้งแต่ขั้นตอนพัฒนาโปรแกรม ในทำนองเดียวกัน การสั่ง panic! มักจะมีความเหมาะสมหากคุณกำลังเรียกใช้โค้ดภายนอกที่อยู่นอกเหนือการควบคุมของคุณและโค้ดนั้นส่งคืนสถานะที่ไม่ถูกต้องกลับมาซึ่งคุณไม่มีทางเข้าไปแก้ไขได้เลย
อย่างไรก็ตาม เมื่อความล้มเหลวเป็นเรื่องที่สามารถคาดหมายได้ทั่วไป การส่งคืนค่าแบบ Result จะมีความเหมาะสมมากกว่าการสั่ง panic! ตัวอย่างเช่น เครื่องวิเคราะห์ข้อมูล (parser) ได้รับข้อมูลที่ผิดรูปแบบ หรือคำขอ HTTP (HTTP request) ส่งคืนรหัสสถานะที่ระบุว่าคุณเข้าใช้งานเกินขีดจำกัดความเร็ว (rate limit) ในสถานการณ์เหล่านี้ การส่งคืนค่า Result บ่งบอกว่าความล้มเหลวเป็นทางเลือกที่มีโอกาสเกิดขึ้นตามปกติทั่วไปซึ่งโค้ดผู้เรียกต้องเป็นฝ่ายรับหน้าที่ไปตัดสินใจเองว่าจะจัดการรับมืออย่างไร
เมื่อโค้ดของคุณทำขั้นตอนคำสั่งประมวลผลที่อาจทำให้ผู้ใช้ตกอยู่ในความเสี่ยงหากมีการเรียกใช้ด้วยค่าที่ไม่ถูกต้อง โค้ดของคุณควรตรวจสอบก่อนว่าค่าต่าง ๆ ถูกต้องสมบูรณ์ดี และสั่ง panic หากพบว่าไม่ถูกต้อง เหตุผลนี้สำคัญอย่างยิ่งในแง่ความปลอดภัย: การฝืนประมวลผลคำสั่งกับข้อมูลที่ไม่ถูกต้องอาจเปิดช่องโหว่ความเสี่ยงให้กับโค้ดของคุณได้ และนี่คือเหตุผลหลักที่ทำไมไลบรารีมาตรฐานจะเรียกใช้ panic! หากคุณพยายามเข้าถึงตำแหน่งข้อมูลหน่วยความจำที่เกินขอบเขต (out-of-bounds access): การพยายามเข้าถึงหน่วยความจำที่ไม่ได้เป็นของโครงสร้างข้อมูลปัจจุบันถือเป็นปัญหาความปลอดภัยที่พบบ่อยมาก บ่อยครั้งที่ฟังก์ชันจะมี ข้อตกลงการทำงาน (contracts): พฤติกรรมของฟังก์ชันจะได้รับการรับประกันเฉพาะเมื่อข้อมูลนำเข้าเป็นไปตามข้อกำหนดเฉพาะที่ตั้งไว้ การสั่งให้เกิด panic เมื่อข้อตกลงนั้นถูกฝ่าฝืนจึงมีความเหมาะสม เพราะการละเมิดข้อตกลงจะบ่งบอกถึงบั๊กจากฝั่งผู้เรียกใช้งานเสมอ และมันไม่ใช่รูปแบบข้อผิดพลาดที่คุณจะต้องการปล่อยให้โค้ดผู้เรียกต้องคอยเขียนประโยรับมืออย่างเปิดเผย แท้จริงแล้วไม่มีวิธีที่สมเหตุสมผลใด ๆ ที่โค้ดผู้เรียกจะสามารถกู้คืนการทำงานกลับมาได้ แต่นักพัฒนาที่เป็นผู้เขียนโค้ดจำเป็นต้องเข้ามาแก้ไขโค้ดต่างหาก ข้อกำหนดข้อตกลงการทำงานสำหรับฟังก์ชัน โดยเฉพาะอย่างยิ่งกรณีที่การฝ่าฝืนจะส่งผลให้เกิด panic ควรระบุและอธิบายไว้ให้ชัดเจนในส่วนเอกสาร API ของฟังก์ชันนั้น ๆ
อย่างไรก็ตาม การใส่คำสั่งตรวจสอบข้อผิดพลาดจำนวนมากไว้ในฟังก์ชันทั้งหมดของคุณจะทำให้โค้ดเยิ่นเย้อและน่ารำคาญ โชคดีที่คุณสามารถใช้ประโยชน์จากระบบประเภทข้อมูลของ Rust (และรวมถึงระบบตรวจเช็คประเภทข้อมูลของคอมไพเลอร์) เพื่อทำหน้าที่ตรวจสอบหลาย ๆ สิ่งแทนคุณได้ หากฟังก์ชันของคุณกำหนดประเภทข้อมูลเฉพาะเจาะจงเป็นพารามิเตอร์ คุณก็จะเขียนตรรกะการประมวลผลต่อไปได้ทันทีโดยอิงตามสมมติฐานว่าคอมไพเลอร์ได้ตรวจสอบความถูกต้องของค่านั้นมาให้แล้ว ตัวอย่างเช่น หากคุณระบุโครงสร้างประเภทข้อมูลโดยตรงแทนที่จะใส่ใน Option ตัวโปรแกรมจะคาดหวังว่าต้องมี ข้อมูลบางอย่าง อยู่จริง ไม่ใช่ความว่างเปล่า โค้ดของคุณจึงไม่ต้องคอยจัดการกับสองกรณีแยกสำหรับตัวแปรย่อย Some และ None อีกต่อไป: จะเหลือเพียงกรณีเดียวคือต้องมีค่าข้อมูลอยู่แน่นอน โค้ดใด ๆ ที่พยายามจะส่งค่าความว่างเปล่ามายังฟังก์ชันนี้จะไม่สามารถคอมไพล์ผ่านตั้งแต่แรก ดังนั้นฟังก์ชันของคุณจึงไม่ต้องทำคำสั่งตรวจสอบจุดนี้ในตอนรันไทม์ (runtime) อีกตัวอย่างหนึ่งคือการใช้ประเภทจำนวนเต็มแบบไม่มีเครื่องหมาย เช่น u32 ซึ่งจะช่วยรับประกันได้ว่าค่าพารามิเตอร์จะไม่มีวันติดลบแน่นอน
การสร้างประเภทข้อมูลขึ้นเองเพื่อใช้ตรวจสอบความถูกต้อง (Custom Types for Validation)
เราลองยกระดับแนวคิดการใช้ระบบประเภทข้อมูลของ Rust เพื่อช่วยรับประกันว่าเราได้ค่าที่ถูกต้องไปอีกขั้นหนึ่ง โดยการหันมาศึกษาเรื่องการสร้างประเภทข้อมูลเฉพาะขึ้นมาเองเพื่อวัตถุประสงค์ในการตรวจสอบความถูกต้อง ย้อนความกลับไปโครงการเกมทายตัวเลขในบทที่ 2 ซึ่งโค้ดของเราได้บอกให้ผู้ใช้ทายตัวเลขระหว่าง 1 ถึง 100 ในตอนนั้นเราไม่ได้เขียนคำสั่งตรวจสอบเลยว่าค่าที่ผู้ใช้ทายมานั้นอยู่ระหว่างตัวเลขเหล่านั้นจริงหรือไม่ก่อนจะนำไปเปรียบเทียบกับตัวเลขปริศนา เราตรวจสอบเพียงแค่ค่าที่ทายเข้ามาต้องเป็นจำนวนบวกเท่านั้น ในกรณีนั้น ผลลัพธ์ที่ตามมาไม่ได้ร้ายแรงนัก: การพิมพ์แจ้งเตือนว่า “สูงเกินไป” หรือ “ต่ำเกินไป” ของโปรแกรมก็ยังคงให้ข้อมูลที่ถูกต้อง แต่ทว่ามันจะเป็นพัฒนาการที่มีประโยชน์มากหากเราสามารถแนะนำทางเลือกให้ผู้ใช้ทายค่าตัวเลขที่ถูกต้อง และกำหนดพฤติกรรมการทำงานให้แตกต่างกันเมื่อผู้ใช้ทายตัวเลขนอกช่วงเป้าหมาย เปรียบเทียบกับกรณีที่ผู้ใช้เผลอพิมพ์ตัวอักษรเข้ามาแทน
แนวทางหนึ่งคือการเปลี่ยนมาทำการแปลงผลลัพธ์คำทาย (parse) ให้เป็นชนิด i32 แทนที่จะเป็น u32 เพียงอย่างเดียวเพื่อเปิดโอกาสให้รองรับค่าตัวเลขติดลบได้ จากนั้นค่อยใส่คำสั่งเช็คเงื่อนไขว่าตัวเลขอยู่ในช่วงเป้าหมายหรือไม่ ดังนี้:
use std::cmp::Ordering;
use std::io;
use rand::prelude::*;
fn main() {
println!("Guess the number!");
let secret_number = rand::rng().random_range(1..=100);
loop {
// --snip--
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: i32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
if guess < 1 || guess > 100 {
println!("The secret number will be between 1 and 100.");
continue;
}
match guess.cmp(&secret_number) {
// --snip--
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
คำสั่งนิพจน์ if จะตรวจสอบว่าค่าของเราอยู่นอกช่วงเป้าหมายหรือไม่ แจ้งปัญหาให้ผู้ใช้ทราบ และเรียกใช้เมธอด continue เพื่อเริ่มต้นรอบถัดไปของลูปและบอกให้ทายใหม่อีกครั้ง หลังจากโครงสร้างคำสั่ง if นี้ไป เราสามารถเปรียบเทียบระหว่างค่า guess กับตัวเลขปริศนาต่อไปได้อย่างมั่นใจว่าค่าของ guess จะอยู่ระหว่าง 1 ถึง 100 แน่นอน
อย่างไรก็ตาม วิธีนี้ยังไม่ใช่วิธีแก้ปัญหาที่ดีที่สุด: หากตัวโปรแกรมมีความจำเป็นระดับวิกฤตที่ต้องทำงานเฉพาะกับค่าระหว่าง 1 ถึง 100 เท่านั้น และโปรแกรมมีฟังก์ชันจำนวนมากที่มีข้อกำหนดแบบเดียวกันนี้ การเขียนตรวจสอบโครงสร้างเงื่อนไขแบบนี้ในทุก ๆ ฟังก์ชันจะกลายเป็นงานที่น่าเบื่อเหนื่อยหน่าย (และอาจจะส่งผลเสียต่อประสิทธิภาพการประมวลผลด้วย)
ทางเลือกอื่นที่ดีกว่าคือ เราสามารถสร้างประเภทข้อมูลใหม่ในมอดูล (module) ที่แยกออกไปโดยเฉพาะ และรวมศูนย์เงื่อนไขการตรวจสอบทั้งหมดไว้ในฟังก์ชันสร้างอินสแตนซ์ของประเภทข้อมูลนั้นแทนที่จะต้องเขียนเงื่อนไขการตรวจสอบซ้ำ ๆ ทั่วกระบวนการ ด้วยวิธีนี้ แต่ละฟังก์ชันจะนำประเภทข้อมูลใหม่นั้นไปใช้ในส่วนซิกเนเจอร์พารามิเตอร์ได้อย่างปลอดภัย และนำค่าที่ได้รับไปใช้งานต่อได้อย่างมั่นใจ ตัวอย่างที่ 9-13 แสดงวิธีการหนึ่งในการกำหนดประเภทข้อมูล Guess ซึ่งจะยอมสร้างอินสแตนซ์ของ Guess ขึ้นมาเฉพาะเมื่อฟังก์ชัน new ได้รับข้อมูลตัวเลขระหว่าง 1 ถึง 100 เท่านั้น
#![allow(unused)]
fn main() {
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {value}.");
}
Guess { value }
}
pub fn value(&self) -> i32 {
self.value
}
}
}
สังเกตว่าโค้ดชุดนี้ในไฟล์ src/guessing_game.rs ขึ้นตรงกับการเพิ่มบรรทัดประกาศมอดูล mod guessing_game; ในไฟล์ src/lib.rs ซึ่งเราไม่ได้แสดงให้ดู ณ ที่นี้ ภายในไฟล์มอดูลใหม่นี้เรานิยาม struct ชื่อว่า Guess ซึ่งมีฟิลด์ข้อมูลชื่อว่า value ชนิดประเภทข้อมูล i32 จุดนี้คือตำแหน่งที่ใช้บันทึกจัดเก็บค่าตัวเลขจริง
จากนั้น เราเขียนคำสั่งนำเสนอ (implement) ฟังก์ชันที่เกี่ยวข้อง (associated function) ชื่อว่า new ให้กับ Guess เพื่อใช้สำหรับสร้างออบเจกต์ค่า Guess ขึ้นมา ฟังก์ชัน new นี้ถูกระบุให้รับพารามิเตอร์ตัวหนึ่งชื่อว่า value ประเภท i32 และคืนค่าเป็น Guess ส่วนของโค้ดการทำงานภายในฟังก์ชัน new จะทำหน้าที่ตรวจสอบเงื่อนไขของตัวแปร value เพื่อให้มั่นใจได้เต็มที่ว่าค่าอยู่ระหว่าง 1 ถึง 100 หากตัวแปร value สอบตกจากเงื่อนไขตรวจสอบนี้ เราจะสั่งประมวลผล panic! ซึ่งจะทำหน้าที่เตือนนักพัฒนาที่เป็นคนเขียนโค้ดฝั่งเรียกใช้ฟังก์ชันว่ามีจุดบกพร่องที่เขาต้องเข้าไปแก้ไข เนื่องจากเงื่อนไขการสร้างอินสแตนซ์ Guess โดยมีค่า value นอกขอบเขตเป้าหมายจะถือเป็นการทำลายข้อตกลงการทำงาน (contract) ที่ Guess::new คาดหวังและพึ่งพิงไว้ โดยข้อมูลเงื่อนไขรายละเอียดที่อาจทำให้ Guess::new เกิด panic ได้ควรระบุชี้แจงไว้ในเอกสาร API สำหรับเผยแพร่สู่ภายนอก ซึ่งเราจะมาศึกษาขนบการจัดทำเอกสารระบุรายละเอียดโอกาสเกิด panic! ของเอกสาร API ที่คุณสร้างขึ้นกันในบทที่ 14 หากตัวแปร value สอบผ่านจากเงื่อนไขตรวจสอบ เราจะสร้างอินสแตนซ์ Guess ตัวใหม่ขึ้นมาโดยระบุฟิลด์ value ให้เก็บค่าจากพารามิเตอร์ value และส่งออบเจกต์ Guess คืนกลับไป
ถัดไป เราเขียนนำเสนอเมธอดชื่อว่า value ซึ่งทำการยืมใช้งาน self โดยไม่มีการรับพารามิเตอร์ตัวแปรอื่น และคืนค่ากลับมาเป็นประเภท i32 เมธอดลักษณะเฉพาะเจาะจงแบบนี้มักเรียกกันว่า เกตเตอร์ (getter) เนื่องจากเป้าหมายมีไว้เพื่อดึงเอาข้อมูลบางอย่างจากฟิลด์ภายในส่งกลับคืนออกไป การเตรียมเมธอดสาธารณะ (public method) นี้มีความจำเป็นอย่างมากเนื่องจากฟิลด์ข้อมูล value ของ struct Guess มีสถานะเป็นฟิลด์ส่วนตัว (private field) จุดสำคัญคือฟิลด์ value จำเป็นต้องเป็นฟิลด์ส่วนตัวเพื่อป้องกันไม่ให้โค้ดอื่นภายนอกที่ดึง struct Guess ไปใช้งานแอบเข้ามากำหนดค่าตัวแปร value ได้โดยตรง: โค้ดใด ๆ ภายนอกมอดูล guessing_game ต้อง เรียกผ่านฟังก์ชัน Guess::new เท่านั้นเมื่อต้องการสร้างอินสแตนซ์ของ Guess ซึ่งกระบวนการนี้จะช่วยรับประกันได้อย่างสมบูรณ์แบบว่าออบเจกต์ Guess จะไม่มีวันเก็บค่า value ที่เล็ดลอดการตรวจสอบเงื่อนไขของฟังก์ชัน Guess::new ไปได้เลย
ดังนั้น ฟังก์ชันที่มีความต้องการพารามิเตอร์หรือส่งคืนเฉพาะตัวเลขระหว่าง 1 ถึง 100 ก็จะระบุในส่วนซิกเนเจอร์ประกาศพารามิเตอร์หรือข้อมูลส่งคืนได้เลยว่าเป็นชนิด Guess แทนที่จะเป็น i32 ทั่วไป และตัวฟังก์ชันเองก็ไม่จำเป็นต้องใส่ตรรกะเงื่อนไขตรวจสอบใด ๆ เพิ่มเติมไว้ในโค้ดการทำงานหลักอีกเลย
สรุป (Summary)
ฟีเจอร์การจัดการข้อผิดพลาดของภาษา Rust ได้รับการออกแบบมาเพื่อช่วยให้นักพัฒนาเขียนโค้ดที่แข็งแกร่งและเสถียรยิ่งขึ้น มาโคร panic! จะทำหน้าที่ส่งสัญญาณแจ้งเตือนว่าตัวโปรแกรมได้ตกลงไปอยู่ในสถานะที่ย่ำแย่จนระบบไม่สามารถประมวลผลคำสั่งต่อได้ และจะทำการส่งคำสั่งเพื่อหยุดกระบวนการทำงานแทนที่จะฝืนทำงานต่อด้วยค่าที่ไม่ถูกต้องหรือไม่สมบูรณ์ ส่วนตัวแปร enum Result จะใช้ระบบประเภทข้อมูลของ Rust เพื่อช่วยระบุชี้วัดว่าขั้นตอนประมวลผลคำสั่งดังกล่าวอาจพบอุปสรรคความล้มเหลวในลักษณะที่โค้ดของคุณสามารถบริหารจัดการกู้คืนระบบกลับมาได้ คุณสามารถใช้ประโยชน์ของ Result เพื่อแจ้งเตือนโค้ดระดับบนที่เป็นตัวเรียกใช้โค้ดของคุณว่าพวกเขาก็มีความรับผิดชอบที่ต้องคอยเขียนคำสั่งรับมือกรณีที่สำเร็จหรือล้มเหลวเช่นเดียวกัน การเลือกประยุกต์ใช้งานเครื่องมืออย่าง panic! และ Result ในสถานการณ์ที่ถูกต้องและสอดคล้องเหมาะสมจะทำให้โค้ดโปรแกรมของคุณมีความน่าเชื่อถือและเสถียรภาพสูงเมื่อต้องพบเจอกับปัญหาอุปสรรคที่หลีกเลี่ยงไม่ได้ในอนาคต
เมื่อคุณได้เห็นรูปแบบแนวทางที่มีประโยชน์ในการเลือกประยุกต์ใช้งานระบบเจเนอริก (generics) ร่วมกับ enum Option และ Result ของไลบรารีมาตรฐานแล้ว ถัดไปเราจะมาเรียนรู้เรื่องกลไกการทำงานของเจเนอริก และวิธีการนำระบบนี้ไปเขียนใช้งานจริงในโค้ดของคุณกันครับ