iT邦幫忙

2024 iThome 鐵人賽

DAY 16
1

簡介

結構體(struct)是 Rust 中的一種自定型別,用來封裝多個相關數值成一個組合的型別,每個數值可以是不同的型別,到這邊和元組(Tuple)是一樣的,接下來不同的是,結構體必須要為每個數值對應欄位(fields)命名來表達數值的意義,同時這些欄位也可以用來指定或存取實例(instance)中的值,不像元組必須要要依賴資料的順序。

換句話說,類似 JavaScript 的 Class 的概念。

建立結構體實例

要定義一個結構體就用struct關鍵字,後面接大括號,裡面是用key:value的方式定義每個欄位的名稱以及對應的型別,以下我們定義一個Cat的型別。

struct Cat {
    name: String,
    age: u8,
    weight: f64
}

我們定義的結構體就像是一個餅乾模具,用這個模具成形的餅乾就稱為實例。

要用建出這個結構體實例的方式如下。

fn main() {
    let my_cat = Cat {
        name: String::from("Milo"),
        age: 2,
        weight: 4.6,
    };
    println!("My cat's name: {}", my_cat.name);
}

實例後面用 . 就可以用欄位名稱連結到我們的目標數值。

如果我們要更改實例上某個欄位的數值的話,和一般變數一樣一開始就要給 mut 關鍵字。
另外整個實例上所有的欄位上能不能改的設定都要是一樣的,也就是說不允許有的欄位可變有的不可變,要嘛全部可以要嘛全部不行。

fn main() {
    let mut my_cat = Cat {
        name: String::from("Milo"),
        age: 2,
        weight: 4.6,
    };
    println!("My cat's original name: {}", my_cat.name);
    my_cat.name = String::from("Luna");
    println!("My cat's current name: {}", my_cat.name);
}

Rust 也借鑒了近代程式的一些方便的語法,從 JavaScript 轉過來覺得真熟悉。

欄位初始化簡寫(Field init shorthand)

簡單說就是如果欄位名稱和變數或參數名相同不用再寫一次。
我們先寫一個完整的寫法:

// 完整寫法
fn adopt_cat(name: String, age: u8) -> Cat {
    Cat { name: name, age: age, weight: 5.5 }
}

結構體產生的實例也可以當成函數的返回值,產生實例的寫法本身就是表達式。

因為我們傳的參數名稱agenameCat上面對應的欄位名稱相同,就可以簡寫成以下:

// **欄位初始化簡寫, struct 內的 name 和 age 寫法可以簡化**
fn adopt_cat(name: String, age: u8) -> Cat {
    Cat { name, age, weight: 5.5 }
}

結構體更新語法(Struct update syntax)

這個語法可以用既有實例上的資訊來產生新的實例,然後可以針對我們想要特定欄位數值修改。
語法就是在建立實例的大括號內用..後面接既有的實例。

修改的欄位可以不用照順序,但..一定要放在最後一個,因為它代表的意義是剩下沒指定的欄位都會用和提供的實例相同的值,也就是提供實例上的數值都會當成預設值的意思。

fn main() {
    let my_cat1 = Cat {
        name: String::from("Milo"),
        age: 5,
        weight: 4.6
    };
    let my_cat2 = Cat {
        name: String::from("Luna"),
        ..my_cat1
    };

    println!("My 1st cat's name: {}", my_cat1.name);
    println!("My 2nd cat's name: {}", my_cat2.name);
}

這樣我們就產生兩個Cat的實例,除了name,兩者其他所有欄位數值是相同的。

結構體的所有權與生命週期

接著複習一下所有權還有生命週期,沒錯,函數有的它也跑不掉。

我們先把Cat再多加欄位owner

struct Cat {
    name: String,
    owner: String,
    age: u8,
    weight: f64
}

fn main() {
    let my_cat1 = Cat {
        name: String::from("Milo"),
        owner: String::from("Blue"),
        age: 5,
        weight: 4.6
    };
    let my_cat2 = Cat {
        name: String::from("Luna"),
        ..my_cat1
    };

    println!("My 1st cat's owner: {}", my_cat1.owner);
    println!("My 2nd cat's owner: {}", my_cat2.owner);
}

上面這段程式碼看起來合理,owner 是同一個所以沿用原有的實例的,但…

編譯錯誤了😂:

$ cargo run
error[E0382]: borrow of moved value: `my_cat1.owner`
  --> src/main.rs:30:39
   |
25 |       let my_cat2 = Cat {
   |  ___________________-
26 | |         name: String::from("Luna"),
27 | |         ..my_cat1
28 | |     };
   | |_____- value moved here
