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

Lifetimes

Borrow checker ของ Rust ป้องกัน dangling reference — reference ที่ชี้ไปยังหน่วยความจำที่ถูก free ไปแล้ว ในกรณีง่ายๆ compiler สามารถหาเองได้ว่า reference นั้น valid นานแค่ไหน แต่เมื่อฟังก์ชันรับ reference สองตัวและ return ตัวใดตัวหนึ่ง compiler ต้องการความช่วยเหลือ: output ยืมมาจาก input ตัวไหน และนานแค่ไหน?

Lifetime annotation ตอบคำถามนั้น เขียนเป็น 'a (apostrophe ตามด้วยชื่อสั้นๆ) ภายใน angle bracket

นี่คือฟังก์ชันที่รับ string slice สองตัวและ return ตัวที่ยาวกว่า return value ยืมมาจากตัวใดตัวหนึ่ง — แต่ตัวไหน? ขึ้นอยู่กับค่า ณ runtime annotation 'a บอกว่า: “return value มีชีวิตอยู่ อย่างมากเท่ากับตัวที่สั้นกว่าในสอง input”

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}

ทั้งสาม reference แชร์ lifetime เดียวกันคือ 'a compiler ใช้สิ่งนี้เพื่อรับประกันว่า reference ที่ return จะไม่มีชีวิตอยู่นานกว่าข้อมูลที่มันชี้ไป

ถ้าไม่มี annotation นี้ compiler จะปฏิเสธฟังก์ชันนี้ เพราะไม่สามารถกำหนด return lifetime จาก input เพียงอย่างเดียวได้

Lifetime annotation มีอยู่เพื่อจับบั๊กประเภทนี้ ณ compile time นี่คือฟังก์ชันที่พยายาม return reference ไปยังค่าที่สร้างขึ้นภายในฟังก์ชันเอง:

// @expect-error E0106
fn dangle() -> &String {
let s = String::from("hello"); // s is created here
&s // we return a reference to s
} // s is dropped here -- the reference would dangle
fn main() {}

error แรก ที่คุณได้ไม่ได้เกี่ยวกับ dangling เลย — แต่เป็นเพราะ signature ไม่มี lifetime ให้ compiler ใช้งาน:

