iT邦幫忙

1

二、三天學一點點 Rust:來! Structs 與函式(28)

  • 分享至 

  • xImage
  •  

☕ Rust 教學:從函式建立 Struct 實例與擴展語法

在 Rust 中,結構體(struct)不僅是組織資料的工具,也常與函式搭配使用來建立與組裝實例。此小節將以 Coffee 結構體為例,從基本定義到函式返回 struct、欄位擴展(.. 語法)、擁有權(ownership)等進行逐步教學。


🏗️ 使用函式創建 Struct 實例 (工廠模式)

// 1. 將 struct 定義移出 main 函式
struct Coffee {
    name: String,
    price: i32,
    is_hot: bool,
}

fn main() {
    // 3. 呼叫函式來創建 Coffee 實例
    let coffee = make_coffee(String::from("Latte"), 130, true);
    // let coffee: Coffee = ...; // 也可以像這樣明確註釋類型

    println!(
        "My {} this morning cost {}. It's {} that it was hot.",
        coffee.name, coffee.price, coffee.is_hot
    );
}

// 2. 定義一個「工廠」函式,專門用來創建並回傳 Coffee 實例
fn make_coffee(name: String, price: i32, is_hot: bool) -> Coffee {
    Coffee {
        name: name,
        price: price,
        is_hot: is_hot,
    }
}
  1. 為何要將 struct 定義移出 main?
    作用域與重用性:如果 struct Coffee 定義在 main 函式內部,那麼它的作用域就僅限於 main 函式,其他函式(如 make_coffee)將無法識別 Coffee 這個型別。將其定義在 main 外部(頂層),意味著它在整個模組(通常是整個檔案)中都是可見的,mainmake_coffee 都可以使用它。

  2. 函式的回傳型別 (-> Coffee)
    make_coffee 函式的簽名中,-> Coffee 這部分明確指定了這個函式執行完畢後,將會回傳一個 Coffee 型別的實例。就像函式可以回傳 Stringi32 一樣,它完全可以回傳我們自訂的結構體型別。

  3. 工廠函式 (Factory Function)
    make_coffee 函式扮演了一個「工廠」的角色。它的唯一職責就是根據傳入的參數,「製造」出一個設定好的 Coffee 實例並將其回傳。這種模式有助於將實例的創建邏輯封裝起來,讓 main 函式的主流程更清晰。

(筆記補充) 在後續課程中,將會學到關聯函式 (Associated Functions),例如 impl Coffee { fn new(...) -> Coffee { ... } },這是 Rust 中創建結構體實例更為慣用的方法。


⚙️ 函式呼叫中的所有權轉移

現在我們來仔細追蹤一下,當我們呼叫 make_coffee 時,所有權是如何變化的。

struct Coffee {
    name: String,
    price: i32,
    is_hot: bool,
}

fn main() {
    // 1. 在 main 中創建一個 String,變數 name 擁有它
    let name = String::from("Latte");

    // 2. 呼叫 make_coffee,將 name 傳入
    let coffee = make_coffee(name, 130, true);

    println!(/* ... */);

    // 3. 此時若嘗試使用 name,將會報錯,因為所有權已轉移
    // println!("{}", name); // 這行會編譯失敗!
}

fn make_coffee(name: String, price: i32, is_hot: bool) -> Coffee {
    // 4. name 參數現在擁有傳入的 String
    Coffee {
        name: name, // 5. name 的所有權再次移動到 Coffee 實例的 name 欄位
        price: price,
        is_hot: is_hot,
    }
    // 6. Coffee 實例被回傳,其所有權轉移給 main 中的 coffee 變數
}

所有權的旅程:

  1. main 函式中,let name = String::from("Latte"); 創建了一個 String,其所有權屬於 main 中的 name 變數。
  2. 呼叫 make_coffee(name, ...) 時,因為 String 型別沒有實作 Copy Trait,所以 name 變數的所有權被移動 (moved) 到 make_coffee 函式的 name 參數。
  3. 此時,main 函式中的 name 變數立即失效,不能再被使用。
  4. make_coffee 函式內部,name 參數成為了這個 String 的新擁有者。
  5. Coffee { name: name, ... } 執行時,name 參數的所有權再次移動,這次是移給了新創建的 Coffee 實例的 name 欄位。
  6. 最後,make_coffee 函式回傳這個 Coffee 實例,其所有權被完整地轉移給了 main 函式中的 coffee 變數。

📦 欄位初始化簡寫法 (Field Init Shorthand)

當變數名稱和結構體的欄位名稱完全相同時,Rust 提供了一種方便的簡寫語法,讓程式碼更簡潔。

// ... (Coffee struct 定義)

fn main() {
    // ... (第一個 coffee 實例的創建)

    // 使用簡寫法創建第二個 coffee 實例
    let name = String::from("Caramel Macchiato");
    let price = 150;
    let is_hot = false;
    let coffee = Coffee {
        name,   // 等同於 name: name,
        price,  // 等同於 price: price,
        is_hot, // 等同於 is_hot: is_hot,
    };
    println!("{}, {}, {}", coffee.name, coffee.price, coffee.is_hot);
}

