Rust 語言學習之旅

banq發表於2022-09-29

本教程旨在循序漸進地介紹 Rust 程式語言的特性。大家通常認為 Rust 是一門學習曲線陡峭的語言。我希望在此說明,在我們開始學習複雜的部分之前,還有很多東西需要探索。
點選標題


模組
每個 Rust 程式或者庫都叫 crate。
每個 crate 都是由模組的層次結構組成。
每個 crate 都有一個根模組。
模組裡面可以有全域性變數、全域性函式、全域性結構體、全域性 Trait 甚至是全域性模組!
在 Rust 中,檔案與模組樹的層次結構並不是一對一的對映關係。我們必須在我們的程式碼中手動構建模組樹。


編寫程式
應用程式的根模組需要在一個叫 main.rs 的檔案裡面。

編寫庫
庫的根模組需要在一個叫 lib.rs 的檔案裡面。

引用其他模組和 crate
你可以使用完整的模組路徑路徑引用模組中的專案: std::f64::consts::PI。
更簡單的方法是使用use關鍵字。此關鍵字可以讓我們在程式碼中使用模組中的專案而無需指定完整路徑。例如 use std::f64::consts::PI 這樣我在 main 函式中只需要寫 PI 就可以了。
std 是 Rust 的標準庫。這個庫中包含了大量有用的資料結構和與作業系統互動的函式。
由社群建立的 crate 的搜尋索引可以在這裡找到: https://crates.io.

引用多個專案
在同一個模組路徑中可以引用多個專案,比如:
use std::f64::consts::{PI,TAU}

建立模組
當我們想到專案時,我們通常會想象一個以目錄組織的檔案層次結構。Rust 允許您建立與您的檔案結構密切相關的模組。
在 Rust 中,有兩種方式來宣告一個模組。例如,模組 foo 可以表示為:

  • 一個名為 foo.rs 的檔案。
  • 在名為 foo 的目錄,裡面有一個叫 mod.rs 檔案。


模組可以互相依賴。要建立一個模組和其子模組之間的關係,你需要在父模組中這樣寫:
mod foo;
上面的宣告將使編譯器尋找一個名為 foo.rs或 foo/mod.rs 的檔案,並將其內容插入這個作用域內名為 foo 的模組中。

內聯模組
一個子模組可以直接內聯在一個模組的程式碼中。

內聯模組最常見的用途是建立單元測試。 下面我們建立一個只有在使用 Rust 進行測試時才會存在的內聯模組!

// 當 Rust 不在測試模式時,這個宏會刪除這個內聯模組。
#[cfg(test)]
mod tests {
    // 請注意,我們並不能立即獲得對父模組的訪問。我們必須顯式地匯入它們。
    use super::*;

    ... 單元測試寫在這裡 ...
}


模組內部引用
你可以在你的 use 路徑中使用如下 Rust 關鍵字來獲得你想要的模組:

  • crate - 你的 crate 的根模組
  • super - 當前模組的父模組
  • self - 當前模組


匯出
預設情況下,模組的成員不能從模組外部訪問(甚至它的子模組也不行!)。 我們可以使用 pub 關鍵字使一個模組的成員可以從外部訪問。
預設情況下,crate 中的成員無法從當前 crate 之外訪問。我們可以透過在根模組中 (lib.rs 或 main.rs), 將成員標記為 pub 使它們可以訪問。


變數
變數使用 let 關鍵字來宣告。
在賦值時,Rust 能夠在 99% 的情況下自動推斷其型別。如果不能,你也可以手動將型別新增到變數宣告中。
你也許注意到了,我們可以對同一個變數名進行多次賦值。這就是所謂的變數隱藏,可以更改變數型別以實現對該變數名的後續使用。
變數名總是遵循 蛇形命名法 (snake_case)。

