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

การอ้างอิงและการยืมใช้ข้อมูล (References and Borrowing)

ประเด็นปัญหาของโค้ดส่งคืนแบบทูเพิลในรายการที่ 4-5 ก็คือ เราจำเป็นต้องส่งคืนค่า String ย้อนกลับมาให้ฟังก์ชันผู้เรียกใช้ เพื่อที่จะให้ตัวแปร String นั้นยังสามารถดึงไปใช้งานต่อได้หลังจากสิ้นสุดขั้นตอนการสั่งรันฟังก์ชัน calculate_length เนื่องจากก่อนหน้านี้ตัว String ได้ถูกสั่งย้ายสิทธิ์ (moved) เข้าไปยัง calculate_length เรียบร้อยแล้ว เพื่อเป็นทางเลือกในการจัดการ เราสามารถเปลี่ยนมาส่งผ่านค่าเป็น การอ้างอิง (reference) ไปยังข้อมูล String แทนได้ โดยที่ตัวอ้างอิงจะมีลักษณะพฤติกรรมเสมือนเป็นตัวชี้พิกัด pointer กล่าวคือทำหน้าที่เป็นข้อมูลระบุที่อยู่ของหน่วยความจำที่เราสามารถสะกดรอยตามเพื่อเข้าไปอ่านข้อมูลที่บันทึกอยู่ภายในช่องพิกัดที่อยู่ดังกล่าว โดยที่เนื้อข้อมูลชิ้นนั้นจะถูกถือครองสิทธิ์โดยตัวแปรรายอื่นอยู่ และข้อเด่นประการสำคัญที่เหนือกว่าตัวชี้ pointer คือ การอ้างอิงจะได้รับความคุ้มครองและรับประกันอย่างเข้มงวดว่าจะต้องระบุชี้ไปยังชิ้นข้อมูลที่มีมูลค่าถูกต้องสมบูรณ์และตรงชนิดข้อมูลประเภทนั้นๆ เสมอตลอดอายุขัยการทำงานของการอ้างอิงตัวนั้น

นี่คือตัวอย่างการกำหนดนิยามและการเรียกใช้งานฟังก์ชัน calculate_length โดยรับค่าตัวแปรพารามิเตอร์เป็นประเภทการอ้างอิงไปยังออบเจกต์ แทนที่จะต้องทำการโอนย้ายยึดสิทธิ์การครอบครองหน่วยความจำของข้อมูลนั้นเข้ามา:

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{s1}' is {len}.");
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

ข้อสังเกตประการแรกคือ ชุดโค้ดจัดการกระจายตัวแปรทูเพิลและค่าชนิดข้อมูลขากลับทูเพิลของฟังก์ชันจะหายไปทั้งหมด และประการที่สองคือ พึงสังเกตว่าเราได้เขียนระบุสั่งส่งค่าเป็น &s1 ป้อนเข้าไปในการทำงานของฟังก์ชัน calculate_length และในส่วนของนิยามฟังก์ชันปลายทาง เราเลือกที่จะยอมรับชนิดข้อมูลแบบการอ้างอิงข้อความ &String แทนการรับข้อมูลเต็มรูป String โดยสัญลักษณ์เครื่องหมายแอมเพอร์แซนด์ (&) นี้ทำหน้าที่เป็นตัวแทนสำหรับการอ้างอิงสิทธิ์ ซึ่งจะอนุญาตให้เราดึงชิ้นข้อมูลไปเชื่อมโยงใช้งานต่อได้โดยปราศจากการถ่ายโอนสิทธิ์การครอบครองหน่วยความจำ แผนภาพที่ 4-6 แสดงให้เห็นถึงแนวคิดพฤติกรรมนี้:

Three tables: the table for s contains only a pointer to the table for s1. The table for s1 contains the stack data for s1 and points to the string data on the heap.

รูปภาพที่ 4-6: แผนภาพแสดงข้อมูลตัวแปร &String s กำลังระบุอ้างอิงชี้ไปยัง String s1

