动态

详情 返回 返回

The Rust Programming Language 學習 (四) - 动态 详情

結構體

struct,或者 structure,是一個自定義數據類型,允許你命名和包裝多個相關的值,從而形成一個有意義的組合。如果你熟悉一門面向對象語言,struct 就像對象中的數據屬性。

定義並實例化結構體

和元組一樣,結構體的每一部分可以是不同類型。但不同於元組,結構體需要命名各部分數據以便能清楚的表明其值的意義。由於有了這些名字,結構體比元組更靈活:不需要依賴順序來指定或訪問實例中的值。

定義結構體,需要使用 struct 關鍵字併為整個結構體提供一個名字。結構體的名字需要描述它所組合的數據的意義。接着,在大括號中,定義每一部分數據的名字和類型,我們稱為 字段(field)。如下例子:

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

要在定義結構體後使用它,我們可以通過為每個字段指定具體值的方式來創建該結構體的實例。創建一個實例需要以結構體的名字開頭,接着在大括號中使用 key: value 鍵-值對的形式提供字段,其中 key 是字段的名字,value 是需要存儲在字段中的數據值。實例中字段的順序不需要和它們在結構體中聲明的順序一致。換句話説,結構體的定義就像一個類型的通用模板,而實例則會在這個模板中放入特定數據來創建這個類型的值,如下:

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

為了從結構體中獲取某個特定的值,可以使用點號。如果我們只想要用户的郵箱地址,可以用 user1.email。要更改結構體中的值,如果結構體的實例是可變的,我們可以使用點號併為對應的字段賦值如下:

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

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

注意整個實例必須是可變的;Rust 並不允許只將某個字段標記為可變。另外需要注意同其他任何表達式一樣,我們可以在函數體的最後一個表達式中構造一個結構體的新實例,來隱式地返回這個實例。

注意整個實例必須是可變的;Rust 並不允許只將某個字段標記為可變。另外需要注意同其他任何表達式一樣,我們可以在函數體的最後一個表達式中構造一個結構體的新實例,來隱式地返回這個實例。

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

fn main() {
    let user1 = build_user(
        String::from("someone@example.com"),
        String::from("someusername123"),
    );
}

為函數參數起與結構體字段相同的名字是可以理解的,但是不得不重複 email 和 username 字段名稱與變量有些囉嗦。如果結構體有更多字段,重複每個名稱就更加煩人了。幸運的是,有一個方便的簡寫語法!就像這樣:

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

這裏我們創建了一個新的 User 結構體實例,它有一個叫做 email 的字段。我們想要將 email 字段的值設置為 build_user 函數 email 參數的值。因為 email 字段與 email 參數有着相同的名稱,則只需編寫 email 而不是 email: email

使用結構體更新語法從其他實例創建實例

使用舊實例的大部分值但改變其部分值來創建一個新的結構體實例通常很有用。這可以通過結構體更新語法(struct update syntax)實現。

使用結構體更新語法,我們可以通過更少的代碼來達到相同的效果,如示例所示。.. 語法指定了剩餘未顯式設置值的字段應有與給定實例對應字段相同的值。

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