// 函式也可以使用簡寫法
fn make_coffee(name: String, price: i32, is_hot: bool) -> Coffee {
    Coffee {
        name,   // 因為參數名稱和欄位名稱相同
        price,
        is_hot,
    }
}
  • 簡化原則:如果一個局部變數或函式參數的名稱與你要賦值的結構體欄位名稱完全一樣,你就不需要重複寫 欄位: 變數 ,可以直接寫 變數 名稱即可。
  • 可讀性:這種語法糖 (syntactic sugar) 大大減少了重複的程式碼,使得創建結構體實例的程式碼更加乾淨、易讀。範例中的 make_coffee 函式和 main 函式裡 Caramel Macchiato 的創建都展示了這個技巧。

🔁 結構體更新語法 (Struct Update Syntax)

當我們想創建一個與現有實例大部分欄位都相同,只有少數欄位不同的新實例時,「結構體更新語法」就派上用場了。

// ... (struct 和 make_coffee 函式定義)
fn main() {
    let mocha = make_coffee(String::from("Latte"), 130, true);

    // 使用 mocha 的部分值來創建 caramel_macchiato
    let caramel_macchiato = Coffee {
        name: String::from("Caramel Macchiato"),
        ..mocha // 更新語法
    };

    println!(
        "{}, {}, {}",
        caramel_macchiato.name,   // "Caramel Macchiato"
        caramel_macchiato.price,  // 130 (來自 mocha)
        caramel_macchiato.is_hot  // true (來自 mocha)
    );
}
  1. .. 語法:..實例名稱 這個語法必須放在結構體初始化的大括號中的最後。它的意思是:「對於我在前面沒有明確指定的任何欄位,請從 mocha 實例中獲取其對應的值。」
  2. 運作方式:
    • caramel_macchiato 是一個全新的、獨立的 Coffee 實例。
    • 它的 name 欄位被明確地設定為 String::from("Caramel Macchiato")
    • priceis_hot 欄位沒有被明確指定,因此它們會從 mocha 實例中取得對應的值。這個過程可能是「複製」(Copy) 或「移動」(Move),取決於欄位的型別。

✨ 更新語法與所有權

結構體更新語法與所有權系統的互動非常緊密,特別是當欄位型別不具備 Copy Trait 時。

// ... (struct 和 make_coffee 函式定義)
fn main() {
    let mocha = make_coffee(String::from("Latte"), 130, true);

    let caramel_macchiato = Coffee {
        // name 欄位沒有被指定,將會從 mocha 中獲取
        ..mocha
    };

    println!("{}", caramel_macchiato.name); // 印出 "Latte"

    // 下面這行會導致編譯錯誤!
    // println!("{}", mocha.name);
}
  1. ..mocha 的所有權轉移:
    • 當我們執行 ..mocha 時,Rust 會逐一檢查 mocha 的欄位。
    • mocha.price (型別 i32) 和 mocha.is_hot (型別 bool) 都實作了 Copy Trait,所以它們的值會被複製到 caramel_macchiato 的對應欄位中。
    • mocha.name (型別 String) 沒有實作 Copy Trait。因此,它的所有權會被移動到 caramel_macchiato.name 中。
  2. 後果:
    • 由於 mocha.name 的所有權已經轉移給了 caramel_macchiato,mocha 實例本身變成了「部分移動」的狀態。你不能再存取 mocha.name,因為它已經不屬於 mocha 了。
    • 這就是為什麼 println!("{}", mocha.name); 會報錯的原因。

🛡️ 使用 clone() 解決更新語法中的所有權問題

如果我們希望在更新後,原始的 mocha 實例仍然保持完整可用,我們就需要對那些會被「移動」的欄位進行顯式的「複製」(clone)。

// ... (struct 和 make_coffee 函式定義)
fn main() {
    let mocha = make_coffee(String::from("Latte"), 130, true);

    let caramel_macchiato = Coffee {
        // 顯式地複製 name 欄位
        name: mocha.name.clone(),
        ..mocha
    };

    println!("Caramel Macchiato's name: {}", caramel_macchiato.name);
    // 現在這行可以正常運作了!
    println!("Mocha's name is still: {}", mocha.name);
}
  1. clone() 的作用:clone() 方法可以為某些型別(如 String)創建一個完整的、深層的副本。這意味著它會在堆上分配一塊新的記憶體,並將原始數據複製過去。
  2. 運作方式:
    • name: mocha.name.clone()mocha.name 的 String 數據被複製。caramel_macchiato.name 得到了這個全新的、獨立的 String 副本的所有權。
    • mocha 仍然擁有其原始的 name 欄位。
    • ..mocha 語法接著執行,但此時 name 欄位已經被指定,所以它只會處理剩下的 priceis_hot。這兩個欄位因為是 Copy 型別,所以會被複製。
  3. 結果:在這個過程中,mocha 的任何欄位的所有權都沒有被移動。因此,在創建 caramel_macchiato 之後,mocha 實例仍然是完全有效的,我們可以繼續存取它的所有欄位,包括 mocha.name

🧠 小結

主題 重點
struct 定義 建議放在 main 外,供全域函式使用
回傳 struct 函式可用 -> Coffee 直接返回 struct 實例
擁有權 String 等型別會搬移所有權,需注意欄位使用
.. 語法 可快速從另一個 struct 實例擴展欄位值
clone 避免所有權轉移的錯誤方式,產生一份新的內容副本

透過這些技巧與語法,Rust 的結構體操作會更彈性、更安全,也更能符合所有權與生命週期的編譯期檢查邏輯。


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言