หมายเหตุ: ทิศทางกลับกันของการอ้างอิงสิทธิ์ด้วยการเรียกใช้เครื่องหมาย & คือ การถอดการอ้างอิง (dereferencing) ซึ่งจะสามารถสั่งการได้โดยเรียกใช้ตัวดำเนินการถอดรหัสอ้างอิงสัญลักษณ์เครื่องหมายดอกจัน * เราจะได้เห็นตัวอย่างการเรียกใช้งานตัวดำเนินการนี้ในบทที่ 8 และพูดคุยรายละเอียดเชิงลึกเรื่องการถอดรหัสอ้างอิงในบทที่ 15

ลองมาวิเคราะห์ขั้นตอนการสั่งเรียกใช้ฟังก์ชันในจุดนี้อย่างใกล้ชิด:

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{s1}' is {len}.");
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

ไวยากรณ์สัญลักษณ์ &s1 ช่วยให้เราสร้างตัวแปรอ้างอิงเพื่อ ระบุชี้ ไปยังค่าข้อมูลของตัวแปร s1 โดยไม่ได้ครอบครองสิทธิ์ความเป็นเจ้าของหน่วยความจำของข้อมูลนั้น และเนื่องจากตัวแปรอ้างอิงนี้ไม่ได้ถือครองสิทธิ์ครอบครอง ชิ้นข้อมูลปลายทางที่มันชี้ระบุไปหาจึงไม่มีวันโดนสั่งทำลายคืนหน่วยความจำทิ้ง (dropped) เมื่อสิ้นสุดอายุขัยขอบเขตการทำงานของตัวแปรอ้างอิง

ทำนองเดียวกัน ในส่วนของลายเซ็นนิยามฟังก์ชัน เราเติมเครื่องหมาย & พ่วงเข้าไปเพื่อช่วยประกาศเตือนให้ระบบรับทราบว่าพารามิเตอร์ s นี้ยอมรับชนิดข้อมูลในรูปการอ้างอิงสิทธิ์ มาลองเขียนคำอธิบายโค้ดกำกับเพิ่มเติมกัน:

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{s1}' is {len}.");
}

fn calculate_length(s: &String) -> usize { // s is a reference to a String
    s.len()
} // Here, s goes out of scope. But because s does not have ownership of what
  // it refers to, the String is not dropped.

ขอบเขตการทำงานที่ตัวแปรอ้างอิง s จะมีผลทำงานได้จะสอดคล้องตรงตามขอบเขตระดับพารามิเตอร์ของฟังก์ชันทั่วไป แต่ชิ้นงานมูลค่าข้อมูลปลายทางที่ตัวแปรอ้างอิงนี้ชี้ระบุไปหาจะไม่มีวันโดนฟังก์ชัน drop ทำลายล้างคืนพื้นที่เมื่อสิ้นสุดการใช้งาน s เนื่องจากตัวแปร s ไม่ได้เป็นผู้ครอบครองสิทธิ์การเป็นเจ้าของหน่วยความจำ เมื่อเราออกแบบฟังก์ชันให้ยอมรับพารามิเตอร์เป็นประเภทการอ้างอิงแทนการโอนรับข้อมูลเต็ม เราจึงไม่มีความจำเป็นต้องคอยเขียนประโยคส่งข้อมูลขากลับคืนเพื่อส่งมอบสิทธิ์การครอบครองหน่วยความจำกลับคืนไปให้ เนื่องจากเราไม่เคยยึดถือครองสิทธิ์ครอบครองชิ้นนั้นมาตั้งแต่แรกเลย

เรานิยามกระบวนการสร้างตัวอ้างอิงนี้ว่าเป็นการ ยืมใช้ข้อมูล (borrowing) เช่นเดียวกับในชีวิตจริง หากมีใครบางคนเป็นเจ้าของทรัพยากรชิ้นหนึ่ง คุณสามารถทำเรื่องขอยืมของสิ่งนั้นมาจากเขาได้ และเมื่อคุณใช้สอยเสร็จสิ้นแล้ว คุณมีหน้าที่ต้องนำทรัพยากรชิ้นนั้นส่งคืนเจ้าของเดิม คุณไม่ได้มีสิทธิ์ครอบครองสิทธิ์ขาด