fn main() {
    // --snip--

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

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

創建了一個新實例 user2,該實例具有不同的 email 值,但 username、 active 和 sign_in_count 字段的值與 user1 相同。..user1 必須放在最後,以指定其餘的字段應從 user1 的相應字段中獲取其值,但我們可以選擇以任何順序為任意字段指定值,而不用考慮結構體定義中字段的順序

在這個例子中,我們在創建 user2 後不能再使用 user1,因為 user1 的 username 字段中的 String 被移到 user2 中。如果我們給 user2 的 email 和 username 都賦予新的 String 值,從而只使用 user1 的 active 和 sign_in_count 值,那麼 user1 在創建 user2 後仍然有效。active 和 sign_in_count 的類型是實現 Copy trait 的類型.這裏簡單點理解就是username和email兩個string類型涉及到了移動的問題,而active是布爾類型sing_in_count是整數類型這兩個類型都是在編譯期就確定下來的類型不涉及到移動的問題.

使用沒有命名字段的元組結構體來創建不同的類型

也可以定義與元組類似的結構體,稱為元組結構體(tuple struct)。

元組結構體有着結構體名稱提供的含義,但沒有具體的字段名,只有字段的類型。當你想給整個元組取一個名字,並使元組成為與其他元組不同的類型時,元組結構體是很有用的,這時像常規結構體那樣為每個字段命名就顯得多餘和形式化了.

要定義元組結構體,以 struct 關鍵字和結構體名開頭並後跟元組中的類型。例如,下面是兩個分別叫做 Color 和 Point 元組結構體的定義和用法:

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

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

注意 black 和 origin 值的類型不同,因為它們是不同的元組結構體的實例。你定義的每一個結構體有其自己的類型,即使結構體中的字段有着相同的類型。例如,一個獲取 Color 類型參數的函數不能接受 Point 作為參數,即便這兩個類型都由三個 i32 值組成。在其他方面,元組結構體實例類似於元組:可以將其解構為單獨的部分,也可以使用 . 後跟索引來訪問單獨的值,等等

沒有任何字段的類單元結構體

我們也可以定義一個沒有任何字段的結構體!它們被稱為類單元結構體(unit-like structs),因為它們類似於 (),即“元組類型”一節中提到的 unit 類型。類單元結構體常常在你想要在某個類型上實現 trait 但不需要在類型中存儲數據的時候發揮作用。

下面是一個聲明和實例化一個名為 AlwaysEqual 的 unit 結構的例子。

struct AlwaysEqual;
fn main() {
    let subject = AlwaysEqual;
}

結構體數據所有權

可以使結構體存儲被其他對象擁有的數據的引用,不過這麼做的話需要用上生命週期(lifetime),這是一個第 10 章會討論的 Rust 功能。
看下面例子:

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

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

第 10 章會講到如何修復這個問題以便在結構體中存儲引用,不過現在,我們會使用像 String 這類擁有所有權的類型來替代 &str 這樣的引用以修正這個錯誤。

使用結構體

為了理解何時會需要使用結構體,讓我們編寫一個計算長方形面積的程序。

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
}

通過派生 trait 增加實用功能
如果能夠在調試程序時打印出 Rectangle 實例來查看其所有字段的值就更好了

println! 宏能處理很多類型的格式,不過,{} 默認告訴 println! 使用被稱為 Display 的格式:意在提供給直接終端用户查看的輸出。目前為止見過的基本類型都默認實現了 Display,因為它就是向用户展示 1 或其他任何基本類型的唯一方式。不過對於結構體,println! 應該用來輸出的格式是不明確的,因為這有更多顯示的可能性:是否需要逗號?需要打印出大括號嗎?所有字段都應該顯示嗎?由於這種不確定性,Rust 不會嘗試猜測我們的意圖,所以結構體並沒有提供一個 Display 實現。

{} 中加入 :? 指示符告訴 println! 我們想要使用叫做 Debug 的輸出格式。Debug 是一個 trait,它允許我們以一種對開發者有幫助的方式打印結構體,以便當我們調試代碼時能看到它的值。

我們必須為結構體顯式選擇這個功能。為此,在結構體定義之前加上外部屬性 #[derive(Debug)]

#[derive(Debug)]
struct Rectangle{
    width: u32,
    height: u32,
}
fn main() {
    let rect1 = Rectangle{width: 30, height: 50};
    println!("將結構體重的數據打印出來{:?}",rect1);
    println!("長方型的面積是{}",area(rect1));
}
fn area(rectangle: Rectangle) -> u32{
    rectangle.width * rectangle.height
}