29 |
30 |       println!("My 1st cat's name: {}", my_cat1.owner);
   |                                         ^^^^^^^^^^^^^ value borrowed here after move
   |
   = note: move occurs because `my_cat1.owner` has type `String`, which does not implement the `Copy` trait
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0382`.

這是因為結構體更新語法和賦值意義上是相同的,所以 my_cat1.owner 的所有權被給出去了,其他欄位還是可以正常取得,name的話是因為我們給它另外一個name資料,ageweight是因為它們都是有實作Copy特徵的型別,實際上會複製了一份資料所以不影響到原有實例上數值的所有權。

解決方法有兩個:

  1. 實作Clone或另外新增一個owner資料給my_cat2,這類方式都會多佔另外一個記憶空間。
  2. owner的型別改成參考,其實更合理,我們不需要每次有新的cat就要跟著多一個owner出來。

我們試著改成第二種做法之後,有點熟悉的生命週期錯誤又出現啦XD

struct Cat {
    name: String,
    owner: &str,
    age: u8,
    weight: f64
}

fn main() {
    let owner= String::from("Blue");
    let my_cat1 = Cat {
        name: String::from("Milo"),
        owner: &owner,
        age: 5,
        weight: 4.6
    };
    let my_cat2 = Cat {
        name: String::from("Luna"),
        ..my_cat1
    };

    println!("My 1st cat's owner: {}", my_cat1.owner);
    println!("My 2nd cat's owner: {}", my_cat2.owner);
}
$ cargo run
error[E0106]: missing lifetime specifier
  --> src/main.rs:11:12
   |
11 |     owner: &str,
   |            ^ expected named lifetime parameter
   |
help: consider introducing a named lifetime parameter
   |
9  ~ struct Cat<'a> {
10 |     name: String,
11 ~     owner: &'a str,
   |

For more information about this error, try `rustc --explain E0106`.

但沒事,我們現在已經理解這個錯誤的意思了。

結構體有用到的參考都需要我們顯式標註生命週期,所以和函數一樣,我們在結構體名稱後面加上<>來傳入泛型生命週期參數,這樣在定義內部結構我們就可以用這個泛型生命週期參數,修改一下就可以正常編譯囉。

struct Cat<'a> {
    name: String,
    owner: &'a str,
    age: u8,
    weight: f64
}

和函數有一個比較大的差別是,結構體不適用於生命週期省略,所以即使只有一個欄位用到參考,這個參考也要加入生命週期詮釋,或更簡單的說:結構體用到參考的欄位型別都要加上生命週期詮釋

結構體不適用生命週期省略的原因大致上有:

  • 複雜的生命週期關係:函數的生命週期省略規則是有輸入和輸出的明確關係,結構體沒有這樣清晰的輸入輸出概念,而且結構體通常會有多個引用,這些都增加正確自動判斷的複雜度,很容易推斷錯誤導致不符合開發者預期的行為。
  • API 設計考慮:結構體通常代表程序中的核心數據結構,也常常作為公共 API 的一部分,明確的生命週期標註使 API 更清晰,更容易理解和使用,並強制開發者仔細考慮其數據結構的生命週期特性。

總結來說,結構體生命週期省略的限制是 Rust 設計者們為了保證程式碼的安全性和可靠性而做出的一種謹慎選擇。

接著再介紹兩種特殊的結構體。

tuple structs

這種結構體他沒有欄位名稱,只會定義欄位型別,特性和元組幾乎相同,最大的差別是他可以自訂本身的型別及名稱,用來和一般的tuple或其他的tuple structs做區別。舉例來說,我們用來表示顏色可以用 3個數字表達 RGB 的組合,表示座標可以用 3 個數字表達 XYZ 的組合,但是我們在用的時候不希望混淆這兩種類型。

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn describe_color(color: Color) {
    println!("Color - Red: {}, Green: {}, Blue: {}", color.0, color.1, color.2);
}

fn describe_point(point: Point) {
    println!("Point - X: {}, Y: {}, Z: {}", point.0, point.1, point.2);
}

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);

    describe_color(black);
    describe_point(origin);
}

tuple structs 的定義方式是關鍵字struct然後定義名稱和要定義的元組,所以用小括號
建出來的實例跟元組一樣可以用 . 來加上索引取得各數值。上面的例子可以看出來即使元組的各個元素定義的型別是完全一樣的,我們依然可以區分兩者,ColorPoint是不一樣的型別,如果函數的參數用錯型別的話編譯器會直接報錯的。

總結會用到tuple structs的情境就是:

  1. 想要和其他不同型別的元組做出區別。
  2. 對結構體內的欄位命名是不必要的時候。

unit-like structs

既然有類似元組的結構體,當然也會有類似單元型別 () 的結構體,這是一種沒有任何欄位的結構體。用來表示「沒有具體數值」的情況。

主要的用途:

  1. 標識用途:用來表示一個特定的狀態或類型,無需存儲數據。
  2. 佔位符:用來佔據某個位置,並在後續代碼中可能會被用來觸發特定行為。

寫法也很簡單,關鍵字struct後面定義名稱直接分號結束,不用任何括號,因為沒有要儲存任何數據,之後就和一般結構體一樣的方式建立它的實例。

struct AdminAccess;
struct GuestAccess;

fn main() {
    let admin = AdminAccess;
    let guest = GuestAccess;
    perform_admin_action(admin);
}

fn perform_admin_action(_admin: AdminAccess) {
    println!("Admin action performed successfully!");
}

舉例來說,上面的程式碼就讓函式可以根據不同的型別做檢查,不符合的型別會報錯。我們可以把這種權限標記在不同的使用者上,讓我們的程式碼更具可讀性和可維護性,

再之後用於標識特徵的實作也會用到unit-like structs,之後介紹到的時候會更瞭解它的重要性,目前的部分就先介紹到這樣。

結語

今天介紹了從基本的結構體到unit-like structs的應用以及結構體處理所有權和生命週期的情況,下一篇我們會繼續延伸結構體來介紹方法(method),概念上就是 JavaScript 裡 class 有的那個 method 沒錯。


上一篇
Day15 - 生命週期
下一篇
Day17 - 方法與關聯函數
系列文
螃蟹幼幼班:Rust 入門指南30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言