แล้วจะเกิดอะไรขึ้นหากเราพยายามที่จะเขียนโค้ดสั่งแก้ไขข้อมูลของทรัพยากรที่เรายืมมาใช้งาน? ให้ลองศึกษาโค้ดตัวอย่างในรายการที่ 4-6 และขอบอกใบ้ผลลัพธ์ล่วงหน้า: มันไม่ทำงานอย่างแน่นอน!

fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}

นี่คือข้อความแสดงข้อผิดพลาด:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
 --> src/main.rs:8:5
  |
8 |     some_string.push_str(", world");
  |     ^^^^^^^^^^^ `some_string` is a `&` reference, so it cannot be borrowed as mutable
  |
help: consider changing this to be a mutable reference
  |
7 | fn change(some_string: &mut String) {
  |                         +++

For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

เฉกเช่นเดียวกับลักษณะนิสัยตัวแปรปกติทั่วไปที่จะแก้ไขค่าไม่ได้เป็นค่าเริ่มต้น (immutable by default) ระบบตัวแปรอ้างอิงก็รับข้อตกลงพฤติกรรมนี้มาใช้เช่นเดียวกัน เราจะไม่ได้รับอนุญาตให้ปรับปรุงแก้ไขมูลค่าใดๆ ของชิ้นงานผ่านตัวแปรอ้างอิงปรกติตัวนี้

การอ้างอิงแบบที่แก้ไขค่าได้ (Mutable References)

เราสามารถปรับปรุงแก้ไขข้อผิดพลาดของโค้ดในรายการที่ 4-6 เพื่ออนุญาตให้เราเขียนคำสั่งปรับแก้ค่าตัวแปรที่เรายืมใช้ข้อมูลได้สำเร็จผ่านการเปลี่ยนรูปมาใช้งาน การอ้างอิงแบบที่แก้ไขค่าได้ (mutable reference) ดังนี้:

fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

อันดับแรก เราสั่งปรับแก้ตัวแปร s ให้เป็นแบบ mut เสียก่อน จากนั้นเราเขียนสร้างการอ้างอิงแบบแก้ไขค่าได้ระบุเป็น &mut s ตอนสั่งป้อนเข้าสู่ฟังก์ชัน change และปรับแก้รายละเอียดในลายเซ็นข้อกำหนดฟังก์ชันให้ยินยอมพร้อมใจที่จะรับอาร์กิวเมนต์เป็นประเภทการอ้างอิงแบบแก้ไขค่าได้อิงตามรูปแบบ some_string: &mut String ขั้นตอนเหล่านี้ช่วยบ่งบอกเจตนาให้แก่ผู้ดูแลโค้ดอย่างชัดเจนว่า ฟังก์ชัน change จะมีกระบวนการเข้าไปปรับแก้ค่าข้อมูลของตัวแปรที่ยืมมาใช้งาน

ข้อกำหนดที่สำคัญมากของตัวแปรอ้างอิงแบบแก้ไขค่าได้คือ: หากคุณได้ทำสร้างตัวแปรอ้างอิงแบบแก้ไขค่าได้ไปชี้ยังข้อมูลชิ้นหนึ่งแล้ว คุณจะไม่สามารถสร้างตัวแปรอ้างอิงอื่นใดเข้ามาร่วมอ้างอิงข้อมูลชิ้นนั้นไปพร้อมๆ กันได้อีก ตัวอย่างโค้ดด้านล่างนี้ที่พยายามสั่งสร้างตัวแปรอ้างอิงแบบแก้ไขค่าได้พร้อมกัน 2 ตัวเพื่อชี้หา s จะพบคอมไพเลอร์แจ้งรายงานข้อผิดพลาด:

fn main() {
    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s;

    println!("{r1}, {r2}");
}

นี่คือข้อความแสดงข้อผิดพลาด:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> src/main.rs:5:14
  |
4 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here
5 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
6 |
7 |     println!("{r1}, {r2}");
  |                -- first borrow later used here

For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

ข้อความแจ้งรายงานข้อผิดพลาดชี้ว่าประโยคโค้ดนี้ไม่สามารถประมวลผลได้เนื่องจากเรากระทำละเมิดกฎการยืมใช้ข้อมูล s ในรูปแบบแก้ไขค่าได้ (mutable borrow) ซ้ำซ้อนมากกว่าหนึ่งรายไปพร้อมๆ กัน โดยสิทธิ์การยืมใช้แก้ไขแบบแรกประกาศไว้ในตัวแปร r1 ซึ่งจะมีผลครอบคลุมขอบเขตงานต่อเนื่องไปจนกว่าจะสั่งรันประมวลผลใช้งานบรรทัดมาโคร println! แต่ทว่าในช่วงระยะเวลาระหว่างจัดตั้งตัวแปรอ้างอิงนั้นกับการหยิบใช้จริง เรากลับไปพยายามแทรกสร้างตัวแปรอ้างอิงแก้ไขตัวใหม่ขึ้นมาจัดเก็บไว้ใน r2 เพื่อขอยืมใช้งานเนื้อข้อมูลตัวเดียวกับของ r1

ข้อกำหนดการจำกัดการเรียกใช้งานตัวแปรอ้างอิงแบบแก้ไขค่าได้หลายรายพร้อมๆ กันนี้ แม้จะดูเป็นอุปสรรคต่อการปรับข้อมูลตามใจชอบ แต่มันเป็นการควบคุมดูแลเพื่อความเป็นระเบียบและปลอดภัยขั้นสูงสุด เป็นกฎสำคัญข้อหนึ่งที่ชาว Rustacean มือใหม่มักจะรู้สึกขัดใจในช่วงแรกเริ่มเนื่องจากในระบบภาษาเขียนโปรแกรมคอมพิวเตอร์อื่นส่วนใหญ่จะยินยอมให้คุณสลับไปอัปเดตข้อมูลได้ทุกเมื่อที่ปรารถนา ประโยชน์สำคัญของการบังคับตั้งด่านกรองข้อจำกัดนี้คือ ภาษา Rust สามารถสกัดกั้นดักจับปัญหาภาวะช่วงชิงข้อมูลอัปเดต (data races) ได้สำเร็จตั้งแต่ขั้นตอนคอมไพล์ โดยปัญหา ภาวะช่วงชิงข้อมูล (data race) จะมีทิศทางสอดคล้องทำนองเดียวกับภาวะแย่งชิงทรัพยากร (race condition) และจะเกิดขึ้นเมื่อมีลักษณะเงื่อนไขพฤติกรรม 3 ประการนี้อุบัติขึ้นพร้อมกัน:

  • มีตัวชี้พิกัด pointer ตั้งแต่ 2 ตัวขึ้นไปพยายามตรงเข้าถึงชิ้นข้อมูลตัวเดียวกันในขณะเวลาเดียวกัน
  • มีตัวชี้พิกัด pointer อย่างน้อย 1 รายกำลังดำเนินการเขียนข้อมูลทับ (write) ลงไปในชิ้นข้อมูลนั้น
  • ปราศจากการระบุใช้กลไกพิเศษเพื่อคอยเข้ามาช่วยจัดระเบียบจัดลำดับเวลาในการเข้าถึงข้อมูล (synchronization)

ภาวะช่วงชิงข้อมูลมักนำไปสู่ผลลัพธ์พฤติกรรมการทำงานโปรแกรมที่ไม่พึงประสงค์ระบุค่าแน่ชัดไม่ได้ (undefined behavior) และบ่อยครั้งเป็นเรื่องยากที่จะวิเคราะห์และหาทางแก้ไขปัญหาเมื่อต้องสืบหาเหตุการณ์ในตอนโปรแกรมรัน (runtime) Rust ปิดความเสี่ยงไฟนี้ถาวรโดยการปฏิเสธการคอมไพล์โค้ดใดๆ ก็ตามที่ตรวจพบแนวโน้มเงื่อนไขที่จะเกิด data races!

และเฉกเช่นเดิม เราสามารถเลือกใส่ปีกกาครอบย่อเพื่อสร้างขอบเขตงานย่อยใหม่ขึ้นมา ซึ่งช่วยให้เราตั้งตัวแปรอ้างอิงแบบแก้ไขค่าได้หลายตัวยืมใช้สิทธิ์ข้อมูลชิ้นเดียวกันได้ ขอเพียงแต่อย่าให้ช่วงระยะขอบเขตงานของพวกมันมีลักษณะ ทับซ้อนทับเวลาเดียวกัน (simultaneous) เท่านั้นก็เพียงพอ:

fn main() {
    let mut s = String::from("hello");

    {
        let r1 = &mut s;
    } // r1 goes out of scope here, so we can make a new reference with no problems.

    let r2 = &mut s;
}

ภาษา Rust ยังบังคับใช้กฎที่คล้ายคลึงกันสำหรับการเขียนใช้งานผสมผสานระหว่างตัวแปรอ้างอิงแบบแก้ไขค่าได้ (mutable) และแก้ไขค่าไม่ได้ (immutable) โดยโค้ดตัวอย่างด้านล่างนี้จะเกิดข้อผิดพลาดรันไม่ผ่านคอมไพล์:

fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    let r3 = &mut s; // BIG PROBLEM

    println!("{r1}, {r2}, and {r3}");
}

