Reference และ Borrowing
ปัญหากับโค้ด tuple ใน Listing 4-5 คือเราต้อง return String ให้ฟังก์ชัน
ที่เรียก เพื่อให้เรายังใช้ String ได้หลังการเรียก calculate_length
เพราะ String ถูก move เข้า calculate_length แทนนั้น เราให้ reference
ของค่า String ได้ reference คล้าย pointer ตรงที่มันเป็น address ที่เรา
ตามไปเข้าถึงข้อมูลที่เก็บที่ address นั้นได้ ข้อมูลนั้นเป็นเจ้าของโดยตัว
แปรอื่น ต่างจาก pointer reference รับประกันว่าจะชี้ไปยังค่าที่ valid ของ
type หนึ่งตลอด life ของ reference นั้น
นี่คือวิธีที่คุณประกาศและใช้ฟังก์ชัน calculate_length ที่มี reference ของ
object เป็น parameter แทนการรับ ownership ของค่า:
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()
}
ขั้นแรก สังเกตว่าโค้ด tuple ทั้งหมดในการประกาศตัวแปรและ return value ของ
ฟังก์ชันหายไป ขั้นที่สอง สังเกตว่าเราส่ง &s1 เข้า calculate_length
และในการประกาศ เรารับ &String แทน String ampersand เหล่านี้แทน
reference และให้คุณอ้างถึงค่าโดยไม่รับ ownership ของมัน Figure 4-6 แสดง
แนวคิดนี้
Figure 4-6: ไดอะแกรมของ &String s ที่ชี้ไปยัง
String s1
หมายเหตุ: ตรงข้ามของการอ้างอิงโดยใช้
&คือ dereference ซึ่งทำได้ด้วย dereference operator*เราจะเห็นการใช้ dereference operator บางตัวใน บทที่ 8 และพูดถึงรายละเอียดของ dereference ในบทที่ 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()
}
syntax &s1 ให้เราสร้าง reference ที่ อ้างถึง ค่าของ s1 แต่ไม่ owner
มัน เพราะ reference ไม่ owner มัน ค่าที่มันชี้ไปจะไม่ถูก drop เมื่อ
reference หยุดใช้
ในทำนองเดียวกัน signature ของฟังก์ชันใช้ & เพื่อบ่งบอกว่า type ของ
parameter s เป็น reference มาเพิ่ม annotation อธิบายบ้าง:
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.
scope ที่ตัวแปร s valid เหมือนกับ scope ของ parameter ฟังก์ชันใด ๆ แต่
ค่าที่ reference ชี้ไปไม่ถูก drop เมื่อ s หยุดใช้ เพราะ s ไม่มี
ownership เมื่อฟังก์ชันมี reference เป็น parameter แทนค่าจริง เราไม่ต้อง
return ค่าเพื่อคืน ownership เพราะเราไม่เคยมี ownership
เราเรียกการสร้าง reference ว่า borrowing เหมือนในชีวิตจริง ถ้าคนเป็น เจ้าของบางอย่าง คุณยืมจากเขาได้ เมื่อคุณใช้เสร็จ คุณต้องคืน คุณไม่ใช่ เจ้าของ
แล้วเกิดอะไรขึ้นถ้าเราพยายามแก้สิ่งที่กำลังยืมอยู่? ลองโค้ดใน Listing 4-6 Spoiler — มันไม่ทำงาน!
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
นี่คือ error:
$ 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 the data it refers to 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 โดย default reference ก็เช่นกัน เราไม่ได้ รับอนุญาตให้แก้สิ่งที่เรามี reference ของมัน
Mutable Reference
เราแก้โค้ดจาก Listing 4-6 ให้เราแก้ค่าที่ borrow ได้ ด้วยการแก้เล็กน้อยที่ ใช้ 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 จากนั้นเราสร้าง mutable reference ด้วย
&mut s ตรงที่เราเรียกฟังก์ชัน change และ update signature ของฟังก์ชัน
ให้รับ mutable reference ด้วย some_string: &mut String นี่ทำให้ชัดเจนมาก
ว่าฟังก์ชัน change จะ mutate ค่าที่มัน borrow
Mutable reference มีข้อจำกัดใหญ่ — ถ้าคุณมี mutable reference ของค่า คุณ
จะมี reference อื่นของค่านั้นไม่ได้ โค้ดนี้ที่พยายามสร้าง mutable
reference สองตัวของ s จะล้มเหลว:
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{r1}, {r2}");
}
นี่คือ error:
$ 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
error นี้บอกว่าโค้ดนี้ invalid เพราะเรา borrow s เป็น mutable มากกว่า
หนึ่งครั้งในเวลาเดียวกันไม่ได้ mutable borrow แรกอยู่ใน r1 และต้องอยู่
จนกว่าจะใช้ใน println! แต่ระหว่างการสร้าง mutable reference นั้นและการ
ใช้มัน เราพยายามสร้าง mutable reference อีกตัวใน r2 ที่ borrow ข้อมูล
เดียวกับ r1
ข้อจำกัดที่ป้องกัน mutable reference หลายตัวของข้อมูลเดียวกันในเวลาเดียวกัน อนุญาตให้ mutate ได้ แต่ในลักษณะที่ควบคุมมาก เป็นสิ่งที่ Rustacean ใหม่ ๆ ต่อสู้ด้วย เพราะภาษาส่วนใหญ่ให้คุณ mutate เมื่อไหร่ก็ได้ ประโยชน์ของการมี ข้อจำกัดนี้คือ Rust ป้องกัน data race ตอน compile time ได้ data race คล้ายกับ race condition และเกิดขึ้นเมื่อสามพฤติกรรมนี้เกิด:
- pointer ตั้งแต่สองตัวขึ้นไปเข้าถึงข้อมูลเดียวกันในเวลาเดียวกัน
- อย่างน้อยหนึ่ง pointer กำลังถูกใช้เขียนข้อมูล
- ไม่มีกลไกที่ถูกใช้ synchronize การเข้าถึงข้อมูล
Data race ทำให้เกิด undefined behavior และวินิจฉัยและแก้ยากเมื่อพยายามตาม หาตอน runtime — Rust ป้องกันปัญหานี้โดยปฏิเสธที่จะ compile โค้ดที่มี data race!
เช่นเคย เราใช้ curly bracket สร้าง scope ใหม่ได้ ให้มี mutable reference หลายตัว แค่ไม่ใช่ พร้อมกัน:
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 reference โค้ด นี้ทำให้เกิด error:
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}");
}
นี่คือ error:
$ 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 หนึ่งของ ค่าเดียวกัน
ผู้ใช้ immutable reference ไม่ได้คาดว่าค่าจะเปลี่ยนใต้พวกเขาทันที! อย่างไรก็ ตาม immutable reference หลายตัวอนุญาต เพราะไม่มีใครที่แค่อ่านข้อมูลที่มี ความสามารถส่งผลต่อการอ่านข้อมูลของคนอื่น
หมายเหตุว่า scope ของ reference เริ่มจากตรงที่ถูกแนะนำ และต่อเนื่องไปจน
ครั้งสุดท้ายที่ reference นั้นถูกใช้ เช่น โค้ดนี้จะ compile ผ่าน เพราะการ
ใช้ครั้งสุดท้ายของ immutable reference อยู่ใน println! ก่อน mutable
reference ถูกแนะนำ:
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}");
}
scope ของ immutable reference r1 และ r2 จบหลัง println! ที่พวกมัน
ถูกใช้ครั้งสุดท้าย ซึ่งก่อน mutable reference r3 ถูกสร้าง scope เหล่านี้
ไม่ทับซ้อนกัน โค้ดนี้จึงอนุญาต — compiler บอกได้ว่า reference ไม่ถูกใช้
แล้วในจุดก่อนท้าย scope
แม้ error เรื่อง borrowing อาจทำให้หงุดหงิดบางครั้ง จำไว้ว่ามันคือ Rust compiler ชี้ bug ที่อาจเกิดแต่เนิ่น ๆ (ตอน compile time แทน runtime) และ แสดงให้คุณเห็นตำแหน่งปัญหาเป๊ะ ๆ จากนั้น คุณไม่ต้องตามว่าทำไมข้อมูลไม่ใช่ สิ่งที่คุณคิด
Dangling Reference
ในภาษาที่มี pointer ง่ายที่จะสร้าง dangling pointer — pointer ที่อ้างถึง ตำแหน่งในหน่วยความจำที่อาจถูกให้คนอื่น — โดย free หน่วยความจำขณะที่ยัง รักษา pointer ของหน่วยความจำนั้น ใน Rust ตรงข้าม compiler รับประกันว่า reference จะไม่เป็น dangling reference — ถ้าคุณมี reference ของข้อมูล compiler จะรับประกันว่าข้อมูลจะไม่ออกจาก scope ก่อน reference ของข้อมูล
ลองพยายามสร้าง dangling reference เพื่อดูว่า Rust ป้องกันด้วย compile-time error อย่างไร:
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
นี่คือ error:
$ 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
Error message นี้อ้างถึงฟีเจอร์ที่เรายังไม่ครอบคลุม — lifetime เราจะพูดถึง lifetime ในรายละเอียดในบทที่ 10 แต่ถ้าคุณละเลยส่วนเกี่ยวกับ lifetime ข้อ ความนี้มีกุญแจว่าทำไมโค้ดนี้เป็นปัญหา:
this function's return type contains a borrowed value, but there is no value
for it to be borrowed from
มาดูใกล้ ๆ ว่าเกิดอะไรขึ้นในแต่ละ stage ของโค้ด 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 จะถูก
deallocate แต่เราพยายาม return reference ของมัน นั่นหมายความว่า reference
นี้จะชี้ไปยัง String ที่ invalid ไม่ดีเลย! Rust จะไม่ให้เราทำสิ่งนี้
วิธีแก้ที่นี่คือ return String ตรง ๆ:
fn main() {
let string = no_dangle();
}
fn no_dangle() -> String {
let s = String::from("hello");
s
}
นี่ทำงานได้โดยไม่มีปัญหา Ownership ถูก move ออก และไม่มีอะไรถูก deallocate
กฎของ Reference
มาทบทวนสิ่งที่เราพูดถึงเกี่ยวกับ reference:
- ในเวลาใดก็ตาม คุณมีได้ หนึ่ง mutable reference หรือ immutable reference จำนวนเท่าไรก็ได้
- Reference ต้อง valid เสมอ
ต่อไป เราจะดู reference แบบอื่น — slice