Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rust 擁有權 vs 借用:生命週期問題指南 🦀

🔍 核心答案:只有借用才有生命週期問題!

簡單來說

  • 擁有權:我的東西,沒有生命週期問題
  • ⚠️ 借用:別人的東西,需要生命週期保護

📊 對比表格

類型是否需要生命週期範例說明
擁有類型❌ 不需要String, Vec<T>, u32我的東西,隨便用
借用類型✅ 需要&str, &Vec<T>, &u32別人的東西,要小心

💡 為什麼只有借用需要生命週期?

🏠 擁有權的本質

  • 擁有 = 我控制這塊記憶體
  • 我可以決定什麼時候釋放
  • 不會有懸空指標問題

📞 借用的本質

  • 借用 = 指向別人記憶體的指標
  • 如果原主人消失了,指標就變成「懸空指標」💥
  • 生命週期確保原主人活得夠久

✅ 擁有權:沒有生命週期問題

#![allow(unused)]
fn main() {
struct Person {
    name: String,    // 擁有 String,沒有生命週期標註
    age: u32,       // 擁有 u32,沒有生命週期標註
}

fn create_person() -> Person {
    Person {
        name: "小明".to_string(),  // 創建新的 String
        age: 25,
    }
    // 返回擁有權,完全沒問題!✅
}

// 更多擁有權例子
struct Config {
    host: String,        // 擁有
    port: u16,          // 擁有
    users: Vec<String>, // 擁有
    settings: HashMap<String, String>, // 擁有
}

impl Config {
    fn new(host: String, port: u16) -> Self {
        Config {
            host,
            port,
            users: Vec::new(),
            settings: HashMap::new(),
        }
    }
    
    // 所有方法都不需要生命週期標註
    fn get_host(&self) -> &str {
        &self.host
    }
}
}

⚠️ 借用:需要生命週期標註

#![allow(unused)]
fn main() {
struct PersonRef<'a> {
    name: &'a str,   // 借用,需要 'a
    age: u32,        // 擁有,不需要 'a
}

// 這樣會編譯失敗!❌
fn create_person_ref() -> PersonRef {
    let name = "小明".to_string();
    PersonRef {
        name: &name,  // 錯誤!name 會被銷毀
        age: 25,
    }
}

// 正確的借用用法 ✅
fn use_borrowed_data() {
    let name = "小明";  // 字串字面量,生命週期很長
    let person = PersonRef {
        name: &name,    // 可以借用
        age: 25,
    };
    println!("{} is {} years old", person.name, person.age);
    // name 活得比 person 久,所以安全
}
}

🎯 實際範例對比

方案 A:全用擁有權(推薦給初學者)

#![allow(unused)]
fn main() {
struct DatabaseConfig {
    host: String,           // 擁有
    username: String,       // 擁有
    password: String,       // 擁有
    database_name: String,  // 擁有
}

struct Application {
    name: String,           // 擁有
    version: String,        // 擁有
    config: DatabaseConfig, // 擁有
}

impl Application {
    fn new(name: String, version: String, config: DatabaseConfig) -> Self {
        Application { name, version, config }
    }
    
    // 完全沒有生命週期問題!
    fn get_connection_string(&self) -> String {
        format!("{}@{}/{}", 
                self.config.username, 
                self.config.host, 
                self.config.database_name)
    }
}
}

方案 B:混合借用(需要處理生命週期)

#![allow(unused)]
fn main() {
struct DatabaseConfigRef<'a> {
    host: &'a str,           // 借用 - 需要 'a
    username: &'a str,       // 借用 - 需要 'a
    password: &'a str,       // 借用 - 需要 'a
    database_name: &'a str,  // 借用 - 需要 'a
}

struct ApplicationRef<'a> {
    name: &'a str,                    // 借用 - 需要 'a
    version: &'a str,                 // 借用 - 需要 'a
    config: &'a DatabaseConfigRef<'a>, // 借用 - 需要 'a
}

impl<'a> ApplicationRef<'a> {
    fn new(name: &'a str, version: &'a str, config: &'a DatabaseConfigRef<'a>) -> Self {
        ApplicationRef { name, version, config }
    }
    
    // 返回的字串也需要生命週期標註
    fn get_connection_string(&self) -> String {
        format!("{}@{}/{}", 
                self.config.username, 
                self.config.host, 
                self.config.database_name)
    }
}
}

🚀 什麼時候該用借用?

✅ 適合借用的情況

  1. 短期使用:函數參數傳遞
  2. 避免複製:大型資料結構
  3. 效能優化:避免不必要的記憶體分配