นี่คือข้อความแสดงข้อผิดพลาด:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:14
  |
4 |     let r1 = &s; // no problem
  |              -- immutable borrow occurs here
5 |     let r2 = &s; // no problem
6 |     let r3 = &mut s; // BIG PROBLEM
  |              ^^^^^^ mutable borrow occurs here
7 |
8 |     println!("{r1}, {r2}, and {r3}");
  |                -- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

เย้! สรุปใจความคือ เรา ยัง ไม่สามารถสร้างตัวแปรอ้างอิงแบบแก้ไขค่าได้ (mutable reference) ขึ้นมาใช้งานได้เลยในช่วงเวลาที่เรายังมีตัวแปรอ้างอิงแก้ไขค่าไม่ได้ (immutable reference) โยงคาถือครองสิทธิ์ข้อมูลชิ้นเดียวกันนั้นอยู่

เนื่องจากผู้ใช้งานตัวแปรอ้างอิงแบบแก้ไขค่าไม่ได้ ย่อมคาดหวังว่าข้อมูลเป้าหมายจะต้องคงตัวที่ค่าเดิมตลอดโดยไม่มีทางผันแปรหรือถูกลักลอบปรับปรุงค่าไปกะทันหันในระหว่างประมวลผลงานของตน! แต่อย่างไรก็ตาม การสวมประกาศตัวแปรอ้างอิงแบบแก้ไขไม่ได้พร้อมกันหลายๆ ราย (multiple immutable references) จะได้รับการยินยอมให้ผ่านตามกติกา เนื่องจากทุกๆ คนทำเพียงแค่อ่านเนื้อความข้อมูลเฉยๆ (reading) จึงไม่มีผู้ใดส่งผลเสียหรือทำลายสิทธิ์เข้าถึงการอ่านข้อมูลของนักพัฒนาคนอื่น