fn main() {
    // rust 推斷出x的型別
    let x = 13;
    println!("{}", x);

    // rust也可以顯式宣告型別
    let x: f64 = 3.14159;
    println!("{}", x);

    // rust 也支援先宣告後初始化,但很少這樣做
    let x;
    x = 0;
    println!("{}", x);
}



可變變數
Rust 非常關心哪些變數是可修改的可改變。值分為兩種型別:

  • 可變的 - 編譯器允許對變數進行讀取和寫入。
  • 不可變的 - 編譯器只允許對變數進行讀取。

可變值用 mut 關鍵字表示。

fn main() {
    let mut x = 42;
    println!("{}", x);
    x = 13;
    println!("{}", x);
}


基本型別
Rust 有多種常見的型別:

  • 布林型 - bool 表示 true 或 false
  • 無符號整型- u8 u32 u64 u128 表示正整數
  • 有符號整型 - i8 i32 i64 i128 表示正負整數
  • 指標大小的整數 - usize isize 表示記憶體中內容的索引和大小
  • 浮點數 - f32 f64
  • 元組(tuple) - (value, value, ...) 用於在棧上傳遞固定序列的值
  • 陣列 - 在編譯時已知的具有固定長度的相同元素的集合
  • 切片(slice) - 在執行時已知長度的相同元素的集合
  • str(string slice) - 在執行時已知長度的文字

你也可以透過將型別附加到數字的末尾來明確指定數字型別(如 13u32 和 2u8)

fn main() {
    let x = 12; // 預設情況下,這是i32
    let a = 12u8;
    let b = 4.3; // 預設情況下,這是f64
    let c = 4.3f32;
    let bv = true;
    let t = (13, false);
    let sentence = "hello world!";
    println!(
        "{} {} {} {} {} {} {} {}",
        x, a, b, c, bv, t.0, t.1, sentence
    );
}


用 as 關鍵字,Rust 使數字型別轉換非常容易:

fn main() {
    let a = 13u8;
    let b = 7u32;
    let c = a as u32 + b;
    println!("{}", c);

    let t = true;
    println!("{}", t as u8);
}



常量
常量允許我們高效地指定一個在程式碼中會被多次使用的公共值。不同於像變數一樣在使用的時候會被複制,常量會在編譯期間直接用它們的值來替換變數的文字識別符號。
不同於變數,常量必須始終具有顯式的型別。
常量名總是遵循 全大寫蛇形命名法(SCREAMING_SNAKE_CASE)。

const PI: f32 = 3.14159;

fn main() {
    println!(
        "To make an apple {} from scratch, you must first create a universe.",
        PI
    );
}


陣列
陣列是所有相同型別資料元素的固定長度集合。
一個陣列的資料型別是 [T;N],其中 T 是元素的型別,N 是編譯時已知的固定長度。
可以使用 [x] 運算子提取單個元素,其中 x 是所需元素的 usize 索引(從 0 開始)。

fn main() {
    let nums: [i32; 3] = [1, 2, 3];
    println!("{:?}", nums);
    println!("{}", nums[1]);
}


函式
函式可以有 0 個或者多個引數。
在這個例子中,add 接受型別為 i32(32 位長度的整數)的兩個引數。
函式名總是遵循 蛇形命名法 (snake_case)。

fn add(x: i32, y: i32) -> i32 {
    return x + y;
}

fn main() {
    println!("{}", add(42, 13));
}


多個返回值
函式可以透過元組來返回多個值。
元組元素可以透過他們的索引來獲取。
Rust 允許我們將後續會看到的各種形式的解構,也允許我們以符合邏輯的方式提取資料結構的子片段。

fn swap(x: i32, y: i32) -> (i32, i32) {
    return (y, x);
}

fn main() {
    // 返回一個元組
    let result = swap(123, 321);
    println!("{} {}", result.0, result.1);

    // 將元組解構為兩個變數
    let (a, b) = swap(result.0, result.1);
    println!("{} {}", a, b);
}


返回空值
如果沒有為函式指定返回型別,它將返回一個空的元組,也稱為單元。
一個空的元組用 () 表示。
直接使用 () 的情況相當不常見。但它經常會出現(比如作為函式返回值),所以瞭解其來龍去脈非常重要。

fn make_nothing() -> () {
    return ();
}

// 返回型別隱含為 ()
fn make_nothing2() {
    // 如果沒有指定返回值,這個函式將會返回 ()
}

fn main() {
    let a = make_nothing();
    let b = make_nothing2();

    // 列印a和b的debug字串,因為很難去列印空
    println!("The value of a: {:?}", a);
    println!("The value of b: {:?}", b);
}



if/else if/else
Rust 中的程式碼分支不足為奇。
Rust 的條件判斷沒有括號!~~需要括號幹什麼。~~我們現有的邏輯就看起來就很乾淨整潔呀。
不過呢,所有常見的邏輯運算子仍然適用:==,!=, <, >, <=, >=, !,

fn main() {
    let x = 42;
    if x < 42 {
        println!("less than 42");
    } else if x == 42 {
        println!("is 42");
    } else {
        println!("greater than 42");
    }
}


迴圈
需要一個無限迴圈?
使用 Rust 很容易實現。
break 會退出當前迴圈。

fn main() {
    let mut x = 0;
    loop {
        x += 1;
        if x == 42 {
            break;
        }
    }
    println!("{}", x);
}


loop 可以被中斷以返回一個值。

fn main() {
    let mut x = 0;
    let v = loop {
        x += 1;
        if x == 13 {
            break "found the 13";
        }
    };
    println!("from loop: {}", v);
}


while
while 允許你輕鬆地向迴圈新增條件。
如果條件一旦變為 false,迴圈就會退出。

fn main() {
    let mut x = 0;
    while x != 42 {
        x += 1;
    }
}



for
Rust 的 for 迴圈是一個強大的升級。它遍歷來自計算結果為迭代器的任意表示式的值。 迭代器是什麼?迭代器是一個你可以一直詢問“下一項是什麼?”直到沒有其他項的物件。
我們將在以後的章節中進一步探討這一點,與此同時,我們知道 Rust 使建立生成整數序列的迭代器變得容易。
.. 運算子建立一個可以生成包含起始數字、但不包含末尾數字的數字序列的迭代器。
..= 運算子建立一個可以生成包含起始數字、且包含末尾數字的數字序列的迭代器。

fn main() {
    for x in 0..5 {
        println!("{}", x);
    }

    for x in 0..=5 {
        println!("{}", x);
    }
}



match
想念你的 switch 語句嗎?Rust 有一個非常有用的關鍵字,用於匹配值的所有可能條件, 並在匹配為真時執行相應程式碼。我們先來看看對數字的使用。在未來章節中,我們將有更多 更復雜的資料模式匹配的說明,我向你保證,它將值得等待。
match 是窮盡的,意為所有可能的值都必須被考慮到。
匹配與解構相結合是迄今為止你在 Rust 中看到的最常見的模式之一。

fn main() {
    let x = 42;

    match x {
        0 => {
            println!("found zero");
        }
        // 我們可以匹配多個值
        1 | 2 => {
            println!("found 1 or 2!");
        }
        // 我們可以匹配迭代器
        3..=9 => {
            println!("found a number 3 to 9 inclusively");
        }
        // 我們可以將匹配數值繫結到變數
        matched_num @ 10..=100 => {
            println!("found {} number between 10 to 100!", matched_num);
        }
        // 這是預設匹配,如果沒有處理所有情況,則必須存在該匹配
        _ => {
            println!("found something else!");
        }
    }
}


從塊表示式返回值
if,match,函式,以及作用域塊都有一種返回值的獨特方式。
如果 if、match、函式或作用域塊中的最後一條語句是不帶 ; 的表示式, Rust 將把它作為一個值從塊中返回。這是一種建立簡潔邏輯的好方法,它返回一個 可以放入新變數的值。
注意,它還允許 if 語句像簡潔的三元表示式一樣操作。

fn example() -> i32 {
    let x = 42;
    // Rust的三元表示式
    let v = if x < 42 { -1 } else { 1 };
    println!("from if: {}", v);

    let food = "hamburger";
    let result = match food {
        "hotdog" => "is hotdog",
        // 注意,當它只是一個返回表示式時,大括號是可選的
        _ => "is not hotdog",
    };
    println!("identifying food: {}", result);

    let v = {
        // 這個作用域塊讓我們得到一個不影響函式作用域的結果
        let a = 1;
        let b = 2;
        a + b
    };
    println!("from block: {}", v);

    // 在最後從函式中返回值的慣用方法
    v + 4
}

fn main() {
    println!("from function: {}", example());
}


Struct結構體
一個 struct 就是一些欄位的集合。
欄位是一個與資料結構相關聯的資料值。它的值可以是基本型別或結構體型別。
它的定義就像給編譯器的藍圖,告訴編譯器如何在記憶體中佈局彼此相鄰的欄位。

struct SeaCreature {
    // String 是個結構體
    animal_type: String,
    name: String,
    arms: i32,
    legs: i32,
    weapon: String,
}


方法呼叫
與函式(function)不同,方法(method)是與特定資料型別關聯的函式。類似類中的方法。
靜態方法 — 屬於某個型別,呼叫時使用 :: 運算子。
例項方法 — 屬於某個型別的例項,呼叫時使用 . 運算子。

fn main() {
    // 使用靜態方法來建立一個String例項
    let s = String::from("Hello world!");
    // 使用例項來呼叫方法
    println!("{} is {} characters long.", s, s.len());
}


所有權和資料借用
相較於其他程式語言,Rust 具有一套獨特的記憶體管理範例。為了不讓您被概念性的東西淹沒,我們將一一展示這些編譯器的行為和驗證方式。 有一點很重要:所有這些規則的終極目的不是為了為難您,而是為了更好地降低程式碼的出錯率!

所有權
例項化一個型別並且將其繫結到變數名上將會建立一些記憶體資源,而這些記憶體資源將會在其整個生命週期中被 Rust 編譯器檢驗。 被繫結的變數即為該資源的所有者。

struct Foo {
    x: i32,
}

fn main() {
    // 我們例項化這個結構體並將其繫結到具體的變數上
    // 來建立記憶體資源
    let foo = Foo { x: 42 };
    // foo 即為該資源的所有者
}


基於作用域scope的資源管理
Rust 將使用資源最後被使用的位置或者一個函式域的結束來作為資源被析構和釋放的地方。 此處析構和釋放的概念被稱之為 drop(釋放丟棄)。
記憶體細節:
  • Rust 沒有垃圾回收機制。
  • 在 C++ 中,這被也稱為“資源獲取即初始化“(RAII)。


struct Foo {
    x: i32,
}

fn main() {
    let foo_a = Foo { x: 42 };
    let foo_b = Foo { x: 13 };

    println!("{}", foo_a.x);
    // foo_a 將在這裡被 dropped 因為其在這之後再也沒有被使用

    println!("{}", foo_b.x);
    // foo_b 將在這裡被 dropped 因為這是函式域的結尾
}


釋放是分級進行的
刪除一個結構體時,結構體本身會先被釋放,緊接著才分別釋放相應的子結構體並以此類推。
記憶體細節:
  • Rust 透過自動釋放記憶體來幫助確保減少記憶體洩漏。
  • 每個記憶體資源僅會被釋放一次。


struct Bar {
    x: i32,
}

struct Foo {
    bar: Bar,
}

fn main() {
    let foo = Foo { bar: Bar { x: 42 } };
    println!("{}", foo.bar.x);
    // foo 首先被 dropped 釋放
    // 緊接著是 foo.bar
}


移交所有權
將所有者作為引數傳遞給函式時,其所有權將移交至該函式的引數。 在一次移動後,原函式中的變數將無法再被使用。
記憶體細節:

  • 在移動期間,所有者的堆疊值將會被複制到函式呼叫的引數堆疊中。


struct Foo {
    x: i32,
}

fn do_something(f: Foo) {
    println!("{}", f.x);
    // f 在這裡被 dropped 釋放
}

fn main() {
    let foo = Foo { x: 42 };
    // foo 被移交至 do_something
    do_something(foo);
    // 此後 foo 便無法再被使用
}


歸還所有權
所有權也可以從一個函式中被歸還。

struct Foo {
    x: i32,
}

fn do_something() -> Foo {
    Foo { x: 42 }
    // 所有權被移出
}

fn main() {
    let foo = do_something();
    // foo 成為了所有者
    // foo 在函式作用域結尾被 dropped 釋放
}


使用引用借用所有權
引用允許我們透過 & 運算子來借用對一個資源的訪問許可權。 引用也會如同其他資源一樣被釋放。

struct Foo {
    x: i32,
}

fn main() {
    let foo = Foo { x: 42 };
    let f = &foo;
    println!("{}", f.x);
    // f 在這裡被 dropped 釋放
    // foo 在這裡被 dropped 釋放
}


透過引用借用可變所有權
我們也可以使用 &mut 運算子來借用對一個資源的可變訪問許可權。 在發生了可變借用後,一個資源的所有者便不可以再次被借用或者修改。

記憶體細節:

Rust 之所以要避免同時存在兩種可以改變所擁有變數值的方式,是因為此舉可能會導致潛在的資料爭用(data race)。

struct Foo {
    x: i32,
}

fn do_something(f: Foo) {
    println!("{}", f.x);
    // f 在這裡被 dropped 釋放
}

fn main() {
    let mut foo = Foo { x: 42 };
    let f = &mut foo;

    // 會報錯: do_something(foo);
    // 因為 foo 已經被可變借用而無法取得其所有權

    // 會報錯: foo.x = 13;
    // 因為 foo 已經被可變借用而無法被修改

    f.x = 13;
    // f 會因為此後不再被使用而被 dropped 釋放
    
    println!("{}", foo.x);
    
    // 現在修改可以正常進行因為其所有可變引用已經被 dropped 釋放
    foo.x = 7;
    
    // 移動 foo 的所有權到一個函式中
    do_something(foo);
}


解引用
使用 &mut 引用時, 你可以透過 * 運算子來修改其指向的值。 你也可以使用 * 運算子來對所擁有的值進行複製(前提是該值可以被複製——我們將會在後續章節中討論可複製型別)。

fn main() {
    let mut foo = 42;
    let f = &mut foo;
    let bar = *f; // 取得所有者值的複製
    *f = 13;      // 設定引用所有者的值
    println!("{}", bar);
    println!("{}", foo);
}


傳遞借用的資料
Rust 對於引用的規則也許最好用以下的方式總結:

  • Rust 只允許同時存在一個可變引用或者多個不可變引用,不允許可變引用和不可變引用同時存在。
  • 一個引用永遠也不會比它的所有者存活得更久。

而在函式間進行引用的傳遞時,以上這些通常都不會成為問題。
記憶體細節:
  • 上面的第一條規則避免了資料爭用的出現。什麼是資料爭用?在對資料進行讀取的時候,資料爭用可能會因為同時存在對資料的寫入而產生不同步。這一點往往會出現在多執行緒程式設計中。
  • 而第二條引用規則則避免了透過引用而錯誤的訪問到不存在的資料(在 C 語言中被稱之為懸垂指標)。


struct Foo {
    x: i32,
}

fn do_something(f: &mut Foo) {
    f.x += 1;
    // 可變引用 f 在這裡被 dropped 釋放
}

fn main() {
    let mut foo = Foo { x: 42 };
    do_something(&mut foo);
    // 因為所有的可變引用都在 do_something 函式內部被釋放了
    // 此時我們便可以再建立一個
    do_something(&mut foo);
    // foo 在這裡被 dropped 釋放
}


引用的引用
引用甚至也可以用在其他引用上。

struct Foo {
    x: i32,
}

fn do_something(a: &Foo) -> &i32 {
    return &a.x;
}

fn main() {
    let mut foo = Foo { x: 42 };
    let x = &mut foo.x;
    *x = 13;
    // x 在這裡被 dropped 釋放從而允許我們再建立一個不可變引用
    let y = do_something(&foo);
    println!("{}", y);
    // y 在這裡被 dropped 釋放
    // foo 在這裡被 dropped 釋放
}


顯式生命週期
儘管 Rust 不總是在程式碼中將它展示出來,但編譯器會理解每一個變數的生命週期並進行驗證以確保一個引用不會有長於其所有者的存在時間。 同時,函式可以透過使用一些符號來引數化函式簽名,以幫助界定哪些引數和返回值共享同一生命週期。 生命週期註解總是以 ' 開頭,例如 'a,'b 以及 'c。

struct Foo {
    x: i32,
}

// 引數 foo 和返回值共享同一生命週期
fn do_something<'a>(foo: &'a Foo) -> &'a i32 {
    return &foo.x;
}

fn main() {
    let mut foo = Foo { x: 42 };
    let x = &mut foo.x;
    *x = 13;
    // x 在這裡被 dropped 釋放從而允許我們再建立一個不可變引用
    let y = do_something(&foo);
    println!("{}", y);
    // y 在這裡被 dropped 釋放
    // foo 在這裡被 dropped 釋放
}


多個生命週期
生命週期註解可以透過區分函式簽名中不同部分的生命週期,來允許我們顯式地明確某些編譯器靠自己無法解決的場景。

struct Foo {
    x: i32,
}

// foo_b 和返回值共享同一生命週期
// foo_a 則擁有另一個不相關聯的生命週期
fn do_something<'a, 'b>(foo_a: &'a Foo, foo_b: &'b Foo) -> &'b i32 {
    println!("{}", foo_a.x);
    println!("{}", foo_b.x);
    return &foo_b.x;
}

fn main() {
    let foo_a = Foo { x: 42 };
    let foo_b = Foo { x: 12 };
    let x = do_something(&foo_a, &foo_b);
    // foo_a 在這裡被 dropped 釋放因為只有 foo_b 的生命週期在此之後還在延續
    println!("{}", x);
    // x 在這裡被 dropped 釋放
    // foo_b 在這裡被 dropped 釋放
}


靜態生命週期
一個靜態變數是一個在編譯期間即被建立並存在於整個程式始末的記憶體資源。他們必須被明確指定型別。 一個靜態生命週期是指一段記憶體資源無限期地延續到程式結束。需要注意的一點是,在此定義之下,一些靜態生命週期的資源也可以在執行時被建立。 擁有靜態生命週期的資源會擁有一個特殊的生命週期註解 'static。 'static 資源永遠也不會被 drop 釋放。 如果靜態生命週期資源包含了引用,那麼這些引用的生命週期也一定是 'static 的。(任何缺少了此註解的引用都不會達到同樣長的存活時間)
記憶體細節:



static PI: f64 = 3.1415;

fn main() {
    // 靜態變數的範圍也可以被限制在一個函式內
    static mut SECRET: &'static str = "swordfish";

    // 字串字面值擁有 'static 生命週期
    let msg: &'static str = "Hello World!";
    let p: &'static f64 = &PI;
    println!("{} {}", msg, p);

    // 你可以打破一些規則,但是必須是顯式地
    unsafe {
        // 我們可以修改 SECRET 到一個字串字面值因為其同樣是 'static 的
        SECRET = "abracadabra";
        println!("{}", SECRET);
    }
}


資料型別中的生命週期
和函式相同,資料型別也可以用生命週期註解來引數化其成員。 Rust 會驗證引用所包含的資料結構永遠也不會比引用指向的所有者存活週期更長。 我們不能在執行中擁有一個包括指向虛無的引用結構存在!

struct Foo<'a> {
    i:&'a i32
}

fn main() {
    let x = 42;
    let foo = Foo {
        i: &x
    };
    println!("{}",foo.i);
}


希望您能愈發清晰地認識到 Rust 是如何致力於解決系統程式設計中的諸多常見挑戰:
  • 無意間對資源的修改
  • 忘記及時地釋放資源
  • 資源意外地被釋放兩次
  • 在資源被釋放後使用了它
  • 由於讀取資料的同時有其他人正在向資源中寫入資料而引起的資料爭用
  • 在編譯器無法做擔保時,清晰看到程式碼的作用域


記憶體
Rust 程式有 3 個存放資料的記憶體區域:

  • 資料記憶體 - 對於固定大小和靜態(即在整個程式生命週期中都存在)的資料。 考慮一下程式中的文字(例如 “Hello World”),該文字的位元組只能讀取,因此它們位於該區域中。 編譯器對這類資料做了很多最佳化,由於位置已知且固定,因此通常認為編譯器使用起來非常快。
  • 棧記憶體 - 對於在函式中宣告為變數的資料。 在函式呼叫期間,記憶體的位置不會改變,因為編譯器可以最佳化程式碼,所以棧資料使用起來比較快。
  • 堆記憶體 - 對於在程式執行時建立的資料。 此區域中的資料可以新增、移動、刪除、調整大小等。由於它的動態特性,通常認為它使用起來比較慢, 但是它允許更多創造性的記憶體使用。當資料新增到該區域時,我們稱其為分配。 從本區域中刪除 資料後,我們將其稱為釋放。


在記憶體中建立資料
當我們在程式碼中例項化一個結構體時,我們的程式會在記憶體中並排建立關聯的欄位資料。
當我們透過制定所有欄位值的方式來例項化時:
struct 結構體名 { ... }.
結構體欄位可以透過 . 運算子來獲取。

struct SeaCreature {
    animal_type: String,
    name: String,
    arms: i32,
    legs: i32,
    weapon: String,
}

fn main() {
    // SeaCreature的資料在棧上
    let ferris = SeaCreature {
        // String 結構體也在棧上,
        // 但也存放了一個資料在堆上的引用
        animal_type: String::from("螃蟹"),
        name: String::from("Ferris"),
        arms: 2,
        legs: 4,
        weapon: String::from("大鉗子"),
    };

    let sarah = SeaCreature {
        animal_type: String::from("章魚"),
        name: String::from("Sarah"),
        arms: 8,
        legs: 0,
        weapon: String::from("無"),
    };
    
    println!(
        "{} 是隻{}。它有 {} 只胳膊 {} 條腿,還有一個{}。",
        ferris.name, ferris.animal_type, ferris.arms, ferris.legs, ferris.weapon
    );
    println!(
        "{} 是隻{}。它有 {} 只胳膊 {} 條腿。它沒有殺傷性武器…",
        sarah.name, sarah.animal_type, sarah.arms, sarah.legs
    );
}

例子的記憶體詳情:
  • 引號內的文字是隻讀資料(例如“ferris”),因此它位於資料記憶體區。
  • 函式呼叫 String::from 建立一個結構體 String,該結構體與 SeaCreature 的欄位並排放置在棧中。 字串容器透過如下步驟表示可更改的文字:
    1. 在堆上建立可修改文字的記憶體。
    2. 將堆中儲存物件的記憶體位置的引用儲存在 String 結構體中(在以後的課程中會詳細介紹)。
  • 最後,我們的兩個朋友 Ferris 和 Sarah 有在程式中總是固定的位置的資料結構,所以它們被放在棧上。


類元組結構體:

struct Location(i32, i32);

fn main() {
    // 這仍然是一個在棧上的結構體
    let loc = Location(42, 32);
    println!("{}, {}", loc.0, loc.1);
}


待續..

相關文章