#![allow(unused)]
fn main() {
// 函數參數借用 - 很常見且安全
fn print_info(name: &str, age: u32) {
    println!("{} is {} years old", name, age);
}

// 處理大型資料時借用
fn process_large_data(data: &Vec<u8>) -> usize {
    data.len()  // 只是讀取,不需要擁有
}
}

❌ 不適合借用的情況

  1. 長期存儲:結構體字段
  2. 返回值:從函數返回
  3. 複雜的所有權關係

🚨 特殊情況:借用進借用出(需要生命週期)

核心概念:借用進也有借用出 → 需要生命週期 🚨

當函數接收引用參數(借用進)並且返回引用(借用出)時,編譯器需要知道返回的引用能活多久。

❌ 不寫生命週期會編譯失敗的情況

#![allow(unused)]
fn main() {
// 這樣寫會編譯錯誤!
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
}

編譯器錯誤訊息

error[E0106]: missing lifetime specifier
 --> src/lib.rs:1:37
  |
1 | fn longest(x: &str, y: &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 `x` or `y`

🤔 編譯器的困惑

編譯器不知道:

  • 返回的 &str 是來自 x 還是 y
  • 如果來自 x,那 x 要活多久?
  • 如果來自 y,那 y 要活多久?
  • 我該如何檢查生命週期安全?😵

✅ 正確的寫法

#![allow(unused)]
fn main() {
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
}

編譯器現在理解了

  • xy 都有相同的生命週期 'a
  • 返回值也是 'a 生命週期
  • 返回的引用不會比 xy 活得更久 ✅

📚 更多借用進借用出的範例

1. 返回引用的一部分

#![allow(unused)]
fn main() {
// ❌ 編譯錯誤(實際上這個可以省略,因為只有一個輸入引用)
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];  // 返回 s 的一部分
        }
    }
    
    &s[..]
}

// ✅ 正確(其實因為生命週期省略規則,上面的寫法也對)
fn first_word<'a>(s: &'a str) -> &'a str {
    let bytes = s.as_bytes();
    
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    
    &s[..]
}
}

2. 多個輸入引用,返回其中一個

#![allow(unused)]
fn main() {
// ❌ 編譯錯誤:不知道返回哪個引用
fn pick_one(first: &str, second: &str, use_first: bool) -> &str {
    if use_first {
        first
    } else {
        second
    }
}

// ✅ 正確版本
fn pick_one<'a>(first: &'a str, second: &'a str, use_first: bool) -> &'a str {
    if use_first {
        first
    } else {
        second
    }
}
}

3. 不同生命週期的例子

#![allow(unused)]
fn main() {
// 兩個參數可能有不同的生命週期,但返回值綁定到其中一個
fn choose_first<'a, 'b>(x: &'a str, _y: &'b str) -> &'a str {
    x  // 總是返回第一個,所以返回值生命週期只跟 'a 有關
}

// 返回值生命週期必須和至少一個輸入參數相關
fn get_longer<'a, 'b>(x: &'a str, y: &'b str) -> &'a str 
where 
    'b: 'a  // 'b 的生命週期至少要和 'a 一樣長
{
    if x.len() > y.len() {
        x
    } else {
        // 這裡實際上有問題,因為 y 的生命週期是 'b
        // 但我們說返回 'a,所以需要 'b: 'a 約束
        x  // 為了編譯通過,還是返回 x
    }
}
}

🏗️ 結構體方法中的借用進借用出

// 結構體儲存引用,需要生命週期
struct Book<'a> {
    title: &'a str,
    author: &'a str,
}

impl<'a> Book<'a> {
    // 方法返回內部的引用(借用進借用出)
    fn get_title(&self) -> &'a str {
        self.title
    }
    
    // 這個也需要,因為 self 是借用,返回也是借用
    fn get_author(&self) -> &'a str {
        self.author
    }
}

// 更複雜的實際應用範例:字串處理器
struct TextProcessor<'a> {
    content: &'a str,
}

impl<'a> TextProcessor<'a> {
    // 借用進(self)借用出(返回值)
    fn find_word(&self, word: &str) -> Option<&'a str> {
        let start = self.content.find(word)?;
        let end = start + word.len();
        Some(&self.content[start..end])
    }
    
    // 借用進借用出:返回內容的一部分
    fn get_lines(&self) -> Vec<&'a str> {
        self.content.lines().collect()
    }
    
    // 借用進借用出:返回第一行
    fn first_line(&self) -> &'a str {
        self.content.lines().next().unwrap_or("")
    }
}