這並不是最漂亮的輸出,不過它顯示這個實例的所有字段,毫無疑問這對調試有幫助。當我們有一個更大的結構體時,能有更易讀一點的輸出就好了,為此可以使用 {:#?} 替換 println! 字符串中的 {:?}

另一種使用 Debug 格式打印數值的方法是使用 dbg! 宏。dbg! 宏接收一個表達式的所有權,打印出代碼中調用 dbg! 宏時所在的文件和行號,以及該表達式的結果值,並返回該值的所有權

註釋:與 println! 宏打印到標準輸出控制流(stdout)不同,調用 dbg! 宏會打印到標準錯誤控制流(stderr)。

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

fn main() {
    let scale = 2;
    let rect1 = Rectangle {
        width: dbg!(30 * scale),
        height: 50,
    };

    dbg!(&rect1);
}
[src/main.rs:10:16] 30 * scale = 60
[src/main.rs:14:5] &rect1 = Rectangle {
    width: 60,
    height: 50,
}

我們可以把 dbg! 放在表達式 30 * scale 周圍,因為 dbg! 返回表達式的值的所有權,所以 width 字段將獲得相同的值,就像我們在那裏沒有 dbg! 調用一樣。我們不希望 dbg! 擁有 rect1 的所有權,所以我們在下一次調用 dbg! 時傳入一個引用.

除了 Debug trait,Rust 還為我們提供了很多可以通過 derive 屬性來使用的 trait,他們可以為我們的自定義類型增加實用的行為。

方法

方法 與函數類似:它們使用 fn 關鍵字和名稱聲明,可以擁有參數和返回值,同時包含在某處調用該方法時會執行的代碼。不過方法與函數是不同的,因為它們在結構體的上下文中被定義(或者是枚舉或 trait 對象的上下文),並且它們第一個參數總是 self,它代表調用該方法的結構體實例。

定義方法

#[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!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}

為了使函數定義於 Rectangle 的上下文中,我們開始了一個 impl 塊(impl 是 implementation 的縮寫),這個 impl 塊中的所有內容都將與 Rectangle 類型相關聯。接着將 area 函數移動到 impl 大括號中,並將簽名中的第一個(在這裏也是唯一一個)參數和函數體中其他地方的對應參數改成 self。然後在 main 中將我們先前調用 area 方法並傳遞 rect1 作為參數的地方,改成使用方法語法(method syntax)在 Rectangle 實例上調用 area 方法。方法語法獲取一個實例並加上一個點號,後跟方法名、圓括號以及任何參數。

在 area 的簽名中,使用 &self 來替代 rectangle: &Rectangle&self 實際上是 self: &Self 的縮寫。在一個 impl 塊中,Self 類型是 impl 塊的類型的別名。方法的第一個參數必須有一個名為 self 的Self 類型的參數,所以 Rust 讓你在第一個參數位置上只用 self 這個名字來縮寫。注意,我們仍然需要在 self 前面使用 & 來表示這個方法借用了 Self 實例,就像我們在 rectangle: &Rectangle 中做的那樣。方法可以選擇獲得 self 的所有權,或者像我們這裏一樣不可變地借用 self,或者可變地借用 self,就跟其他參數一樣。

這裏選擇 &self 的理由跟在函數版本中使用 &Rectangle 是相同的:我們並不想獲取所有權,只希望能夠讀取結構體中的數據,而不是寫入。如果想要在方法中改變調用方法的實例,需要將第一個參數改為 &mut self。通過僅僅使用 self 作為第一個參數來使方法獲取實例的所有權是很少見的;這種技術通常用在當方法將 self 轉換成別的實例的時候,這時我們想要防止調用者在轉換之後使用原始的實例

使用方法替代函數,除了可使用方法語法和不需要在每個函數簽名中重複 self 的類型之外,其主要好處在於組織性。我們將某個類型實例能做的所有事情都一起放入 impl 塊中,而不是讓將來的用户在我們的庫中到處尋找 Rectangle 的功能。

請注意,我們可以選擇將方法的名稱與結構中的一個字段相同。例如,我們可以在 Rectangle 上定義一個方法,並命名為 width:

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

impl Rectangle {
    fn width(&self) -> bool {
        self.width > 0
    }
}

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

    if rect1.width() {
        println!("The rectangle has a nonzero width; it is {}", rect1.width);
    }
}

