2020年3月16日

rust05 Structs

struct 是自訂資料類別,可包裝多個相關的 values。如果是物件導向程式語言, struct 就像是物件中的 attributes,以下會討論 tuple 跟 struct 的差異,說明 struct 的使用方法,並討論如何定義 method 與相關函數,來限制 structs 資料的行為。Structs 與 Enum 都是建立新類別的方法,並充分運用了 Rust 的編譯時期資料型別檢查的功能。

Defining and Instantiating Structs

struct 跟 tuple 類似,每一個部分可以是不同的資料型別,但不同於 tuple,struct 需要為每一個部分的資料命名,表達其值的意義。因為有了名字,struct 比 tuple 靈活,不需要靠順序來存取裡面的值。

struct 的每一個部分的資料稱為 field

struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

定義 struct 後,透過 key:value 名稱來存取裡面的值,例如下面是修改 user1 的 email

let mut user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};

user1.email = String::from("anotheremail@example.com");

Using the Filed Init Shorthand when Variables and Fields Have the same value

這是產生 user 的 function

fn build_user(email: String, username: String) -> User {
    User {
        email: email,
        username: username,
        active: true,
        sign_in_count: 1,
    }
}

當 field 的名稱與 value 都相同時,email 以及 username 都是一樣的,可使用 field init shorthand 改寫這個 function

fn build_user(email: String, username: String) -> User {
    User {
        email,
        username,
        active: true,
        sign_in_count: 1,
    }
}

Creating Instances From Other Instances With Struct Update Syntax

透過 struct update syntax 這種語法更新 struct 裡面的 field

這是用 user1 的兩個欄位,產生新的 user2

let user2 = User {
    email: String::from("another@example.com"),
    username: String::from("anotherusername567"),
    active: user1.active,
    sign_in_count: user1.sign_in_count,
};

可改用這種語法

let user2 = User {
    email: String::from("another@example.com"),
    username: String::from("anotherusername567"),
    ..user1
};

Using Tuple Structs without Named Fields to Create Different Types

tuple 跟 struct 類似,但沒有欄位名稱。如果想要為 tuple 命名,可使用 tuple structs

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

let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

雖然都是一樣的結構,但 black 跟 origin 是不同的類別

Unit-Like Structs Without Any Fields

unit-line struct 類似於 (),就是 unit 類別,裡面沒有任何 fields。可用在想讓某個類別實現 trait,但不需要再類別中儲存資料的狀況。

Ownership of Struct Data

如果在 Struct 中使用了 String,而不是 &str 的 slice,這就是要讓這個 struct 自己擁有這個欄位的資料,只要 struct 有效,資料就有效。

也可以讓 struct 儲存被其他類別擁有的資料的 reference,但就需要用到 lifetimes 的概念。

lifetime 可確保 struct 引用的資料有效性跟 struct 本身保持一致。

struct User {
    username: &str,
    email: &str,
    sign_in_count: u64,
    active: bool,
}

fn main() {
    let user1 = User {
        email: "someone@example.com",
        username: "someusername123",
        active: true,
        sign_in_count: 1,
    };
}

編譯錯誤

error[E0106]: missing lifetime specifier
 --> src/main.rs:2:15
  |
2 |     username: &str,
  |               ^ expected lifetime parameter

error[E0106]: missing lifetime specifier
 --> src/main.rs:3:12
  |
3 |     email: &str,
  |            ^ expected lifetime parameter

chap 10 會討論 lifetime,解決這邊的錯誤。

一個使用 struct 的範例

撰寫一個計算長方形面積的程式。

先用基本的變數方式撰寫

fn main() {
    let width1 = 30;
    let height1 = 50;

    println!(
        "The area of the rectangle is {} square pixels.",
        area(width1, height1)
    );
}

fn area(width: u32, height: u32) -> u32 {
    width * height
}

fn area 有兩個參數,但沒有表現出相關的特性,可以改用 tuple 或 struct

