嗨嗨!大家好!歡迎來到 Rust 三十天挑戰的第五天!
昨天我們學習了所有權這個核心概念,但你可能已經發現了一個問題:如果每次把變數傳給函式就會轉移所有權,那程式碼會變得非常難寫和不實用。想像一下,如果你想計算一個字串的長度,卻因此失去了這個字串的使用權,這根本不合理對吧?
幸好,Rust 提供了一個優雅的解決方案:參考 (References) 與借用 (Borrowing)。
今天我們要學習如何在不轉移所有權的情況下,讓函式能夠使用資料。這就像是把你的書「借」給朋友看,而不是直接「送」給他一樣 —— 你依然是書的主人,朋友只是暫時使用而已。
參考就像是變數的「別名」或「指向」,它讓我們可以使用某個值,但不擁有它。在 Rust 中,我們使用 & 符號來建立參考:
fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);  // 傳遞 s1 的參考
    
    println!("'{}' 的長度是 {}", s1, len);  // s1 仍然可以使用!
}
fn calculate_length(s: &String) -> usize {  // s 是 String 的參考
    s.len()
}  // s 離開作用域,但因為它沒有擁有資料,所以不會釋放記憶體
在這個例子中,&s1 建立了一個指向 s1 的參考,而函式參數 s: &String 接收這個參考。重要的是,s1 的所有權沒有轉移,所以我們之後還能繼續使用它!
我們將使用參考稱為「借用」(borrowing),因為我們只是借用值而不擁有它。但借用有一些重要的規則:
fn main() {
    let reference_to_nothing = dangle();  // 這會編譯錯誤!
}
fn dangle() -> &String {  // 試圖回傳一個參考
    let s = String::from("hello");
    &s  // 回傳 s 的參考,但 s 即將被釋放!
}  // s 離開作用域並被釋放,所以參考指向了無效的記憶體
Rust 編譯器會阻止這種「懸置參考」(dangling references) 的產生。
在任何給定時間,你可以擁有:
但不能同時擁有兩者!
讓我們看看為什麼需要這個規則:
fn main() {
    let mut s = String::from("hello");
    
    let r1 = &s;      // 沒問題 - 不可變參考
    let r2 = &s;      // 沒問題 - 可以有多個不可變參考
    println!("{} and {}", r1, r2);  // r1 和 r2 在這裡結束使用
    
    let r3 = &mut s;  // 沒問題 - 可變參考
    println!("{}", r3);
}
但如果同時存在可變和不可變參考就會出錯:
fn main() {
    let mut s = String::from("hello");
    
    let r1 = &s;      // 不可變參考
    let r2 = &mut s;  // 編譯錯誤!不能在不可變參考存在時建立可變參考
    
    println!("{} {}", r1, r2);
}
如果我們想要透過參考來修改值,就需要使用可變參考:
fn main() {
    let mut s = String::from("hello");
    
    change(&mut s);  // 傳遞可變參考
    
    println!("{}", s);  // 輸出:hello, world
}
fn change(some_string: &mut String) {
    some_string.push_str(", world");
}
注意幾個重要的點:
mut
&mut
&mut String
同一時間只能有一個可變參考:
fn main() {
    let mut s = String::from("hello");
    
    let r1 = &mut s;
    let r2 = &mut s;  // 編譯錯誤!
    
    println!("{}, {}", r1, r2);
}
這個限制防止了「資料競爭」(data races),確保在修改資料時不會有其他代碼同時讀取或修改相同的記憶體。
參考的生命週期必須在被參考值的生命週期內:
fn main() {
    let r;                    // 宣告 r,但還沒初始化
    
    {
        let x = 5;
        r = &x;               // 錯誤!x 的生命週期太短
    }                         // x 在這裡被釋放
    
    println!("r: {}", r);     // r 參考了已被釋放的記憶體
}
正確的做法:
fn main() {
    let x = 5;                // x 進入作用域
    let r = &x;               // r 參考 x
    
    println!("r: {}", r);     // 沒問題,x 仍然有效
}                             // x 和 r 都離開作用域
除了參考整個 String,我們還可以參考字串的一部分,這叫做「字串切片」:
fn main() {
    let s = String::from("hello world");
    
    let hello = &s[0..5];    // 或 &s[..5]
    let world = &s[6..11];   // 或 &s[6..]
    let whole = &s[..];      // 整個字串的切片
    
    println!("hello: {}", hello);  // hello
    println!("world: {}", world);  // world
    println!("whole: {}", whole);  // hello world
}
字串切片的型別是 &str,它是一個不可變參考。
fn main() {
    let s = "Hello, world!";  // s 的型別是 &str
    
    // 字串字面值是程式二進位檔中特定位置的切片
    // 這也是為什麼字串字面值是不可變的
}
使用字串切片可以讓函式更靈活:
fn first_word(s: &String) -> &str {  // 只能接受 String 的參考
    let bytes = s.as_bytes();
    
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    
    &s[..]
}
fn first_word_better(s: &str) -> &str {  // 可以接受 &String 和 &str
    let bytes = s.as_bytes();
    
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    
    s
}
fn main() {
    let my_string = String::from("hello world");
    let word1 = first_word(&my_string);        // 只能這樣
    let word2 = first_word_better(&my_string); // 可以這樣
    let word3 = first_word_better("hello");    // 也可以這樣
    
    println!("{}, {}, {}", word1, word2, word3);
}
切片不僅適用於字串,也適用於其他集合:
fn main() {
    let a = [1, 2, 3, 4, 5];
    let slice = &a[1..3];  // 型別是 &[i32]
    
    println!("切片:{:?}", slice);  // [2, 3]
    
    // 將切片傳給函式
    print_slice(slice);
    print_slice(&a[..]);  // 傳遞整個陣列的切片
}
fn print_slice(slice: &[i32]) {
    for item in slice {
        println!("值:{}", item);
    }
}
// ❌ 錯誤的寫法
fn wrong_way() {
    let mut s = String::from("hello");
    let r1 = &s;
    let r2 = &mut s;  // 錯誤!
    println!("{} {}", r1, r2);
}
// ✅ 正確的寫法
fn right_way() {
    let mut s = String::from("hello");
    let r1 = &s;
    println!("{}", r1);  // r1 的最後使用
    
    let r2 = &mut s;     // 沒問題,r1 已經不再使用
    println!("{}", r2);
}
// ❌ 錯誤的寫法
fn wrong_return() -> &String {
    let s = String::from("hello");
    &s  // 錯誤!s 即將被釋放
}
// ✅ 正確的寫法
fn right_return() -> String {
    let s = String::from("hello");
    s  // 回傳所有權
}
// ✅ 或者接受參考參數
fn process_and_return(input: &str) -> &str {
    // 處理 input 並回傳它的一部分
    &input[0..1]
}
今天我們學會了參考與借用的核心概念:
參考的基本概念:
& 建立參考,&mut 建立可變參考借用的黃金規則:
實用技巧:
&str 比 &String 更靈活&[T] 可以處理陣列的一部分為什麼這樣設計?
寫一個程式實作以下功能:
get_length(s: &String) -> usize,回傳字串的長度capitalize_first_letter(s: &mut String),將字串的第一個字母改為大寫count_words(text: &str) -> usize,計算字串中的單字數量main 函式中測試這些函式fn main() {
    // 測試 get_length
    let text = String::from("Hello, world!");
    let length = get_length(&text);
    println!("字串長度:{}", length);
    
    // 測試 capitalize_first_letter
    let mut greeting = String::from("hello, rust!");
    println!("修改前:{}", greeting);
    capitalize_first_letter(&mut greeting);
    println!("修改後:{}", greeting);
    
    // 測試 count_words
    let sentence = "Rust is awesome and powerful";
    let word_count = count_words(sentence);
    println!("單字數量:{}", word_count);
}
fn get_length(s: &String) -> usize {
    // 你來實作!
}
fn capitalize_first_letter(s: &mut String) {
    // 你來實作!
}
fn count_words(text: &str) -> usize {
    // 你來實作!
}
學習重點:
&String, &str)&mut String)注意這個函式回傳 String 而不是 &str,因為我們需要建立一個新的字串。在後續學習生命週期的概念之後,我們會學到如何更優雅地處理這類問題。
明天我們將學習結構體與列舉,這些自訂型別將讓我們能夠建立更複雜、更有意義的資料結構。我們也會看到參考在結構體中的應用!
參考與借用是 Rust 中非常重要的概念,它們讓我們能夠寫出高效且安全的程式碼。一旦掌握了這些概念,你會發現 Rust 程式設計的美妙之處!
那麼!我們明天見!