พึงสังเกตว่าขอบเขตการทำงานของตัวแปรอ้างอิงจะเริ่มต้นนับจากจุดบรรทัดที่มันประกาศตัวขึ้น และคงประสิทธิภาพใช้งานไปเรื่อยๆ จนถึงจังหวะเวลาแถวคำสั่งสุดท้ายที่มีการหยิบใช้งานตัวแปรอ้างอิงตัวนั้นจริงๆ ยกตัวอย่างเช่น ซอร์สโค้ดนี้จะผ่านขั้นตอนคอมไพล์ได้ฉลุย เนื่องจากลำดับการเรียกใช้งานขั้นสุดท้ายของตัวแปรอ้างอิงแบบแก้ไขไม่ได้ประมวลผลจบลงแล้วที่บรรทัดคำสั่ง println! ก่อนหน้าจะเริ่มมีคำสั่งจัดสร้างประกาศตัวแปรอ้างอิงแบบแก้ไขค่าได้พ่วงเข้ามาด้านล่าง:

fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    println!("{r1} and {r2}");
    // Variables r1 and r2 will not be used after this point.

    let r3 = &mut s; // no problem
    println!("{r3}");
}

ขอบเขตการทำงานของตัวแปรอ้างอิงแก้ไขไม่ได้ r1 และ r2 จะยุติอายุขัยลงหลังจากจบบรรทัดทำงาน println! ที่พวกมันถูกหยิบใช้งานเป็นรอบสุดท้าย ซึ่งถือเป็นพิกัดเวลาก่อนที่จะประมวลผลสร้างตัวแปรอ้างอิงแก้ไขค่าได้ r3 ขึ้นมา ขอบเขตงานของพวกมันจึงไม่ได้คาบเกี่ยวซ้อนทับกัน ตัวคอมไพเลอร์ Rust สามารถวิเคราะห์ได้ชัดแจ้งว่าตัวแปรอ้างอิงเหล่านั้นหมดสิ้นหน้าที่การใช้งานไปเรียบร้อยแล้วตั้งแต่ก่อนจะก้าวไปถึงขอบเขตวงเล็บปีกกาปิด

