Rust 入門 (五)


定義並介紹結構體

結構體和我們前面學習的元組類似,結構體中的每一項都可以是不同的數據類型。和元組不同的地方在於,我們需要給結構體的每一項命名。結構體較元組的優勢是:我們聲明和訪問數據項的時候不必使用索引,可以直接使用名字。

聲明結構體

我們直接看一個結構體的例子:

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

結構體使用關鍵字 struct 開頭,緊跟結構體的名字,之后就是大括號包裹的多條結構體數據項,每個數據項由名字和類型組成,我們把每個數據項稱為字段。

結構體實例化

我們聲明了一個結構體后,如何使用它呢?接下來創建一個結構體的實例:

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

可以看到,創建結構體實例 (結構體實例化) 是直接以結構體名字開頭,之后就是大括號包裹的鍵值對。這些鍵值對順序和聲明結構體的順序無關,換句話說,聲明結構體就是定義一個通用的模版,結構體實例化就是給模版填充值。

結構體數據的存取

創建了結構體實例,那我們應該如何存取實例中的數據呢?比如我們要獲取郵箱信息,可以 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");

這個實例整個都是可變的,如果我只想修改 email 和 username 兩個字段,而不想修改其它的字體,應該怎么辦呢?

修改部分字段

要知道,rust 不允許我們只把部分字段標記為可變。那我們可不可以把這個結構體放在函數中,讓函數返回一個新的實例呢?看例子:

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

在這個例子中,函數參數名字和結構體字段名字是相 的,如果有很多字段,一個一個地寫名字和參數是很無聊的,不過,rust 為我們提供了簡寫的方式

結構體字段初始化簡寫

我們直接看例子吧:

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

email 和 username 的結構體字段名字和函數傳入的參數變量的名字是相同的,我們可以只寫一遍。

結構體更新

如果舊的結構體實例中的一部分值修改,使之變成一個新的實例,使用結構體更新語法會更加方便。如果在 user1 的基礎上修改 email 和 username 而不改變其他的值,我們通常會這樣寫:

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
};

利用 .. 語法達到了和前面案例相同的結果。

元組結構體

我們也可以定義看起來像元組的結構體,我們稱它為元組結構體。元組結構體只定義字段的類型而不定義字段的名字,它主要用於給整個元組一個名字來區分不同的元組。直接看例子:

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

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

定義了黑色和原點,顯然二者的類型不相同,雖然聲明的字段類型相同,但是二者是使用不同的元組結構體實例化的。

空結構體

我們也可以定義空結構體,它不包含任何字段。詳細內容后文再聊。

寫個關於結構體的例子

為了學習什么時候用到結構體,我們來寫一個計算長方形面積的程序。我們從使用簡單變量寫起,一直寫到使用結構體為止。

編寫項目

我們先來創建一個名叫 rectangles 的項目,然后寫一個通過寬高計算面積的函數。

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

    println!(
        "長方形的面積是 {}。",
        area(width1, height1)
    );
}

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

我們運行的結果是:

cargo run
   Compiling rectangles v0.1.0 (/Users/shanpengfei/work/rust-work-space/study/rectangles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.51s
     Running `target/debug/rectangles`
長方形的面積是 1500。

我們調用 area 函數完成了對長方形面積的計算,但是描述長方形的寬高是分開的,我們能不能想個辦法,把兩個值變成一個值?很容易想到的辦法就是元組,對,沒錯,是元組。

利用元組重構項目

我們直接看重構完成后的代碼:

fn main() {
    let rect1 = (30, 50); // 定義元組

    println!(
        "長方形的面積是 {}。",
        area(rect1)
    );
}

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

現在只有一個值了,但是又有了一個新的問題:元組沒有名字,計算面積還好,元組中的兩個值混了也沒事,如果是把這個長方形畫出來,那就得記着 0 位置是寬,1 位置是高,別人調用我們的代碼時,別人也得記着這個順序。這是很不友好的,那應該怎么解決呢?

利用結構體重構項目

我看來看重構后的代碼:

struct Rectangle { // 定義結構體
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 }; // 結構體實例化

    println!(
        "長方形的面積是 {}。",
        area(&rect1)
    );
}

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

現在只有一個參數了,而且參數也有了實際的含義了,似乎完成了我們的目標。但是 area 函數只能計算長方形的面積,我們希望這個函數盡可能地在 Rectangle 結構體內部,因為它不能處理其它的結構體。那我們應該如果做呢?我們可以把該函數轉變成 Rectangle 結構體的方法。在此之前,我們先看一個調試程序的小技巧。