error[E0106]: missing lifetime specifier
--> src/main.rs:2:16
|
2 | 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`
|
2 | fn dangle() -> &'static String {
| +++++++
help: instead, you are more likely to want to return an owned value
|
2 - fn dangle() -> &String {
2 + fn dangle() -> String {
|

ถ้าคุณทำตามคำแนะนำแรกของ rustc ตามตัวอักษรโดยเพิ่ม <'a> อย่างชัดเจนโดยไม่เปลี่ยนอย่างอื่น error จะเปลี่ยนไป — ตอนนี้ compiler สามารถพูดถึงปัญหา dangling reference ที่แท้จริงได้แล้ว:

// @expect-error E0515
fn dangle<'a>() -> &'a String {
let s = String::from("hello"); // s is created here
&s // we return a reference to s
} // s is dropped here -- the reference would dangle
fn main() {}
error[E0515]: cannot return reference to local variable `s`
--> src/main.rs:4:5
|
4 | &s // we return a reference to s
| ^^ returns a reference to data owned by the current function

ไม่มี lifetime annotation ใดแก้ปัญหานี้ได้ — 'a จะตั้งชื่อได้แค่ lifetime ที่มีอยู่แล้วในฝั่งผู้เรียกเท่านั้น dangle ไม่มี input reference ให้ 'a ยืมมาจาก จึงไม่มีอะไรให้ตั้งชื่อได้อย่างถูกต้อง ทางแก้ที่แท้จริง ตามที่ rustc แนะนำในข้อที่สอง คือ return String แบบ owned แทนที่จะเป็น reference

Compiler พูดว่าอย่างไร: Borrow ที่มีชีวิตอยู่นานกว่าข้อมูลของมัน

หัวข้อที่มีชื่อว่า “Compiler พูดว่าอย่างไร: Borrow ที่มีชีวิตอยู่นานกว่าข้อมูลของมัน”

E0106/E0515 จับ return value ที่ dangling ปัญหาเดียวกันในหลักการเกิดขึ้นบ่อยพอ ๆ กันกับ local variable ที่ borrow ของมันถูกใช้หลังจาก variable เองหายไปแล้ว — จับได้ด้วย error คนละตัว คือ E0597:

// @expect-error E0597
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("{}", r);
}
error[E0597]: `x` does not live long enough
--> src/main.rs:5:13
|
4 | let x = 5;
| - binding `x` declared here
5 | r = &x;
| ^^ borrowed value does not live long enough
6 | }
| - `x` dropped here while still borrowed
7 | println!("{}", r);
| - borrow later used here

x ถูก drop ตอนจบ inner block แต่ r ถูกใช้หลังจากจุดนั้น — compiler จับ use-after-free ได้ก่อนที่โปรแกรมของคุณจะรันมันเสียอีก E0106, E0515, และ E0597 คือ diagnostic สามตัวที่ต่างกันสำหรับแนวคิดหลักเดียวกัน: reference ต้องไม่มีชีวิตอยู่นานกว่าค่าที่มันชี้ไปเลย ไม่ว่าค่านั้นจะหายไปเพราะ return จาก function หรือเพราะออกจาก block

ถ้า struct เก็บ reference คุณต้อง annotate lifetime เพื่อให้ compiler รู้ว่า struct ไม่สามารถมีชีวิตอยู่นานกว่าข้อมูลที่มัน borrow:

struct Excerpt<'a> {
text: &'a str,
}

นี่บอกว่า “Excerpt ไม่สามารถมีชีวิตอยู่นานกว่า string ที่มัน borrow มา” compiler บังคับใช้การรับประกันนี้โดยอัตโนมัติ Method บน struct ที่มี lifetime parameter ต้องระบุมันซ้ำในบรรทัด impl:

struct Excerpt<'a> {
text: &'a str,
}
impl<'a> Excerpt<'a> {
fn announce(&self) -> &str {
self.text
}
}

impl<'a> Excerpt<'a> ประกาศ 'a สำหรับ block นี้ แล้วใช้มันเพื่อบอกว่า “impl นี้สำหรับ Excerpt ที่ borrow ด้วย lifetime 'a บางตัว” — return type ของ announce ไม่ต้องระบุ 'a ซ้ำอย่างชัดเจนตรงนี้ เพราะ elision rule ข้อที่ 3 ซึ่งจะพูดถึงต่อไป

ในหลาย pattern ที่พบบ่อย Rust สามารถอนุมาน lifetime โดยอัตโนมัติด้วย elision rule สามข้อ ทำให้คุณไม่ต้องเขียน annotation ทุกครั้ง แต่ละกฎมีรูปร่างง่ายพอที่จะแสดงแยกกันได้:

กฎที่ 1 — reference parameter แต่ละตัวได้ lifetime ของตัวเองที่เป็นอิสระ นี่คือจุดเริ่มต้นที่อีกสองกฎต่อยอด เมื่อไม่มีอะไรต้องเชื่อมโยง parameter เข้าหากันหรือกับ return type กฎที่ 1 อย่างเดียวก็เพียงพอ:

fn describe_pair(a: &str, b: &str) -> String {
format!("{a} and {b}")
}

Compiler ปฏิบัติกับมันเป็น fn describe_pair<'a, 'b>(a: &'a str, b: &'b str) -> String — a และ b ได้ lifetime ที่เป็นอิสระ 'a และ 'b เพราะ return type เป็น String แบบ owned ที่ไม่ได้ borrow อะไรเลย จึงไม่มี output lifetime ที่ต้องตรงกับตัวไหนเลย

กฎที่ 2 — เมื่อมี input reference แค่ตัวเดียว lifetime ของมันจะถูกกำหนดให้ output reference ทุกตัว

fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}

Desugar เป็น fn first_word<'a>(s: &'a str) -> &'a str — มี reference parameter แค่ตัวเดียว ดังนั้น return value ที่ borrow มาได้มาจากตัวนั้นเท่านั้น

กฎที่ 3 — ถ้า parameter ตัวหนึ่งคือ &self หรือ &mut self lifetime ของมันจะถูกกำหนดให้ output reference ทุกตัว ไม่ว่าจะมี parameter อื่นกี่ตัวก็ตาม:

struct Wrapper { text: String }
impl Wrapper {
fn get(&self) -> &str {
&self.text
}
}

Desugar เป็น fn get<'a>(&'a self) -> &'a str — method นี้ borrow จาก self ดังนั้น &str ที่ return มาถือว่า borrow จาก self ด้วย

longest จากก่อนหน้านี้ต้องการ annotation ที่ชัดเจนพอดีเพราะไม่มีกฎทั้งสามข้อใช้ได้กับมันเลย: มี reference parameter สอง ตัว (กฎที่ 2 จึงใช้ไม่ได้ — ไม่ใช่ “แค่ตัวเดียว”) และไม่มีตัวไหนเป็น &self (กฎที่ 3 จึงใช้ไม่ได้เช่นกัน) เฉพาะเมื่อกฎทั้งสามข้อรวมกันกำหนด output lifetime ได้ครบถ้วนเท่านั้น Rust จึงให้คุณละ annotation ได้

flowchart TD
  Start["Function has reference parameters and/or a returned reference"]
  R1["Rule 1: give each input reference its own lifetime"]
  R2{"Exactly one input reference?"}
  R3{"Is one parameter '&self' or '&mut self'?"}
  Ambiguous["Output lifetime is ambiguous -- write it explicitly"]
  Resolved["Elision succeeds -- no annotation needed"]
  Start --> R1 --> R2
  R2 -->|Yes| Resolved
  R2 -->|No| R3
  R3 -->|Yes| Resolved
  R3 -->|No| Ambiguous
กฎ elision สามข้อในหนึ่งการตัดสินใจ

'static คือ lifetime พิเศษที่มีชื่อ หมายความว่า “valid ตลอดระยะเวลาของโปรแกรม” คุณเคยเห็นมันมาแล้วโดยไม่รู้ตัว: string literal ทุกตัวคือ &'static str เพราะ literal ถูกฝังตรงเข้าไปใน binary ที่ compile แล้วและไม่เคยถูก free

let s: &'static str = "I have a static lifetime.";

'static ปรากฏในสองสถานการณ์ที่ต่างกันมาก และควรแยกให้ออก:

  • Reference ไปยังข้อมูล static แท้จริง — string literal และค่าใน static item — เป็น 'static โดยไม่มีค่าใช้จ่ายใดๆ เพราะข้อมูลนั้นมีชีวิตอยู่ตลอดทั้งโปรแกรมจริงๆ
  • T: 'static เป็น trait bound (พบบ่อยบน generic parameter โดยเฉพาะกับ Box<dyn Trait + 'static> หรือตอน spawn thread) หมายความว่า “type นี้ไม่มี reference ที่ไม่ใช่ 'static อยู่ข้างใน” — owned type ส่วนใหญ่ (String, Vec<T>, i32) ผ่านเงื่อนไขนี้โดยอัตโนมัติ เพราะมันเป็นเจ้าของข้อมูลแทนที่จะยืมมัน มันไม่ได้หมายความว่า “มีชีวิตอยู่ตลอดไป” สำหรับ owned value — มันหมายความว่า value นั้นไม่ได้แอบยืมข้อมูลจากที่ที่อาจหายไปได้

การหยิบ 'static มาใช้เพื่อทำให้ lifetime error เงียบลงเป็นสัญชาตญาณที่พบบ่อยแต่มักผิด — มันแทบไม่เคยแก้ปัญหา dangling reference ที่แท้จริง (อย่างที่ตัวอย่าง dangle ข้างบนแสดง คำแนะนำ 'static ของ rustc เองก็ไม่ได้แก้อะไรจริงๆ) และบ่อยครั้งความหมายที่แท้จริงคือโค้ดควรเป็นเจ้าของข้อมูลแทนที่จะยืมมัน

ตัวอย่าง return-position impl Trait ในบทเรียน Traits จริง ๆ แล้วคือฟีเจอร์ lifetime: ใน edition 2024 impl Trait ที่ return ออกมาจะ capture ทุก lifetime ที่อยู่ใน scope โดยอัตโนมัติ ดังนั้น function ที่ borrow จาก parameter จึงไม่ต้องมี annotation + '_ แบบที่ต้องมีบน edition = "2021" (error[E0700]) ทางออกสำหรับ “ไม่ capture อะไรเลย” คือ bound use<> ที่ว่างซึ่งแสดงไว้ที่นั่น ถ้ายังไม่ได้อ่านตัวอย่างนั้น ตอนนี้เป็นเวลาที่ดีที่จะกลับไปอ่าน เพราะตอนนี้คุณเข้าใจ elision แล้ว — มันคือคำถาม “output เกี่ยวข้องกับ lifetime ไหน” คำถามเดียวกัน แค่ตอบโดยอัตโนมัติจาก edition แทนที่จะเป็น 'a ที่ชัดเจน

ทุก lifetime ที่คุณ annotate มาจนถึงตอนนี้ถูกเลือกครั้งเดียวโดยผู้เรียก ณ จุดที่ function ถูกเรียก บางครั้งคุณต้องการสิ่งตรงข้าม: bound ที่ต้องเป็นจริง สำหรับทุก lifetime ที่เป็นไปได้ ไม่ใช่แค่ lifetime หนึ่งที่เจาะจง ที่พบบ่อยที่สุดคือตอนรับ closure ที่จะถูกเรียกด้วย reference ที่ borrow ขึ้นมาใหม่ทุกครั้ง นี่คือ higher-ranked trait bound เขียนเป็น for<'a>:

fn apply_to_local<F>(f: F) -> i32
where
F: for<'a> Fn(&'a i32) -> i32,
{
let x = 10;
f(&x)
}
fn main() {
let double = |n: &i32| n * 2;
println!("{}", apply_to_local(double));
}
20

for<'a> Fn(&'a i32) -> i32 หมายความว่า “สำหรับ lifetime 'a ใดก็ได้ ที่ผู้เรียก f เลือก f ต้องรับ &'a i32 ได้” — apply_to_local สร้าง x และ borrow มัน ภายใน function เอง จึงไม่มี lifetime เดียวที่ตั้งชื่อไว้ล่วงหน้าจะอธิบายมันได้ ในทางปฏิบัติคุณแทบไม่ต้องเขียน for<'a> ด้วยมือเลย compiler จะ infer มันโดยอัตโนมัติสำหรับ closure parameter แบบนี้ และคุณส่วนใหญ่จะเจอ syntax นี้ตอนอ่าน compiler error หรือ library documentation มากกว่าจะเขียนมันเอง

Reference ที่มี lifetime ยาวกว่าใช้แทนที่ reference ที่สั้นกว่าได้เสมอ — คุณสมบัตินี้เรียกว่า variance และนี่คือเหตุผลที่โค้ดต่อไปนี้ compile ผ่านโดยไม่มี coercion syntax เลย:

fn main() {
let s: &'static str = "hello";
let owned = String::from("world");
let r: &str = &owned;
fn takes_short<'a>(x: &'a str) -> &'a str { x }
println!("{}", takes_short(s)); // a &'static str satisfies any shorter &'a
println!("{}", takes_short(r));
}
hello
world

มองว่า lifetime คือการรับประกัน “valid อย่างน้อยนานเท่านี้” — 'static คือการรับประกันที่แข็งแกร่งที่สุดเท่าที่จะเป็นไปได้ จึงผ่านเงื่อนไขที่อ่อนกว่าได้โดยอัตโนมัติ ความสัมพันธ์นี้ไปทางเดียวเท่านั้น: คุณส่ง &'a str ที่มีชีวิตสั้นไปแทนที่ตำแหน่งที่ต้องการ &'static str โดยเฉพาะไม่ได้ เพราะการรับประกันที่สั้นกว่าไม่สามารถแทนที่การรับประกันที่แข็งแกร่งกว่าได้

// A function with a lifetime annotation:
// the returned reference lives at least as long as the shorter of x or y.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// A struct that holds a reference — must annotate the lifetime.
struct Excerpt<'a> {
text: &'a str,
}
impl<'a> Excerpt<'a> {
fn announce(&self) -> &str {
self.text
}
}
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
println!("longest: {}", result);
}
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find '.'");
let excerpt = Excerpt { text: first_sentence };
println!("excerpt: {}", excerpt.announce());
}

Lifetime ไม่ใช่ระยะเวลาเป็นวินาทีหรือแนวคิด runtime เลย — มันคือชื่อที่ compiler ตั้งให้กับ ช่วงหนึ่งของโค้ด ที่ borrow หนึ่ง ๆ valid อยู่ เพื่อให้เปรียบเทียบช่วงเหล่านั้นกันได้ (“borrow นี้จบก่อนที่ value นั้นจะถูก drop ไหม”) Elision rule เป็นแค่การเดาที่ compiler ทำไว้ล่วงหน้าสำหรับรูปร่างที่พบบ่อยที่สุดของการเปรียบเทียบนั้น เมื่อรูปร่างของ function ไม่ตรงกับการเดาเหล่านั้น คุณจึงต้องระบุชื่อเองเพื่อให้มีอะไรให้เปรียบเทียบ

Lifetime annotation ที่ชัดเจนคือส่วนที่แปลกทาง syntax ที่สุดของ Rust สำหรับมือใหม่ และ elision มีอยู่เพื่อลดความถี่ที่คุณต้องเขียนมันโดยเฉพาะ — function จริงส่วนใหญ่ไม่ต้องการ 'a ที่ชัดเจนเลย สิ่งที่คุณได้แลกมาคือบั๊ก dangling reference (use-after-free, การ return pointer ไปยัง stack frame ที่ถูก free แล้ว) ย้ายจาก “runtime crash หรือแย่กว่านั้น memory corruption แบบเงียบ ๆ ใน C/C++” มาเป็น “compile error พร้อมเลขบรรทัดที่ชัดเจน” โดยไม่มี runtime cost เลย — lifetime ถูกลบทิ้งทั้งหมดก่อนที่โปรแกรมจะรัน

  • “Lifetime annotation ทำให้ value มีชีวิตอยู่นานขึ้น” ตรงข้ามกับการ “ควบคุม” lifetime เลย — มันแค่ อธิบาย ความสัมพันธ์ที่มีอยู่แล้วระหว่าง borrow ให้ compiler ตรวจสอบ มันขยายเวลาที่ value ใด ๆ มีชีวิตอยู่จริงไม่ได้เลย
  • “การเพิ่ม 'static ให้ field ของ struct หรือ bound เป็นวิธีที่ปลอดภัยในการทำให้ lifetime error หายไป” อย่างที่ตัวอย่าง dangle และ Cache แสดง 'static มักจะ compile ไม่ผ่านด้วยเหตุผลอื่น หรือบังคับให้ผู้เรียกทุกคนต้องส่งเฉพาะข้อมูลที่มีชีวิตอยู่ตลอดโปรแกรมจริง ๆ — มันแทบไม่เคยเป็นสิ่งที่ error เกี่ยวกับ borrow กำลังถามหาจริง ๆ เลย
  • “for<'a> เป็นสิ่งที่ฉันต้องเขียนตลอดเวลาเมื่อใช้ closure” Compiler infer higher-ranked trait bound ให้อัตโนมัติสำหรับ closure parameter ส่วนใหญ่ท่วมท้น คุณจะเจอ syntax นี้ตอนอ่านเป็นหลัก ไม่ใช่ตอนเขียน function signature
  • “&'static str ต้องการการแปลงอย่างชัดเจนเพื่อใช้แทนที่ &'a str” ไม่ต้องการ conversion หรือ coercion syntax ใด ๆ เลย — นี่คือสิ่งที่ variance รับประกัน: reference ที่มีชีวิตยาวกว่าผ่านเงื่อนไขที่สั้นกว่าได้อยู่แล้ว

เมื่อ function ทำให้เกิด lifetime error โค้ดที่ generate มามัก “แก้” ด้วยการเพิ่ม 'static bound ให้ generic parameter หรือ lifetime ของ struct เอง แทนที่จะปรับโครงสร้างให้ value เป็น owned หรือ borrowed ในช่วงเวลาที่สั้นกว่าและถูกต้อง

🐛 หา bug ในโค้ดนี้

struct Cache<'a> {
data: &'a str,
}
fn build_cache(input: &str) -> Cache<'static> {
Cache { data: input }
}
fn main() {
let text = String::from("local data");
let cache = build_cache(&text);
println!("{}", cache.data);
}
เฉลย

โค้ดนี้ compile ไม่ผ่าน — error: lifetime may not live long enough ชี้ไปที่ Cache { data: input } และอธิบายว่า lifetime ของ input “must outlive 'static” นี่คือรูปร่างการแก้ที่โค้ดที่ AI generate มักหยิบมาใช้พอดีเมื่อ lifetime error ปรากฏขึ้น: ใส่ 'static ให้ return type เพื่อให้ error ดู “ถูกจัดการแล้ว” แต่ input: &str ในที่นี้อาจเป็น reference ไปยังข้อมูล local ที่มีชีวิตสั้นก็ได้ — function ไม่มีทางสัญญากับผู้เรียกได้ว่าข้อมูลของพวกเขามีชีวิตอยู่ตลอดไป และ compiler จับคำโกหกนั้นได้ตั้งแต่จุดที่สัญญา (ที่เป็นไปไม่ได้) นั้นถูกให้ไว้ ก่อนที่บั๊กฝั่งผู้เรียกที่มันจะก่อขึ้นมา ไม่มี annotation ไหนแก้เวอร์ชันนี้ได้เลย: function ควร return Cache<'a> ที่ผูกกับ input lifetime จริง (fn build_cache<'a>(input: &'a str) -> Cache<'a>) หรือไม่ก็ Cache ควรเป็นเจ้าของ String แทนที่จะ borrow &str เลยตั้งแต่ต้น

Lifetime annotation ใน Rust มีไว้เพื่ออะไร?
ใน fn longest<'a>(x: &'a str, y: &'a str) -> &'a str annotation 'a รับประกันอะไร?
elision rule ข้อไหนใช้ได้กับ fn get(&self) -> &str บน struct ใดๆ ทำให้มัน compile ผ่านโดยไม่มี lifetime ที่ชัดเจน?
compile ไหม?
fn main() {
    let r;
    {
        let x = 5;
        r = &x;
    }
    println!("{}", r);
}
โปรแกรมนี้ print อะไร?
fn main() {
    let s: &'static str = "hello";
    fn takes_short<'a>(x: &'a str) -> &'a str { x }
    println!("{}", takes_short(s));
}
ทำไมการเพิ่ม 'static ใน Cache<'static> ใน fn build_cache(input: &str) -> Cache<'static> { Cache { data: input } } จึง compile ไม่ผ่าน?
higher-ranked trait bound อย่าง for<'a> Fn(&'a i32) -> i32 ใช้ทำอะไร?