ถึงแม้ว่าสภาวะข้อผิดพลาดเกี่ยวกับการยืมใช้ข้อมูล (borrowing errors) นี้จะสร้างความขัดใจและหงุดหงิดใจให้คุณอยู่บ้างในบางครั้งคราว แต่โปรดตระหนักว่านี่คือขั้นตอนคุ้มครองความปลอดภัยที่คอมไพเลอร์ Rust ช่วยมาคอยสะกิดบอกกล่าวถึงสภาวะแนวโน้มจะเกิดบั๊กและรอยรั่วความปลอดภัยล่วงหน้าให้เราทราบทันที (ในขั้นตอนคอมไพล์ แทนการปล่อยไปเสียหายและแครชในขณะรันโปรแกรม) และยังชี้พิกัดปัญหานั้นอย่างเที่ยงตรง ช่วยลดเวลาที่คุณต้องไปเหนื่อยสืบหาเหตุการณ์ในภายหลังว่าเหตุใดข้อมูลประมวลผลถึงแสดงผลพฤติกรรมรายงานค่าผิดพลาด

ปัญหาการอ้างอิงลอยเคว้งชี้หาพื้นที่ว่างเปล่า (Dangling References)

ในระบบภาษาโปรแกรมคอมพิวเตอร์ที่อนุญาตให้นักพัฒนาควบคุมตัวชี้พิกัด pointer ได้โดยตรง มักมีโอกาสเสี่ยงที่จะเผลอสร้างความพลั้งพลาดเกิดปัญหา ตัวชี้ลอยเคว้ง (dangling pointer) ขึ้นได้โดยง่าย ซึ่งก็คือสภาวะที่ตัวชี้ pointer กำลังอ้างอิงชี้ไปยังพิกัดหน่วยความจำเดิมที่ถูกคำสั่ง deallocate คืนพื้นที่ให้ตัวกลางระบบไปเรียบร้อยแล้วและพื้นที่ตำแหน่งนั้นถูกนำไปยกให้โปรแกรมอื่นใช้งานไปแล้ว แต่ตัว pointer นี้ยังเก็บจดจำข้อมูลเบาะแสที่อยู่พิกัดเก่าโยงชี้ไว้อยู่ แต่ทว่าใน Rust ตัวคอมไพเลอร์จะรับประกันความปลอดภัยให้มั่นใจได้เลยว่า ตัวอ้างอิงของคุณจะไม่มีวันกลายเป็นชนิดอ้างอิงลอยเคว้งนี้เด็ดขาด: หากคุณได้มีตัวแปรอ้างอิงชี้ไปยังข้อมูลชุดหนึ่ง คอมไพเลอร์ Rust จะคุมเข้มกฎให้ข้อมูลชุดเป้าหมายนั้นไม่หลุดออกนอกขอบเขตงานไปก่อนที่ตัวอ้างอิงชี้ตัวนั้นจะหลุดขอบเขตตามไป

ลองมาจงใจเขียนสร้างสภาวะการอ้างอิงลอยเคว้งขึ้นมาเพื่อร่วมศึกษาว่า Rust มีทิศทางดักจับปัญหานี้ด้วยข้อผิดพลาดในขั้นตอนคอมไพล์อย่างไร:

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");

    &s
}

นี่คือข้อความแสดงข้อผิดพลาด:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
 --> src/main.rs:5:16
  |