在這裏,我們選擇讓 width 方法的行為是如果實例的 width 字段的值大於 0,返回 true。如果該值為 0,則返回 false:我們可以在同名的方法中使用一個字段來達到任何目的。在 main 中,當我們在 rect1.width 後面加上括號時。Rust 知道我們指的是方法 width。當我們不使用圓括號時,Rust 知道我們指的是字段 width。

通常,但並不總是如此,與字段同名的方法將被定義為只返回字段中的值,而不做其他事情。這樣的方法被稱為 getters,Rust 並不像其他一些語言那樣為結構字段自動實現它們。Getters 很有用,因為你可以把字段變成私有的,但方法是公共的,這樣就可以把對字段的只讀訪問作為該類型公共 API 的一部分。

在 C/C++ 語言中,有兩個不同的運算符來調用方法:. 直接在對象上調用方法,而 -> 在一個對象的指針上調用方法,這時需要先解引用(dereference)指針。換句話説,如果 object 是一個指針,那麼 object->something() 就像 (*object).something() 一樣。

Rust 並沒有一個與 -> 等效的運算符;相反,Rust 有一個叫 自動引用和解引用(automatic referencing and dereferencing)的功能。方法調用是 Rust 中少數幾個擁有這種行為的地方。

它是這樣工作的:當使用 object.something() 調用方法時,Rust 會自動為 object 添加 &&mut* 以便使 object 與方法簽名匹配。也就是説,這些代碼是等價的:

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

第一行看起來簡潔的多。這種自動引用的行為之所以有效,是因為方法有一個明確的接收者———— self 的類型。在給出接收者和方法名的前提下,Rust 可以明確地計算出方法是僅僅讀取(&self),做出修改(&mut self)或者是獲取所有權(self)。事實上,Rust 對方法接收者的隱式借用讓所有權在實踐中更友好。

帶有更多參數的方法

讓我們通過實現 Rectangle 結構體上的另一方法來練習使用方法。這回,我們讓一個 Rectangle 的實例獲取另一個 Rectangle 實例,如果 self 能完全包含第二個長方形則返回 true;否則返回 false。一旦定義了 can_hold 方法,

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

在方法簽名中,可以在 self 後增加多個參數,而且這些參數就像函數中的參數一樣工作。

關聯函數

所有在 impl 塊中定義的函數被稱為關聯函數(associated function),因為它們與 impl 後面命名的類型相關。我們可以定義不以 self 為第一參數的關聯函數(因此不是方法),因為它們並不作用於一個結構體的實例。我們已經使用了一個這樣的函數,String::from 函數,它是在 String 類型上定義的。

關聯函數經常被用作返回一個結構體新實例的構造函數。例如我們可以提供一個關聯函數,它接受一個維度參數並且同時作為寬和高,這樣可以更輕鬆的創建一個正方形 Rectangle 而不必指定兩次同樣的值:

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

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

fn main() {
    let sq = Rectangle::square(3);
}

使用結構體名和 :: 語法來調用這個關聯函數:比如 let sq = Rectangle::square(3);。這個方法位於結構體的命名空間中::: 語法用於關聯函數和模塊創建的命名空間。

多個impl塊

每個結構體都允許擁有多個 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
    }
}
user avatar ssbunny 头像 fengdudeyema 头像 decaday 头像 f702 头像 dongdong_chainwiseweb3 头像 idiomeo 头像 guishangguandao 头像 shenchendemaoyi 头像 junyidedalianmao 头像 qiyuxuanangdelvdou 头像 liy 头像 czy 头像
点赞 16 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.