這是 tuple 的版本,但沒有明確說明哪一個參數是長,哪一個是寬

fn main() {
    let rect1 = (30, 50);

    println!(
        "The area of the rectangle is {} square pixels.",
        area(rect1)
    );
}

fn area(dimensions: (u32, u32)) -> u32 {
    dimensions.0 * dimensions.1
}

用 struct 改寫

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    println!(
        "The area of the rectangle is {} square pixels.",
        area(&rect1)
    );
}

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}

利用 derived traits 增加功能

以下程式,在列印 Rectangle 時會發生編譯 error

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    println!("rect1 is {}", rect1);
}

編譯錯誤

error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
 --> src/main.rs:9:29
  |
9 |     println!("rect1 is {}", rect1);
  |                             ^^^^^ `Rectangle` cannot be formatted with the default formatter
  |
  = help: the trait `std::fmt::Display` is not implemented for `Rectangle`
  = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
  = note: required by `std::fmt::Display::fmt`

println! 巨集裡面的 {},預設是使用 Display 格式,對於 struct 並沒有 Display。錯誤訊息說明,要加上 #[derive(Debug)],並改用 {:?}

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    println!("rect1 is {:?}", rect1);
}

#[derive(Debug)]是利用註解來 derive Debug trait,而 {:?} 是列印的格式,結果為

rect1 is Rectangle { width: 30, height: 50 }    

如果改用 {:#?} ,輸出資料就會變成

rect1 is Rectangle {
    width: 30,
    height: 50
}

rust 提供了很多透過 dervie 註解使用的 trait,可謂自訂類別增加一些常用的功能。

Method Syntax

method 跟函數一樣,都適用 fn 宣告,可以有參數及回傳值,但 method 跟 function 不同,在 struct (或是 enum, trait) 的 context 中被定義,第一個參數永遠是 self,就是呼叫該 method 的 struct instance。

Defining Methods

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    // self 是 Rectangle,必須加上 &
    // method 可以取得 self 的 ownership,也可以用 &self 不可變借用,或是 &mut self 可變借用
    // 如果想要在 method 裡面改變資料,就要改用 &mut self
    // 很少會直接寫成 self
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}

where's the -> operator ?

C/C++ 有兩種方式呼叫 method, . 直接在 object 上呼叫 method 或是 -> 在 object 的 pointer 呼叫 method。這邊要先了解 dereference pointer。假設 object 是 pointer,那麼 object->something() 就跟 (*object).somethong() 一樣。

object.something() 呼叫 method 時,rust 會自動為 object 加上 &, &mut* ,以便讓 object 跟 method signature 匹配。

這兩種寫法是一樣的

p1.distance(&p2);
(&p1).distance(&p2);

自動引用是因為 method 有明確的接收者 self,在提供了接收者與 method 名稱條件下,rust 可計算出要使用 &self, &mut self 或是 self

Methods with More Parameters

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    // 取得另一個矩形的 immutable reference,只讀取資料,不需要寫入
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };
    let rect2 = Rectangle { width: 10, height: 40 };
    let rect3 = Rectangle { width: 60, height: 45 };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

Associated Functions

impl 區塊的另一個功能:允許在 impl 中定義,不使用 self 為參數的函數,這種函數稱為 assoicated function。因他們與 struct 結構相關,是函數而不是 method,因為他們不作用於一個 struct 的 instance。

產生 struct 新 instance 的 factory function,常常是用 associated function 實作。

接受一個參數,產生正方形 Rectangle

impl Rectangle {
    fn square(size: u32) -> Rectangle {
        Rectangle { width: size, height: size }
    }
}

:: 呼叫 associated function,例如 let sq = Rectangle::square(3);

Multiple impl blocks

每個 struct 都允許有多個 impl blocks

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

References

The Rust Programming Language

中文版

中文版 2

沒有留言:

張貼留言