ข้ามไปยังเนื้อหา

References และ Borrowing

การ 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 ไม่ได้เป็นเจ้าของค่าที่มันชี้ไป ค่านั้นจึง ไม่ถูก 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 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
}

เมื่อ 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
}

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]
การเลือก parameter type สำหรับ text
สถานการณ์ใช้เพราะอะไร
ฟังก์ชันแค่อ่าน 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 เสมอ

การรับ reference เป็น parameter นั้นตรงไปตรงมา — คอมไพเลอร์รู้อยู่แล้วว่า reference จะไม่มีชีวิตอยู่นานกว่า input ของมัน การ return reference ต่างหากที่น่าสนใจ: คอมไพเลอร์ต้องรู้ว่า reference ที่ return ยืมมาจาก input ตัวไหน เพราะแต่ละจุดที่เรียกอาจส่ง argument ที่มีอายุต่างกัน

// @expect-error E0106
fn 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);
}

Reference คือคำสัญญา ไม่ใช่ pointer ที่มี syntax เพิ่ม: &T บอกว่า “ขอดูค่านี้หน่อย แต่ฉันจะไม่มีชีวิตอยู่นานกว่ามัน” คอมไพเลอร์ track ทุก reference ว่ามันชี้ไปยังค่าที่เป็นเจ้าของตัวไหนเป๊ะ ๆ และปฏิเสธ path ใด ๆ ที่ reference อาจถูกอ่านได้หลังจากเจ้าของนั้นหายไปแล้ว การ return reference ก็แค่ทำให้คำสัญญานั้นชัดเจนขึ้นใน function signature — lifetime parameter บอกว่าค่าที่ return สืบทอดคำสัญญาของใคร

การ borrow เลี่ยง allocation กับการ copy ได้ — &str/&String แทบไม่มีต้นทุนอะไรนอกจาก pointer กับความยาว ราคาที่ต้องจ่ายคือ lifetime ของค่าที่ borrow มากลายเป็นส่วนหนึ่งของ contract สาธารณะของฟังก์ชันคุณ: บางครั้งคอมไพเลอร์ไม่สามารถแสดงสิ่งที่คุณต้องการได้ (เช่น ฟังก์ชันที่บางทีต้อง return reference บางทีต้องสร้าง text ใหม่) และทางแก้ที่ตรงไปตรงมาคือ return String ที่เป็นเจ้าของแทนที่จะฝืนสู้กับ borrow checker ด้วย lifetime annotation การรู้ว่าเมื่อไหร่ควรหยุด borrow แล้วเริ่มเป็นเจ้าของคือทักษะในตัวมันเอง — ตารางตัดสินใจข้างบนคือทางลัดสำหรับ parameter ส่วน return type มักต้องใช้วิจารณญาณแบบเดียวกัน

  • “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 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 แบบนี้

สัญลักษณ์ & ที่วางไว้หน้า type ใน function parameter ทำอะไร?
เกิดอะไรขึ้นกับ String เมื่อฟังก์ชันที่รับ &String return กลับมา?
shared reference (&T) ไปยังค่าเดียวกันสามารถมีอยู่พร้อมกันได้กี่ตัว?
ทำไม function parameter ควรเลือก &str มากกว่า &String?
โค้ดนี้ compile ไหม?
fn longest(a: &str, b: &str) -> &str {
    if a.len() > b.len() { a } else { b }
}
โปรแกรมนี้ print อะไร?
fn greet(name: &str) {
    println!("Hello, {}!", name);
}
fn main() {
    let owned = String::from("Bob");
    greet(&owned);
    greet("Charlie");
}