References และ Borrowing
Borrow แทนที่จะ Move
หัวข้อที่มีชื่อว่า “Borrow แทนที่จะ Move”การ move ค่าเข้าไปในฟังก์ชันมักไม่จำเป็น — คุณแค่ต้องการให้ฟังก์ชันอ่านค่า ไม่ได้ต้องการให้มันเป็นเจ้าของ Rust แก้ปัญหานี้ด้วย references reference ช่วยให้คุณส่งค่าเข้าไปในฟังก์ชันโดยไม่ต้องสละ ownership
syntax &T แปลว่า “shared reference ไปยังค่าประเภท T” การสร้าง reference เรียกว่า borrowing
fn calculate_length(s: &String) -> usize { s.len()} // s goes out of scope, but since it does not own the data, nothing is dropped
fn main() { let s = String::from("hello"); let len = calculate_length(&s); // pass a reference, not the value println!("{} has {} characters", s, len); // s is still valid}syntax &s สร้าง reference ไปยัง s function signature s: &String ประกาศว่า parameter เป็น reference ไม่ใช่ String ที่เป็นเจ้าของ
Reference ไม่ได้เป็นเจ้าของ
หัวข้อที่มีชื่อว่า “Reference ไม่ได้เป็นเจ้าของ”เพราะ reference ไม่ได้เป็นเจ้าของค่าที่มันชี้ไป ค่านั้นจึง ไม่ถูก dropped เมื่อ reference ออกจาก scope ownership ยังอยู่กับตัวแปรเดิม
fn print_greeting(name: &String) { println!("Hello, {}!", name);} // reference dropped here — name itself is unaffected
fn main() { let name = String::from("Alice"); print_greeting(&name); print_greeting(&name); // can borrow again — we still own name println!("Still have: {}", name);}Shared References หลายตัว
หัวข้อที่มีชื่อว่า “Shared References หลายตัว”คุณสามารถมี shared reference (&T) กี่ตัวก็ได้ ไปยังค่าเดียวกันในเวลาเดียวกัน ตราบใดที่ไม่มีตัวไหนเป็น mutable reference shared reference เป็นแบบอ่านอย่างเดียว
fn main() { let s = String::from("shared"); let r1 = &s; let r2 = &s; let r3 = &s; println!("{} {} {}", r1, r2, r3); // all three are valid simultaneously}References กับ Function Signature
หัวข้อที่มีชื่อว่า “References กับ Function Signature”เมื่อ function parameter เป็น &String caller จะส่ง &my_string เมื่อ parameter เป็น &str (string slice) ฟังก์ชันจะยืดหยุ่นกว่า — มันรับได้ทั้ง &String และ string literal ควรเลือก &str มากกว่า &String สำหรับ function parameter เป็นกฎทั่วไป
fn greet(name: &str) { println!("Hello, {}!", name);}
fn main() { let owned = String::from("Bob"); greet(&owned); // &String coerces to &str greet("Charlie"); // string literal is already &str}&str vs &String vs String: ควรใช้แบบไหน?
หัวข้อที่มีชื่อว่า “&str vs &String vs String: ควรใช้แบบไหน?”Text type สามแบบนี้โผล่ในแทบทุก signature ของ Rust และการสับสนระหว่างพวกมันคือต้นตอที่พบบ่อยที่สุดของ .clone()/.to_string() ที่ไม่จำเป็นในโค้ดที่ AI เขียน การเลือกขึ้นอยู่กับคำถามเดียว: ฟังก์ชันนี้ต้องเป็นเจ้าของ text หลังจากนั้นไหม?
flowchart TD
Q{Does the function need to own the text afterward?}
Q -->|Yes - store it, move it into a struct, send it elsewhere| OWN[Parameter type: String]
Q -->|No - it only reads the text| REF[Parameter type: &str]
REF -->|Caller has a String| COERCE1[Pass &my_string - auto-coerces to &str]
REF -->|Caller has a literal| COERCE2[Pass the literal directly - it is already &str] | สถานการณ์ | ใช้ | เพราะอะไร |
|---|---|---|
| ฟังก์ชันแค่อ่าน text | &str | รับได้ทั้ง &String (ผ่าน coercion) และ string literal — input type ที่ยืดหยุ่นที่สุด |
| ฟังก์ชันต้องเก็บ, store, หรือย้าย text ไปที่อื่น | String | การเอา ownership ไปเลี่ยงไม่ได้ ถ้าค่าต้องมีชีวิตอยู่นานกว่าการเรียกครั้งนี้ |
| return มุมมองเข้าไปในข้อมูลที่ caller เป็นเจ้าของอยู่แล้ว | &str ที่ผูกกับ lifetime ของ input | ไม่มี allocation; slice ที่ return borrow มาจาก input |
| return text ใหม่ที่สร้างขึ้นภายในฟังก์ชัน | String | คุณ return reference ไปยังข้อมูลที่อยู่แค่ภายในฟังก์ชันไม่ได้ (ดูด้านล่าง) |
function parameter ที่เขียนเป็น &String | แทบไม่ใช่ตัวเลือกที่ถูกต้องเลย | มันยืดหยุ่นน้อยกว่า &str อย่างชัดเจนโดยไม่ได้ประโยชน์อะไร — เลือก &str เสมอ |
Returning References และ Lifetime Teaser
หัวข้อที่มีชื่อว่า “Returning References และ Lifetime Teaser”การรับ reference เป็น parameter นั้นตรงไปตรงมา — คอมไพเลอร์รู้อยู่แล้วว่า reference จะไม่มีชีวิตอยู่นานกว่า input ของมัน การ return reference ต่างหากที่น่าสนใจ: คอมไพเลอร์ต้องรู้ว่า reference ที่ return ยืมมาจาก input ตัวไหน เพราะแต่ละจุดที่เรียกอาจส่ง argument ที่มีอายุต่างกัน
// @expect-error E0106fn longest(a: &str, b: &str) -> &str { if a.len() > b.len() { a } else { b }}
fn main() { let s1 = String::from("hello"); let s2 = String::from("world!"); println!("{}", longest(&s1, &s2));}error[E0106]: missing lifetime specifier --> src/main.rs:1:33 |1 | fn longest(a: &str, b: &str) -> &str { | ---- ---- ^ expected named lifetime parameter | = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `a` or `b`help: consider introducing a named lifetime parameter |1 | fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { | ++++ ++ ++ ++ทางแก้คือสิ่งที่คอมไพเลอร์แนะนำตรง ๆ: lifetime parameter ชื่อ 'a ที่ผูกความ valid ของค่าที่ return เข้ากับทั้งสอง input พร้อมกัน
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { if a.len() > b.len() { a } else { b }}
fn main() { let s1 = String::from("hello"); let s2 = String::from("world!"); println!("longest: {}", longest(&s1, &s2));}<'a> ไม่ได้เปลี่ยนว่าอะไรมีชีวิตอยู่นานแค่ไหน — มันคือคำสัญญาที่ตรวจสอบตอน compile time ว่า &str ที่ return จะไม่มีชีวิตอยู่นานกว่า a หรือ b ตัวที่สั้นกว่า นี่เป็นแค่ teaser: ชุดกฎ elision เต็มรูปแบบ, 'static, และ lifetime บน struct มีบทของตัวเองใน Lifetimes
fn calculate_length(s: &String) -> usize { s.len()}
fn greet(name: &str) { println!("Hello, {}!", name);}
fn main() { let s = String::from("hello");
// Pass a reference — s is not moved let len = calculate_length(&s); println!("length of {} is {}", s, len);
// Multiple shared borrows are allowed let r1 = &s; let r2 = &s; println!("r1={} r2={}", r1, r2);
// &String coerces to &str let owned = String::from("Alice"); greet(&owned); // borrow, not move greet("Bob"); // string literal println!("owned is still: {}", owned);}Compiling…
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”Reference คือคำสัญญา ไม่ใช่ pointer ที่มี syntax เพิ่ม: &T บอกว่า “ขอดูค่านี้หน่อย แต่ฉันจะไม่มีชีวิตอยู่นานกว่ามัน” คอมไพเลอร์ track ทุก reference ว่ามันชี้ไปยังค่าที่เป็นเจ้าของตัวไหนเป๊ะ ๆ และปฏิเสธ path ใด ๆ ที่ reference อาจถูกอ่านได้หลังจากเจ้าของนั้นหายไปแล้ว การ return reference ก็แค่ทำให้คำสัญญานั้นชัดเจนขึ้นใน function signature — lifetime parameter บอกว่าค่าที่ return สืบทอดคำสัญญาของใคร
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”การ borrow เลี่ยง allocation กับการ copy ได้ — &str/&String แทบไม่มีต้นทุนอะไรนอกจาก pointer กับความยาว ราคาที่ต้องจ่ายคือ lifetime ของค่าที่ borrow มากลายเป็นส่วนหนึ่งของ contract สาธารณะของฟังก์ชันคุณ: บางครั้งคอมไพเลอร์ไม่สามารถแสดงสิ่งที่คุณต้องการได้ (เช่น ฟังก์ชันที่บางทีต้อง return reference บางทีต้องสร้าง text ใหม่) และทางแก้ที่ตรงไปตรงมาคือ return String ที่เป็นเจ้าของแทนที่จะฝืนสู้กับ borrow checker ด้วย lifetime annotation การรู้ว่าเมื่อไหร่ควรหยุด borrow แล้วเริ่มเป็นเจ้าของคือทักษะในตัวมันเอง — ตารางตัดสินใจข้างบนคือทางลัดสำหรับ parameter ส่วน return type มักต้องใช้วิจารณญาณแบบเดียวกัน
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “Reference ก็คือ raw pointer พื้นฐาน ดังนั้น
fn f() -> &Stringควรทำงานเหมือน return pointer ใน C” ใน C การ return pointer ไปยัง local คือบั๊กแบบเงียบ ๆ (dangling pointer, undefined behavior ตอนใช้) ใน Rust มัน compile ไม่ผ่านเลย — error แบบE0106/E0515มีไว้จับสิ่งนี้ตอน compile time โดยเฉพาะ เพราะคอมไพเลอร์ไม่มีที่ที่ valid จะบอกว่า reference นั้น “borrow มาจาก” ตรงไหน - “
&Stringกับ&strใช้แทนกันได้ ดังนั้นเขียนแบบไหนใน signature ก็ไม่ต่างกัน” พวกมัน coerce ให้ caller สะดวกก็จริง แต่ในฐานะ parameter type&strยืดหยุ่นกว่าอย่างชัดเจน — มันรับ literal ที่&Stringรับไม่ได้ — โดยไม่มีข้อเสียเลย แทบไม่มีเหตุผลที่จะเขียน&Stringใน signature เลย - “เพราะฉันมี shared reference ได้หลายตัวพร้อมกัน ฉันน่าจะเขียนผ่านตัวใดตัวหนึ่งได้ด้วย”
&Tเป็นแบบอ่านอย่างเดียวไม่ว่าจะมี reference กี่ตัว — กฎ exclusivity ที่ให้คุณ เขียน ผ่าน reference ได้ (&mut T) เป็นการรับประกันคนละแบบที่เข้มงวดกว่า จะพูดถึงในบทถัดไป Mutable Borrows
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”AI assistant มักเขียน function parameter เป็น &Vec<T> (reference ไปยัง vector ที่เป็นเจ้าของ ขยายขนาดได้) ทั้งที่ฟังก์ชันแค่ iterate หรือ index เท่านั้น — &[T] ซึ่งเป็น slice ทำงานแบบเดียวกันได้ พร้อมรับ array และ slice บางส่วนของ vector ได้ด้วย
🐛 หา bug ในโค้ดนี้
fn sum_all(v: &Vec<i32>) -> i32 { v.iter().sum()}
fn main() { let numbers = vec![1, 2, 3, 4]; println!("{}", sum_all(&numbers));}เฉลย
โค้ดนี้ compile ผ่านและรันได้ปกติ — บั๊กคือ smell เรื่องความยืดหยุ่น ไม่ใช่ compile error &Vec<i32> รับได้แค่ reference ไปยัง Vec<i32> จริง ๆ เท่านั้น — มันปฏิเสธ array ([i32; 4]) และส่วนที่ถูก slice ไปแล้วของ vector (&numbers[1..3]) โดยไม่ต้อง re-slice ใหม่อย่างชัดเจน การเปลี่ยน parameter เป็น &[i32] จะรับทั้งหมดนั้นอัตโนมัติผ่าน coercion ด้วย function body เดิมทุกประการ — บท Slices จะพูดถึงว่าทำไม &[T]/&str จึงเป็นภาพรวมของ &Vec<T>/&String แบบนี้