Rust 學習筆記:結構體

2023年10月7日 · 中文


前言

本系列文書寫我個人初探 Rust 的學習筆記,章節劃分主要基於著名的 The BookThe Rust Programming Language,程式碼部分通常是個人閱讀消化後以方便說明的方式撰寫,完整學習建議直接參見該書。

The Rust Programming Language

該書也有中文翻譯版,不過個人閱讀以英文原版為主以鞏固對 terminology 的一致認識,我認為對未來閱讀以及查找資料會較為順暢。

不論語言該書都是相當優秀的學習資源,選擇適合你的語言開始學習 Rust 吧。

Appendix F: Translations of the Book

結構體 struct

Rust 並不像 C++ 或 Java 提供物件導向的 class 語法,Rust 中使用 structenum 配合 impl 區塊來自定義類型結構與 method,這篇先來學習 struct

struct 定義

定義 struct 的語法如下。

struct Person {
    name: String,
    height: u32,
    weight: u32
}

在 main function 裡建立一個 Person 的 instance,給定 key-value pair,這邊使用了 derive 給了 Person Debug trait,這個 concept 後面會學習到,在這邊只是用來讓我可以把 struct print 出來而已。

#[derive(Debug)]
struct Person {
    name: String,
    height: u32,
    weight: u32
}

fn main() {
    let p = Person {
        name: String::from("Josh"),
        height: 180,
        weight: 80
    };
    
    println!("{:?}", p);    // Person { name: "Josh", height: 180, weight: 80 }
}

取得 struct 中的值

可以用 dot notation 來取用 struct instance 中的值。

println!("{}", p.name);     // Josh

可變結構

一樣可以用 mut 讓 struct 可變,要注意的是 struct 必須是整個可變,我們不能僅讓其中一個 field 可變。

let mut p = Person{
        name: String::from("Josh"),
        height: 180,
        weight: 80
    };
    
println!("{}", p.height);       // 180

p.height += 10;

println!("{}", p.height);       // 190

Struct Update Syntax

在 Rust 中可以運用所謂的 Struct Update Syntax 來利用存在的 instance 來建立新的 instance,並保留其部分設定值。語法為使用 ..

學過 Javascript 的看起來會感覺很像 spread operator 的用法。

let p = Person{
    name: String::from("Josh"),
    height: 180,
    weight: 80
};

let p2 = Person {
    name: String::from("Peter"),
    ..p
};

println!("{:?}", p);        // Person { name: "Josh", height: 180, weight: 80 }
println!("{:?}", p2);       // Person { name: "Peter", height: 180, weight: 80 }

Tuple structs

struct 也可以沒有 key,看起來會很像 tuple,主要用途在於為結構命名。

struct Point(i32, i32, i32);

Unit-like structs

struct 也可以完全沒有欄位,很像 unit type ()

struct Unit; 

實作 method

我們使用 impl 來建立 Person 的 implementation block,在裡面可以實作 method。

struct Person {
    name: String,
    height: u32,
    weight: u32
}

impl Person {
    fn print_name(&self) {
        println!("{}", self.name);
    }
}

fn main() {
    let p = Person{
        name: String::from("Josh"),
        height: 180,
        weight: 80
    };
    
    p.print_name();     // Josh
}

這邊的 &selfself: &self 的簡寫,method 的第一個參數必須是名為 selfSelf 型別。

self 也不一定需要是參考,在部分情況需要時一樣可以不使用參考而取得 self 的所有權。

在 impl block 中可以實作複數個 method,這邊再寫一個計算 bmi 的 method。

struct Person {
    name: String,
    height: u32,
    weight: u32
}

impl Person {
    fn print_name(&self) {
        println!("{}", self.name);
    }
    
    fn get_bmi(&self) -> f64 {
        let h = ((self.height as f64) / 100.0) * ((self.height as f64) / 100.0);
        (self.weight as f64) / h
    }
}



fn main() {
    let p = Person{
        name: String::from("Josh"),
        height: 180,
        weight: 80
    };
    
    println!("{:?}", p.get_bmi());      // 24.691358024691358
}

Associated Functions

若不需要自己的 instance 的話,可以撰寫第一個參數不是 self 的 function,這種 function 在 Rust 中被稱為 Associated Functions,他們不被稱作 methods,因此我們才會說 method 的第一個參數必須是名為 selfSelf 型別。

struct 可以用複數個 impl block,這裡我就分開一個 block。

struct Person {
    name: String,
    height: u32,
    weight: u32
}

impl Person {
    fn print_name(&self) {
        println!("{}", self.name);
    }
    
    fn get_bmi(&self) -> f64 {
        let h = ((self.height as f64) / 100.0) * ((self.height as f64) / 100.0);
        (self.weight as f64) / h
    }
}

impl Person {
    // Associated Function
    fn say_hello() {
        println!("{}", "Hello");
    }
}

fn main() {
    let p = Person{
        name: String::from("Josh"),
        height: 180,
        weight: 80
    };
    
    Person::say_hello();        // Hello
    
    println!("{:?}", p.get_bmi());
}

使用 Associated Function 的語法是 ::,我們在建立 String 時使用 From 經常用到。

事實上這也是 Associated Function 最常見的 use case,用來產生新的 struct instance。

我們可以為 Person 實作 new function 來建立新的 instance。

#[derive(Debug)]
struct Person {
    name: String,
    height: u32,
    weight: u32
}

impl Person {
    fn print_name(&self) {
        println!("{}", self.name);
    }
    
    fn get_bmi(&self) -> f64 {
        let h = ((self.height as f64) / 100.0) * ((self.height as f64) / 100.0);
        (self.weight as f64) / h
    }
}

impl Person {
    // Associated Functions
    fn say_hello() {
        println!("{}", "Hello");
    }
    
    fn new(name: String, height: u32, weight: u32) -> Person {
        Person { name, height, weight }
    }
}

fn main() {
    let p = Person::new(String::from("Josh"), 180u32, 80u32);
    println!("{:?}", p);        // Person { name: "Josh", height: 180, weight: 80 }
}

是不是更像建立 String 的用法了 ?

這邊還用到了一個特性,在 key 和 value 同名時可以省略,而不需要寫成 name: name,這如果是寫過 Javascript 的應該會很熟悉這個方便的寫法。

fn new(name: String, height: u32, weight: u32) -> Person {
    Person { name, height, weight }
}

下一篇會進入 enum 囉。