5 | fn dangle() -> &String {
  |                ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
  |
5 | fn dangle() -> &'static String {
  |                 +++++++
help: instead, you are more likely to want to return an owned value
  |
5 - fn dangle() -> &String {
5 + fn dangle() -> String {
  |

For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

ข้อความแจ้งเตือนข้อผิดพลาดนี้จะมีการเอ่ยอ้างอิงเชื่อมโยงไปถึงฟีเจอร์เด่นเรื่องหนึ่งที่เรายังไม่ได้หยิบยกมาศึกษา นั่นคือเรื่องอายุขัยช่วงเวลาครอบครอง (lifetimes) ซึ่งเราจะมาลุยศึกษากันอย่างเจาะลึกในบทที่ 10 แต่หากเราข้ามข้อมูลรายละเอียดเรื่อง lifetimes ส่วนนั้นไปก่อน ตัวใจความสำคัญของข้อผิดพลาดจะชี้บอกสาเหตุที่แท้จริงที่ทำให้โปรแกรมนี้มีปัญหาไว้ตรงประโยคว่า:

this function's return type contains a borrowed value, but there is no value
for it to be borrowed from

ลองมาไล่สเต็ปวิเคราะห์เนื้อโค้ดฟังก์ชัน dangle ทีละแถวคำสั่งอย่างละเอียด:

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String { // dangle returns a reference to a String

    let s = String::from("hello"); // s is a new String

    &s // we return a reference to the String, s
} // Here, s goes out of scope and is dropped, so its memory goes away.
  // Danger!

เนื่องจากตัวแปร s ได้รับการประกาศจัดสร้างไว้ภายในขอบเขตงานเฉพาะถิ่นของฟังก์ชัน dangle ดังนั้นเมื่อกระบวนการทำงานของ dangle เสร็จสิ้นลง ตัวแปร s จะถูกฟังก์ชัน drop ทำลาย deallocate เคลียร์พื้นที่คืนทันที แต่ในตอนจบเรากลับพยายามจะเขียนคำสั่งโยนส่งการอ้างอิง &s ขากลับโยนออกมาภายนอก ซึ่งหมายความว่าตัวอ้างอิงขากลับนี้จะชี้เคว้งลอยระบุหาช่องพิกัดหน่วยความจำ String ที่โดนทลายล้างสลายไปแล้ว พฤติกรรมนี้ก่ออันตรายร้ายแรงแก่ระบบคอมพิวเตอร์อย่างมาก Rust จึงไม่ยอมให้ผ่านกติกาเด็ดขาด

หนทางแก้ไขปัญหาข้อจำกัดนี้ก็สามารถทำได้ง่ายๆ โดยเปลี่ยนมาส่งคืนข้อมูลอ็อบเจกต์เต็มรูปแบบชนิด String ย้อนกลับออกไปตรงๆ แทนดังนี้:

fn main() {
    let string = no_dangle();
}

fn no_dangle() -> String {
    let s = String::from("hello");

    s
}

วิธีการแก้ไขนี้จะทำงานได้อย่างปลอดภัยและคอมไพล์ผ่านฉลุย สิทธิ์การครอบครองหน่วยความจำจะได้รับการโอนย้ายย้ายสิทธิ์ออกไปภายนอกโปรแกรม และไม่มีชิ้นส่วนข้อมูลใดถูกล้างทำลายทิ้ง

กฎเหล็กอ้างอิงการใช้งานตัวแปรอ้างอิง (The Rules of References)

มาสรุปกติกาข้อตกลงเกี่ยวกับการอ้างอิงตัวแปรที่เราศึกษากันไปดังนี้:

  • ณ ช่วงเวลาขณะใดขณะหนึ่ง คุณจะสามารถเลือกจัดตั้งใช้งานได้ อย่างใดอย่างหนึ่ง ระหว่าง: การอ้างอิงแบบที่แก้ไขค่าได้ (mutable reference) เพียงรายเดียว หรือ การอ้างอิงแบบที่แก้ไขค่าไม่ได้ (immutable references) ปริมาณกี่ตัวก็ได้
  • ตัวแปรอ้างอิง (references) จะต้องชี้ระบุไปยังพิกัดข้อมูลที่ถูกต้องและมีตัวตนอยู่จริงเสมอ

และในหัวข้อต่อไป เราจะมาทำความเข้าใจเกี่ยวกับตัวแปรอ้างอิงอีกประเภทหนึ่งที่ใช้งานสะดวกและน่าสนใจมาก นั่นคือ: ส่วนแบ่งข้อมูล (slices) ครับ