การอ้างอิงและการยืมใช้ข้อมูล (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 แสดงให้เห็นถึงแนวคิดพฤติกรรมนี้:
รูปภาพที่ 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) ครับ