Rust 入門 (五)

定義並介紹結構體

結構體和咱們前面學習的元組相似,結構體中的每一項均可以是不一樣的數據類型。和元組不一樣的地方在於,咱們須要給結構體的每一項命名。結構體較元組的優點是:咱們聲明和訪問數據項的時候沒必要使用索引,能夠直接使用名字。函數

聲明結構體

咱們直接看一個結構體的例子:學習

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

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

結構體實例化

咱們聲明瞭一個結構體後,如何使用它呢?接下來建立一個結構體的實例:ui

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

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

結構體數據的存取

建立告終構體實例,那咱們應該如何存取實例中的數據呢?好比咱們要獲取郵箱信息,能夠 user1.email 獲取郵箱內容,若是實例是可變的,咱們能夠直接給它賦值。直接看個賦值的例子吧:debug

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 不容許咱們只把部分字段標記爲可變。那咱們可不能夠把這個結構體放在函數中,讓函數返回一個新的實例呢?看例子:code

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

在這個例子中,函數參數名字和結構體字段名字是相 的,若是有不少字段,一個一個地寫名字和參數是很無聊的,不過,rust 爲咱們提供了簡寫的方式orm

結構體字段初始化簡寫

咱們直接看例子吧:索引

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

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

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

相關文章
相關標籤/搜索