fn main() {
    let text = String::from("Hello World\nThis is Rust\nLifetime example");
    let processor = TextProcessor { content: &text };
    
    // 查找單詞
    if let Some(word) = processor.find_word("Rust") {
        println!("找到: {}", word);
    }
    
    // 取得所有行
    let lines = processor.get_lines();
    for (i, line) in lines.iter().enumerate() {
        println!("第 {} 行: {}", i + 1, line);
    }
    
    // 取得第一行
    println!("第一行: {}", processor.first_line());
}

🎯 實際使用範例

fn main() {
    let string1 = "long string is long";
    
    {
        let string2 = "xyz";
        let result = longest(string1, string2);
        println!("The longest string is {}", result);
        // result 在這裡還可以使用,因為 string1 和 string2 都還活著
    }
    // string2 死了,但沒關係,我們已經用完 result 了
}

💡 不想寫生命週期的替代方案

方案 1:返回擁有權

#![allow(unused)]
fn main() {
fn longest_owned(x: &str, y: &str) -> String {
    if x.len() > y.len() {
        x.to_string()  // 創建新的 String
    } else {
        y.to_string()  // 創建新的 String
    }
}
}

方案 2:返回索引或布林值

#![allow(unused)]
fn main() {
fn longest_index(x: &str, y: &str) -> bool {
    x.len() > y.len()  // 返回 true 表示 x 比較長
}
}

方案 3:使用靜態字串

#![allow(unused)]
fn main() {
fn longest_static() -> &'static str {
    "這是一個靜態字串"  // 'static 生命週期,活到程式結束
}
}

🎯 完整對比:需要 vs 不需要生命週期

#![allow(unused)]
fn main() {
// ✅ 不需要生命週期:沒有返回引用
fn print_string(s: &str) {
    println!("{}", s);
}

// ✅ 不需要生命週期:返回擁有所有權的值
fn make_uppercase(s: &str) -> String {
    s.to_uppercase()
}

// ❌ 需要生命週期:借用進 + 借用出
fn get_part(s: &str) -> &str {
    &s[0..5]
}

// ✅ 正確版本
fn get_part<'a>(s: &'a str) -> &'a str {
    &s[0..5]
}

// ❌ 需要生命週期:多個借用進 + 借用出
fn combine_refs(x: &str, y: &str) -> &str {
    if x.len() > 0 { x } else { y }
}

// ✅ 正確版本
fn combine_refs<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > 0 { x } else { y }
}

// ✅ 不需要生命週期:返回引用但有明確的生命週期(如 'static)
fn get_static() -> &'static str {
    "hello"  // 字串字面量是 'static
}
}

🧠 記憶口訣

判斷是否需要生命週期 🚨

需要生命週期的情況

#![allow(unused)]
fn main() {
// 1. 借用進 + 借用出
fn func(x: &str) -> &str { x }

// 2. 多個借用進 + 借用出(不確定返回哪個)
fn func(x: &str, y: &str) -> &str { x }

// 3. 結構體存儲引用
struct S<'a> { field: &'a str }

// 4. impl 塊中方法返回引用
impl<'a> S<'a> {
    fn get(&self) -> &'a str { self.field }
}
}

不需要生命週期的情況

#![allow(unused)]
fn main() {
// 1. 只借用進,沒有借用出
fn func(x: &str) { }

// 2. 返回擁有所有權的值
fn func(x: &str) -> String { x.to_string() }

// 3. 沒有引用參與
fn func(x: String) -> String { x }

// 4. 返回引用但有明確的生命週期(如 'static)
fn func() -> &'static str { "hello" }
}

簡單判斷法

  • 如果是 & 開頭 → 可能需要生命週期 ⏰
  • 如果沒有 & → 不需要生命週期 ✅
  • 函數簽名有借用進也有借用出 → 一定需要生命週期 🚨

核心口訣

「借進借出,生命週期要有」

  • & 進來,有 & 出去 → 需要 'a
  • 只進不出,或出的不是引用 → 不需要 'a

實用建議

  1. 初學者策略:多用擁有權,少用借用
  2. 進階優化:理解後再使用借用提升效能
  3. 記住原則:編譯器是你的朋友,會阻止記憶體錯誤
  4. 函數設計:如果可能,優先返回擁有權而不是借用

📝 總結

概念特徵生命週期使用場景
擁有權我的東西,完全控制❌ 不需要結構體字段、返回值、長期存儲
借用別人的東西,臨時使用✅ 需要函數參數、短期操作、效能優化

核心理解

  • 🏠 擁有權 = 房子是我的,我決定什麼時候拆
  • 📞 借用 = 借朋友的房子,朋友搬家前我就得搬出來

實用原則: 生命週期只是 Rust 確保「借用安全」的機制。如果你都用擁有權,就完全不用擔心生命週期問題!