打印結構體

在我們調試程序的時候,經常想看一下結構體每個字段的值是什么,如果直接打印結構體會報錯,比如:

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

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

    println!("rect1 是: {}", rect1); // 報錯: `Rectangle` cannot be formatted with the default formatter
}

rust 為我們提供了打印的方法,在結構體定義的上方加入 #[derive(Debug)] 聲明,在打印的大括號中加入 :? 就可以了,看例子:

#[derive(Debug)]    // 這里加入聲明
struct Rectangle {
    width: u32,
    height: u32,
}

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

    println!("rect1 是: {:?}", rect1); // 這里加入打印的格式
}

運行的結果是:

 cargo run
   Compiling rectangles v0.1.0 (/Users/shanpengfei/work/rust-work-space/study/rectangles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.26s
     Running `target/debug/rectangles`
rect1 是: Rectangle { width: 30, height: 50 }

這個結構體的輸出很不美觀,我們調整一下,讓結構體可以結構化輸出,只需要把 {:?} 改成 {:#?} 即可,然后輸出就變成了:

cargo run
   Compiling rectangles v0.1.0 (/Users/shanpengfei/work/rust-work-space/study/rectangles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.26s
     Running `target/debug/rectangles`
rect1 是: Rectangle {
    width: 30,
    height: 50,
}

后文會詳細介紹 derive 聲明。

結構體方法

方法和函數類似,都是以關鍵字 fn 打頭,后接方法名、參數和返回值,最后是方法體。方法和函數不同之處在於:方法定義在結構體的上下文中,方法的第一個參數是 self (self 代表結構體方法調用者的實例)。

定義方法

我們來修改 area 方法,把它變成 Rectangle 結構體的方法,如下:

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

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


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

    println!(
        "長方形的面積是 {}。",
        &rect1.area()
    );
}

使用 impl 關鍵字和結構體名字 Rectangle 來定義 Rectangle 的上下文,然后把 area 函數放進去,就把函數的第一個參數修改成 self,在主函數中,可以讓 rect1 實例直接調用 area 方法。


對於方法和第一個參數,我們使用 &self 來代替 rectangle: &Rectangle,因為在 impl Rectangle 聲明的上下文中,rust 知道 self 代表的是 Rectangle,但是還需要在 self 前面加上 & 符號,意思是借用 Rectangle 的不可變實例。如果要借用 Rectangle 的可變實例,參數需要寫成 &mut self。


使用方法較函數的優勢在於:添加方法的時候可以直接使用結構體方法語法,而不必要在每個函數中重復寫實例的類型。我們可以把結構體對應的所有方法都寫在結構體的上下文中,當我們為別人提供庫函數的時候不必要在很多地方尋找需要的函數。

多參方法

我們再寫一個例子,第一個長方形能不能裝下第二個長方形,如果能裝下,就返回 true,否則返回 false,實例代碼如下:

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

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

    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!("rect1 裝下 rect2? {}", rect1.can_hold(&rect2));
    println!("rect1 裝下 rect3? {}", rect1.can_hold(&rect3));
}

我們先來看運行結果:

rect1 裝下 rect2? true
rect1 裝下 rect3? false

我們在 Rectangle 上下文中定義第二個方法 can_hold,該方法借用另一個 Rectangle 實例作參數,我們需要告訴調用者參數的類型。

關聯函數

在結構體上下文中也可以定義不含有 self 參數的函數,這種函數被稱為關聯函數。這里叫函數,不叫方法,因為這些函數不是使用結構體實例調用的,而是使用雙冒號調用,比如之前使用的 String::from 就是一個關聯函數。


關聯函數常常被用於返回結構體實例的構造函數。例如,我們可以提供一個關聯函數來生產 Rectangle 結構體的實例。在這里,我們假設寬度是相等的,我們就可以使用一個參數來代替兩個參數了,如下:

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

使用雙冒號 :: 語法來調用關聯函數,舉個簡單的例子 let sq = Rectangle::square(3);,即 結構體::關聯函數

多個 impl 模塊

每個結構體都允許使用多個 impl 聲明的上下文,例如:

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
    }
}

這種語法是有效的,后文學習中可能會使用到,知道有這種語法就好了。

歡迎閱讀單鵬飛的學習筆記


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM