Rust 語法梳理與總結(上)

楔子

關於 Rust 的基礎知識我們已經介紹一部分了,下面來做一個總結。因爲 Rust 是一門難度非常高的語言,在學習完每一個階段之後,對學過的內容適當總結一下是很有必要的。

那麼下面就開始吧,將以前說過的內容再總結一遍,並且在這個過程中還會補充一些之前遺漏的內容。

原生類型

首先是 Rust 的原生類型,原生類型包含標量類型和複合類型。

另外在 Rust 裏面,空元組也被稱爲單元類型。

在聲明變量的時候,可以顯式地指定類型,舉個例子:

fn main(){
    let x: i64 = 123;
    let y: bool = true;
    let z: [u8; 3] = [1, 2, 3];

    println!("x = {}", x);
    println!("y = {}", y);
    println!("z = {:?}", z);
    /*
    x = 123
    y = true
    z = [1, 2, 3]
    */
}

另外數字比較特殊,還可以通過後綴指定類型。

fn main(){
    // u8 類型
    let x = 123u8;
    // f64 類型
    let y = 3.14f64;
    println!("x = {}", x);
    println!("y = {}", y);
    /*
    x = 123
    y = 3.14
    */
}

如果沒有顯式指定類型,也沒有後綴,那麼整數默認爲 i32,浮點數默認爲 f64。

fn main(){
    // 整數默認爲 i32
    let x = 123;
    // 浮點數默認爲 f64
    let y = 3.14;
}

最後 Rust 還有一個自動推斷功能,會結合上下文推斷數值的類型。

fn main(){
    // 本來默認 x 爲 i32,y 爲 f64
    let x = 123;
    let y = 3.14;
    // 但是這裏我們將 x, y 組合成元組賦值給了 t
    // 而 t 是 (u8, f32),所以 Rust 會結合上下文
    // 將 x 推斷成 u8,將 y 推斷成 f32
    let t: (u8, f32) = (x, y);
}

但如果我們在創建 x 和 y 的時候顯式地規定了類型,比如將 x 聲明爲 u16,那麼代碼就不合法了。因爲 t 的第一個元素是 u8,但傳遞的 x 卻是 u16,此時就會報錯,舉個例子:

Rust 對類型的要求非常嚴格,即便都是數值,類型不同也不能混用。那麼這段代碼應該怎麼改呢?

fn main(){
    let x = 123u16;
    let y = 3.14;
    let t: (u8, f32) = (x as u8, y);
}

通過 as 關鍵字,將 x 轉成 u8 就沒問題了。

然後我們上面創建的整數都是十進制,如果在整數前面加上 0x, 0o, 0b,還可以創建十六進制、八進制、二進制的整數。並且在數字比較多的時候,爲了增加可讀性,還可以使用下劃線進行分隔。

fn main(){
    let x = 0xFF;
    let y = 0o77;
    // 數字較多時,使用下劃線分隔
    let z = 0b1111_1011;
    // 以 4 個數字爲一組,這樣最符合人類閱讀
    // 但 Rust 語法則沒有此要求,我們可以加上任意數量的下劃線
    let z = 0b1_1_1_1_______10______1_1;
    println!("x = {}, y = {}, z = {}", x, y, z);
    /*
    x = 255, y = 63, z = 251
    */
}

至於算術運算、位運算等操作,和其它語言都是類似的,這裏不再贅述。

元組

再來單獨看看元組,元組是一個可以包含各種類型值的組合,使用括號來創建,比如 (T1, T2, ...),其中 T1、T2 是每個元素的類型。函數可以使用元組來返回多個值,因爲元組可以擁有任意多個值。

Python 的多返回值,本質上也是返回了一個元組。

fn main(){
    // t 的類型就是 (i32, f64, u8, f32)
    let t = (12, 3.14, 33u8, 2.71f32);

    // 當然你也可以這麼做
    let t: (i32, f64, u8, f32) = (12, 3.14, 33, 2.71);

    // 但下面的做法是非法的
    // 因爲 t 的第一個元素要求是 i32,而我們傳遞的 u8
    /*
    let t: (i32, f64) = (12u8, 3.14)
    */
    // 應該改成這樣
    /*
    let t: (i32, f64) = (12i32, 3.14)
    */
    // 只不過這種做法有點多餘,因爲 t 已經規定好類型了
    // 所以沒必要寫成 12i32,直接寫成 12 就好
}

元組裏面的元素個數是固定的,類型也是固定的,但是每個元素之間可以是不同的類型。

fn main(){
    // 此時 t 的類型就會被推斷爲
    // ((i32, f64, i32)(i32, u16), i32)
    let t = ((1, 2.71, 3)(1, 2u16), 33);
}

然後是元組的打印,有兩種方式。

fn main(){
    let t = (1, 22, 333);
    // 元組打印需要使用 "{:?}"
    println!("{:?}", t);
    // 或者使用 "{:#?}" 美化打印
    println!("{:#?}", t);
    /*
    (1, 22, 333)
    (
        1,
        22,
        333,
    )
    */
}

有了元組之後,還可以對其進行解構。

fn main() {
    let t = (1, 3.14, 7u16);
    // 將 t 裏面的元素分別賦值給 x、y、z
    // 這個過程稱爲元組的結構
    // 變量多元賦值也是通過這種方式實現的
    let (x, y, z) = t;
    println!("x = {}, y = {}, z = {}", x, y, z);
    // x = 1, y = 3.14, z = 7

    // 當然我們也可以通過索引,單獨獲取元組的某個元素
    // 只不過方式是 t.索引,而不是 t[索引]
    let x = t.0;
}

再補充一點,創建元組的時候使用的是小括號,但我們知道小括號也可以起到一個限定優先級的作用。因此當元組只有一個元素的時候,要顯式地在第一個元素後面加上一個逗號。

fn main() {
    // t1 是一個 i32,因爲 (1) 等價於 1
    let t1 = (1);

    // t2 纔是元組,此時 t2 是 (i32,) 類型
    let t2 = (1,);
    println!("t1 = {}", t1);
    println!("t2 = {:?}", t2);
    /*
    t1 = 1
    t2 = (1,)
    */

    // 同樣的,當指定類型的時候也是如此
    // 如果寫成 let t3: (i32),則等價於 let t3: i32
    let t3: (i32,) = (1,);
}

至於將元組作爲函數參數和返回值,也是同樣的用法,這裏就不贅述了。

數組和切片

數組(array)是一組擁有相同類型 T 的對象的集合,在內存中是連續存儲的,所以數組不僅要求長度固定,每個元素類型也必須一樣。數組使用中括號來創建,且它們的大小在編譯時會被確定。

fn main() {
    // 數組的類型被標記爲 [T; length]
    // 其中 T 爲元素類型,length 爲數組長度
    let arr: [u8; 5] = [1, 2, 3, 4, 5];
    println!("{:?}", arr);
    /*
    [1, 2, 3, 4, 5]
    */

    // 不指定類型,可以自動推斷出來
    // 此時會被推斷爲 [i32; 5]
    let arr = [1, 2, 3, 4, 5];

    // Rust 數組的長度也是類型的一部分
    // 所以下面的 arr1 和 arr2 是不同的類型
    let arr1 = [1, 2, 3];  // [i32; 3] 類型
    let arr2 = [1, 2, 3, 4];  // [i32; 4] 類型

    // 所以 let arr1: [i32; 4] = [1, 2, 3] 是不合法的
    // 因爲聲明的類型是 [i32; 4],但傳遞的值的類型是 [i32; 3]
}

如果創建的數組所包含的元素都是相同的,那麼有一種簡便的創建方式。

fn main() {
    // 有 5 個元素,且元素全部爲 3
    let arr = [3; 5];
    println!("{:?}", arr);
    /*
    [3, 3, 3, 3, 3]
    */
}

然後是元素訪問,這個和其它語言一樣,也是基於索引。

fn main() {
    let arr = [1, 2, 3];
    println!("arr[1] = {}", arr[1]);
    /*
    arr[1] = 2
    */

    // 如果想修改數組的元素,那麼數組必須可變
    // 無論是將新的數組賦值給 arr
    // 還是通過 arr 修改當前數組內部的值
    // 都要求數組可變,其它數據結構也是如此
    let mut arr = [1, 2, 3];
    // 修改當前數組的元素,要求 arr 可變
    arr[1] = 222;
    println!("arr = {:?}", arr);
    /*
    arr = [1, 222, 3]
    */

    // 將一個新的數組綁定在 arr 上
    // 也要求 arr 可變
    arr = [2, 3, 4];
    println!("arr = {:?}", arr);
    /*
    arr = [2, 3, 4]
    */
}

說完了數組,再來說一說切片(slice)。切片允許我們對數組的某一段區間進行引用,而無需引用整個數組。

fn main() {
    let arr = [1, 2, 3, 4, 5, 6];
    let slice = &arr[2..5];
    println!("{:?}", slice);  // [3, 4, 5]
    println!("{}", slice[1]);  // 4
}

我們來畫一張圖描述一下:

這裏必須要區分一下切片和切片引用,首先代碼中的 arr[2..5] 是一個切片,由於截取的範圍不同,那麼切片的長度也不同。所以切片它不能夠分配在棧上,因爲棧上的數據必須有一個固定的、且在編譯期就能確定的大小,而切片的長度不固定,那麼大小也不固定,因此它只能分配在堆上。

既然分配在堆上,那麼就不能直接使用它,必須要通過引用。在 Rust 中,凡是堆上的數據,都是通過棧上的引用訪問的,切片也不例外,而 &a[2..5] 便是切片引用。

切片引用是一個寬指針,裏面存儲的是一個指針和一個長度,因此它不光可以是數組的切片,字符串也是可以的。

可能有人好奇 &arr[2..5] 和 &arr 有什麼區別?首先在變量前面加上 & 表示獲取它的引用,並且是不可變引用,而加上 &mut,則表示獲取可變引用。注意:這裏的可變引用中的可變兩個字,它指的不是引用本身是否可變,它描述的是能否通過引用去修改指向的值。

因此 &arr 表示對整個數組的引用,&arr[2..5] 表示對數組某個片段的引用。當然如果截取的片段是整個數組,也就是 &arr[..],那麼兩者是等價的。

然後再來思考一個問題,我們能不能通過切片引用修改底層數組呢?答案是可以的,只是對我們上面那個例子來說不可以。因爲上面例子中的數組是不可變的,所以我們需要聲明爲可變。

fn main() {
    // 最終修改的還是數組,因此數組可變是前提
    let mut arr = [1, 2, 3, 4, 5, 6];
    // 但數組可變還不夠,引用也要是可變的
    // 注意:只有當變量是可變的,才能拿到它的可變引用
    // 因爲可變引用的含義就是:允許通過引用修改指向的值
    // 但如果變量本身不可變的話,那可變引用還有啥意義呢?
    // 因此 Rust 不允許我們獲取一個'不可變變量'的可變引用
    let slice = &mut arr[2..5];
    // 通過引用修改指向的值
    slice[0] = 11111;
    println!("{:?}", arr);
    /*
    [1, 2, 11111, 4, 5, 6]
    */

    // 變量不可變,那麼只能拿到它的不可變引用
    // 而變量可變,那麼不可變引用和可變引用,均可以獲取
    // 下面的 slice 就是不可變引用
    let slice = &arr[2..5];
    // 此時只能獲取元素,不能修改元素
    // 因爲'不可變引用'不支持通過引用去修改值
}

所以要想通過引用去修改值,那麼不僅變量可變,還要獲取它的可變引用。

然後切片引用的類型 &[T],由於數組是 i32 類型,所以這裏就是 &[i32]。

fn main() {
    let mut arr = [1, 2, 3, 4, 5, 6];
    // 切片的不可變引用
    let slice: &[i32] = &arr[2..5];
    println!("{:?}", slice);  // [3, 4, 5]

    // 切片的可變引用
    let slice: &mut [i32] = &mut arr[2..5];
    println!("{:?}", slice);  // [3, 4, 5]

    // 注意這裏的 slice 現在是可變引用,但它本身是不可變的
    // 也就是說我們沒有辦法將一個別的切片引用賦值給它
    // slice = &mut arr[2..6],這是不合法的
    // 如果想這麼做,那麼 slice 本身也要是可變的
    let mut slice: &mut [i32] = &mut arr[2..5];
    println!("{:?}", slice);  // [3, 4, 5]
    // 此時是允許的
    slice = &mut arr[2..6];
    println!("{:?}", slice);  // [3, 4, 5, 6]
}

以上便是 Rust 的切片,當然我們不會直接使用切片,而是通過切片的引用。

自定義類型

Rust 允許我們通過 struct 和 enum 兩個關鍵字來自定義類型:

而常量可以通過 const 和 static 關鍵字來創建。

結構體

結構體有 3 種類型,分別是 C 風格結構體、元組結構體、單元結構體。先來看後兩種:

// 不帶有任何字段,一般用於 trait
struct Unit;

// 元組結構體,相當於給元組起了個名字
struct Color(u8, u8, u8);

fn main() {
    // 單元結構體實例
    let unit = Unit{};

    // 元組結構體實例
    // 可以看到元組結構體就相當於給元組起了個名字
    let color = Color(255, 255, 137);
    println!(
        "r = {}, g = {}, b = {}",
        color.0, color.1, color.2
    ); // r = 255, g = 255, b = 137

    // 然後是元組結構體的解構
    let Color(r, g, b) = color;
    println!("{} {} {}",
             r, g, b);  // 255 255 137
}

注意最後元組結構體實例的解構,普通元組的類型是 (T, ...),所以在解構的時候通過 let (變量, ...)。但元組結構體是 Color(T, ...),所以解構的時候通過 let Color(變量, ...)。

再來看看 C 風格的結構體。

// C 風格結構體
#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

// 結構體也可以嵌套
#[derive(Debug)]
struct Rectangle {
    // 矩形左上角和右下角的座標
    top_left: Point,
    bottom_right: Point,
}

fn main() {
    let p1 = Point { x: 3, y: 5 };
    let p2 = Point { x: 6, y: 10 };
    // 訪問結構體字段,通過 . 操作符
    println!("{}", p2.x); // 6
    // 以 Debug 方式打印結構體實例
    println!("{:?}", p1); // Point { x: 3, y: 5 }
    println!("{:?}", p2); // Point { x: 6, y: 10 }

    // 基於 Point 實例創建 Rectangle 實例
    let rect = Rectangle {
        top_left: p1,
        bottom_right: p2,
    };
    // 計算矩形的面積
    println!(
        "area = {}",
        (rect.bottom_right.y - rect.top_left.y) *
        (rect.bottom_right.x - rect.top_left.x)
    )  // area = 15
}

最後說一下 C 風格結構體的解構:

struct Point {
    x: i32,
    y: f64,
}

fn main() {
    let p = Point { x: 3, y: 5.2 };
    // 用兩個變量保存 p 的兩個成員值,可以這麼做
    // 我們用到了元組,因爲多元賦值本質上就是元組的解構
    let (a, b) = (p.x, p.y);

    // 或者一個一個賦值也行
    let a = p.x;
    let b = p.y;

    // 結構體也支持解構
    // 將 p.x 賦值給變量 a,將 p.y 賦值給變量 b
    let Point { x: a, y: b } = p;
    println!("a = {}, b = {}",
             a, b);  // a = 3, b = 5.2

    // 如果賦值的變量名,和結構體成員的名字相同
    // 那麼還可以簡寫,比如這裏要賦值的變量也叫 x、y
    // 以下寫法和 let Point { x: x, y: y } = p 等價
    let Point { x, y } = p;
    println!("x = {}, y = {}",
             x, y);  // x = 3, y = 5.2
}

最後,如果結構體實例想改變的話,那麼也要聲明爲 mut。

枚舉

enum 關鍵字允許創建一個從數個不同取值中選其一的枚舉類型。

enum Cell {
    // 成員可以是單元結構體
    NULL,
    // 也可以是元組結構體
    Integer(i64),
    Floating(f64),
    DaysSales(u32, u32, u32, u32, u32),
    // 普通結構體,或者說 C 風格結構體
    TotalSales {cash: u32, currency: &'static str}
}

fn deal(c: Cell) {
    match c {
        Cell::NULL => println!("空"),
        Cell::Integer(i) => println!("{}", i),
        Cell::Floating(f) => println!("{}", f),
        Cell::DaysSales(mon, tues, wed, thur, fri) ={
            println!("{} {} {} {} {}",
                     mon, tues, wed, thur, fri)
        },
        Cell::TotalSales { cash, currency } ={
            println!("{} {}", cash, currency)
        }
    }
}

fn main() {
    // 枚舉的任何一個成員,都是枚舉類型
    let c1: Cell = Cell::NULL;
    let c2: Cell = Cell::Integer(123);
    let c3: Cell = Cell::Floating(3.14);
    let c4 = Cell::DaysSales(101, 111, 102, 93, 97);
    let c5 = Cell::TotalSales {
        cash:  504, currency: "USD"};

    deal(c1);  // 空
    deal(c2);  // 123
    deal(c3);  // 3.14
    deal(c4);  // 101 111 102 93 97
    deal(c5);  // 504 USD
}

所以當你要保存的數據的類型不確定,但屬於有限的幾個類型之一,那麼枚舉就特別合適。另外枚舉在 Rust 裏面佔了非常高的地位,像空值處理、錯誤處理都用到了枚舉。

然後是起別名,如果某個枚舉的名字特別長,那麼我們可以給該枚舉類型起個別名。當然啦,起別名不僅僅針對枚舉,其它類型也是可以的。

enum GetElementByWhat {
    Id(String),
    Class(String),
    Tag(String),
}

fn main() {
    // 我們發現這樣寫起來特別的長
    let ele = GetElementByWhat::Id(String::from("submit"));
    // 於是可以起個別名
    type Element = GetElementByWhat;
    let ele = Element::Id(String::from("submit"));
}

給類型起的別名應該遵循駝峯命名法,起完之後就可以當成某個具體的類型來用了。但要注意的是,類型別名並不能提供額外的類型安全,因爲別名不是新的類型。

除了起別名之外,我們還可以使用 use 關鍵字直接將枚舉成員引入到當前作用域。

enum GetElementByWhat {
    Id(String),
    Class(String),
    Tag(String),
}

fn main() {
    // 將 GetElementByWhat 的 Id 成員引入到當前作用域
    use GetElementByWhat::Id;
    let ele = Id(String::from("submit"));

    // 也可以同時引入多個
    // 這種方式和一行一行寫是等價的
    use GetElementByWhat::{Class, Tag};

    // 如果你想全部引入的話,也可以使用通配符
    use GetElementByWhat::*;
}

然後 enum 也可以像 C 語言的枚舉類型一樣使用。

// 這些枚舉成員都有隱式的值
// Zero 等於 0,one 等於 1,Two 等於 2
enum Number {
    Zero,
    One,
    Two,
}

fn main() {
    // 既然是隱式的,就說明不能直接用
    // 需要顯式地轉化一下
    println!("Zero is {}", Number::Zero as i32);
    println!("One is {}", Number::One as i32);
    /*
    Zero is 0
    One is 1
    */

    let two = Number::Two;
    match two {
        Number::Zero => println!("Number::Zero"),
        Number::One => println!("Number::One"),
        Number::Two => println!("Number::Two"),
    }
    /*
    Number::Two
    */

    // 也可以轉成整數
    match two as i32 {
        0 => println!("{}", 0),
        1 => println!("{}", 1),
        2 => println!("{}", 2),
        // 雖然我們知道轉成整數之後
        // 可能的結果只有 0、1、2 三種,但 Rust 不知道
        // 所以還要有一個默認值
        _ => unreachable!()
    }
    /*
    2
    */
}

既然枚舉成員都有隱式的值,那麼可不可以有顯式的值呢?答案是可以的。

// 當指定值的時候,值必須是 isize 類型
enum Color {
    R = 125,
    G = 223,
    B,
}

fn main() {
    println!("R = {}", Color::R as u8);
    println!("G = {}", Color::G as u8);
    println!("B = {}", Color::B as u8);
    /*
    R = 125
    G = 223
    B = 224
    */
}

枚舉的成員 B 沒有初始值,那麼它默認是上一個成員的值加 1。但需要注意的是,如果想實現具有 C 風格的枚舉,那麼必須滿足枚舉裏面的成員都是單元結構體。

// 這個枚舉是不合法的
// 需要將 B(u8) 改成 B
enum Color {
    R,
    G,
    B(u8),
}

還是比較簡單的。

常量

Rust 的常量,可以在任意作用域聲明,包括全局作用域。

// Rust 的常量名應該全部大寫
// 並且聲明的時候必須提供類型,否則編譯錯誤
const AGE: u16 = 17;
// 注意:下面這種方式不行
// 因爲這種方式本質上還是在讓 Rust 做推斷
// const AGE = 17u16;

fn main() {
    // 常量可以同時在全局和函數里面聲明
    // 但變量只能在函數里面
    const NAME: &str = "komeiji satori";
    println!("NAME = {}", NAME);
    println!("AGE = {}", AGE);
    /*
    NAME = komeiji satori
    AGE = 17
    */
}

注意:常量接收的必須是在編譯期間就能確定、且不變的值,我們不能把一個運行時才能確定的值綁定在常量上。

fn count () -> i32 {
    5
}

fn main() {
    // 合法,因爲 5 是一個編譯期間可以確定的常量
    const COUNT1: i32 = 5;
    // 下面也是合法的,像 3 + 2、4 * 8 這種,雖然涉及到了運算
    // 但運算的部分都是常量,在編譯期間可以計算出來
    // 所以會將 3 + 2 換成 5,將 4 * 8 換成 32
    // 這個過程有一個專用術語,叫做常量摺疊
    const COUNT2: i32 = 3 + 2;

    // 但下面不行,count() 是運行時執行的
    // 我們不能將它的返回值綁定在常量上
    // const COUNT: i32 = count();

    // 再比如數組,數組的長度也必須是常量,並且是 usize 類型
    const LENGTH: usize = 5;
    let arr: [i32; LENGTH] = [1, 2, 3, 4, 5];
    // 但如果將 const 換成 let 就不行了
    // 因爲數組的長度是常量,而 let 聲明的是變量
    // 因此以下代碼不合法
    /*
    let LENGTH: usize = 5;
    let arr: [i32; LENGTH] = [1, 2, 3, 4, 5];
    */
}

另外我們使用 let 可以聲明多個同名變量,這在 Rust 裏面叫做變量的隱藏。但常量不行,常量的名字必須是唯一的,而且也不能和變量重名。

除了 const,還有一個 static,它聲明的是靜態變量。但它的生命週期和常量是等價的,都貫穿了程序執行的始終。

// 靜態變量在聲明時同樣要顯式指定類型
static AGE: u8 = 17;
// 常量是不可變的,所以它不可以使用 mut 關鍵字
// 即 const mut xxx 是不合法的,但 static 可以
// 因爲 static 聲明的是變量,只不過它是靜態的
// 存活時間和常量是相同,都和執行的程序共存亡
static mut NAME: &str = "satori";

fn main() {
    // 靜態變量也可以在函數內部聲明和賦值
    static ADDRESS: &str = "じれいでん";
    println!("AGE = {}", AGE);
    println!("ADDRESS = {}", ADDRESS);
    /*
    AGE = 17
    ADDRESS = じれいでん
    */

    // 需要注意:靜態變量如果聲明爲可變
    // 那麼在多線程的情況下可能造成數據競爭
    // 因此使用的時候,需要放在 unsafe 塊裏面
    unsafe {
        NAME = "koishi";
        println!("NAME = {}", NAME);
        /*
        NAME = koishi
        */
    }
}

注意裏面用到了 unsafe ,關於啥是 unsafe 我們後續再聊,總之靜態變量我們一般很少會聲明爲可變。

變量綁定

接下來複習一下變量綁定,給變量賦值在 Rust 裏面有一個專門的說法:將值綁定到變量上。都是一個意思,我們理解就好。

fn main() {
    // 綁定操作通過 let 關鍵字實現
    // 將 u8 類型的 17 綁定在變量 age 上
    let age = 17u8;

    // 將 age 拷貝給 age2
    let age2 = age;
}

如果變量聲明瞭但沒有使用,Rust 會拋出警告,我們可以在沒有使用的變量前面加上下劃線,來消除警告。

可變變量

變量默認都是不可變的,我們可以在聲明的時候加上 mut 關鍵字讓其可變。

#[derive(Debug)]
struct Color {
    R: u8,
    G: u8,
    B: u8,
}

fn main() {
    let c = Color{R: 155, G: 137, B: 255};
    // 變量 c 的前面沒有 mut,所以它不可變
    // 我們不可以對 c 重新賦值,也不可以修改 c 裏的成員值
    // 如果想改變,需要使用 let mut 聲明
    let mut c = Color{R: 155, G: 137, B: 255};
    println!("{:?}", c);
    /*
    Color { R: 155, G: 137, B: 255 }
    */

    // 聲明爲 mut 之後,我們可以對 c 重新賦值
    c = Color{R: 255, G: 52, B: 102};
    println!("{:?}", c);
    /*
    Color { R: 255, G: 52, B: 102 }
    */

    // 當然修改 c 的某個成員值也是可以的
    c.R = 0;
    println!("{:?}", c);
    /*
    Color { R: 0, G: 52, B: 102 }
    */
}

所以要改變變量的值有兩種方式:

但不管是將變量的值整體替換掉,還是對已有的值進行修改,本質上都是在改變變量的值。如果想改變,那麼變量必須聲明爲 mut。

作用域和隱藏

綁定的變量都有一個作用域,它被限制只能在一個代碼塊內存活,其中代碼塊是一個被大括號包圍的語句集合。

fn main() {
    // 存活範圍是整個 main 函數
    let name = "古明地覺";
    {
        // 新的作用域,裏面沒有 name 變量
        // 那麼會從所在的外層作用域中尋找
        println!("{}", name);  // 古明地覺

        // 創建了新的變量
        let name = "古明地戀";
        let age = 16;
        println!("{}", name);  // 古明地戀
        println!("{}", age);   // 16
    }

    // 再次打印 name
    println!("{}", name);  // 古明地覺
    // 但變量 age 已經不存在了
    // 外層作用域創建的變量,內層作用域也可以使用
    // 但內層作用域創建的變量,外層作用域不可以使用
}

我們上面創建了兩個 name,但它們是在不同的作用域,所以彼此沒有關係。但如果在同一個作用域創建兩個同名的變量,那麼後一個變量會將前一個變量隱藏掉。

fn main() {
    let mut name = "古明地覺";
    println!("{}", name);  // 古明地覺
    // 這裏的 name 前面沒有 let
    // 相當於變量的重新賦值,因此值的類型要和之前一樣
    // 並且 name 必須可變
    name = "古明地戀";
    println!("{}", name);  // "古明地戀"

    let num = 123;
    println!("{}", num);  // 123
    // 重新聲明 num,上一個 num 會被隱藏掉
    // 並且兩個 num 沒有關係,是否可變、類型都可以自由指定
    let mut num = 345u16;
    println!("{}", num);  // 345
}

變量的隱藏算是現代靜態語言中的一個比較獨特的特性了。

另外變量聲明的時候可以同時賦初始值,但將聲明和賦值分爲兩步也是可以的。

fn main() {
    let name;
    {
        // 當前作用域沒有 name
        // 那麼綁定的就是外層的 name
        name = "古明地覺"
    }
    println!("{}", name);  // 古明地覺

    // 注意:光看 name = "古明地覺" 這行代碼的話
    // 容易給人一種錯覺,認爲 name 是可變的
    // 但其實不是的,我們只是將聲明和賦值分成了兩步而已
    // 如果再賦一次值的話就會報錯了,因爲我們修改了一個不可變的變量
    // name = "古明地戀"; // 不合法,因爲修改了不可變的變量
}

如果變量聲明之後沒有賦初始值,那麼該變量就是一個未初始化的變量。而 Rust 不允許使用未初始化的變量,因爲會產生未定義行爲。

原生類型的轉換

接下來是類型轉換,首先 Rust 不提供原生類型之間的隱式轉換,如果想轉換,那麼必須使用 as 關鍵字顯式轉換。

fn main() {
    let pi = 3.14f32;
    // 下面的語句是不合法的,因爲類型不同
    // let int: u8 = pi

    // Rust 不支持隱式轉換,但可以使用 as
    let int: u8 = pi as u8;
    // 轉換之後會被截斷
    println!("{} {}", pi, int);  // 3.14 3

    // 整數也可以轉成 char 類型
    let char = 97 as char;
    println!("{}", char); // a

    // 但是整數在轉化的時候要注意溢出的問題
    // 以及無符號和有符號的問題
    let num = -10;
    // u8 無法容納負數,那麼轉成 u8 的結果就是
    // 2 的 8 次方 + num
    println!("{}", num as u8);  // 246
    let num = -300;
    // -300 + 256 = -44,但 -44 還小於 0
    // 那麼繼續加,-44 + 256 = 212
    println!("{}", num as u8);  // 212
    // 轉成 u16 就是 2 的 16 次方 + num
    println!("{}", num as u16);  // 65526

    // 以上有符號和無符號,然後是溢出的問題
    let num = 300u16;
    println!("{}", num as u8);  // 44
    // 轉成 u8 相當於只看最後 8 位
    // 那麼 num as u8 就等價於
    println!("{}", num & 0xFF);  // 44
}

as 關鍵字只允許原生類型之間的轉換,如果你想把包含 4 個元素的 u8 數組轉成一個 u32 整數,那麼 as 就不允許了。儘管在邏輯上這是成立的,但 Rust 覺得不安全,如果你非要轉的話,那麼需要使用 Rust 提供的一種更高級的轉換,並且還要使用 unsafe。

fn main() {
    // 轉成二進制的話就是
    // arr[0] -> 00000001
    // arr[1] -> 00000010
    // arr[2] -> 00000011
    // arr[3] -> 00000100
    let arr: [u8; 4] = [1, 2, 3, 4];

    // 4 個 u8 可以看成是一個 u32
    // 由於 Rust 採用的是小端存儲
    // 所以轉成整數就是
    let num = 0b00000100_00000011_00000010_00000001;
    println!("{}", num);

    // 我們也可以使用 Rust 提供的更高級的類型轉換
    unsafe {
        println!("{}", std::mem::transmute::<[u8; 4], u32>(arr))
    }
    /*
    67305985
    67305985
    */
}

可以看到結果和我們想的是一樣的。然後關於 unsafe 這一塊暫時無需關注,包括裏面那行復雜的類型轉換暫時也不用管,我們會在後續解釋它們,目前只需要知道有這麼個東西即可。

自定義類型的轉換

看完了原生類型的轉換,再來看看自定義類型,也就是結構體和枚舉。針對於自定義類型的轉換,Rust 是基於 trait 實現的,在 Rust 裏面有一個叫 From 的 trait,它內部定義了一個 from 方法。

因此如果類型 T 實現 From trait,那麼通過 T::from 便可以基於其它類型的值生成自己。

#[derive(Debug)]
struct Number {
    val: i32
}

// From 定義了一個泛型 T
// 因此在實現 From 的時候還要指定泛型的具體類型
impl From<i32> for Number {
    // 在調用 Number::from(xxx) 的時候
    // 就會自動執行這裏的 from 方法
    // 因爲實現的是 From<i32>,那麼 xxx 也必須是 i32
    // 再注意一下這裏的 Self,它表示的是當前的結構體類型
    // 但顯然我們寫成 Number 也是可以的,不過更建議寫成 Self
    fn from(item: i32) -> Self {
        Number { val: item }
    }
}

fn main() {
    println!("{:?}", Number::from(666));
    /*
    Number { val: 666 }
    */

    // 再比如 String::from,首先 String 也是個結構體
    // 顯然它實現了 From<&str>
    println!("{}", String::from("你好"));
    /*
    你好
    */
}

既然有 From,那麼就有 Into,Into 相當於是把 From 給倒過來了。並且當你實現了 From,那麼自動就獲得了 Into。

#[derive(Debug)]
struct Number {
    val: u16
}

impl From<u16> for Number {
    fn from(item: u16) -> Self {
        Number { val: item }
    }
}

fn main() {
    println!("{:?}", Number::from(666));
    /*
    Number { val: 666 }
    */

    // 由於不同的類型都可以實現 From<u16> trait
    // 那麼在調用 666u16.into() 的時候,編譯器就不知道轉成哪種類型
    // 因此這裏需要顯式地進行類型聲明
    let n: Number = 666u16.into();
    println!("{:?}", n);  // Number { val: 666 }
}

另外裏面的 666u16 寫成 666 也是可以的。因爲調用了 into 方法,Rust 會根據上下文將其推斷爲 u16。

但如果我們指定了類型,並且類型不是 u16,比如 666u8,那麼就不行了。因爲 Number 沒有實現 From,它實現的是 From,除非我們再單獨實現一個 From

然後除了 From 和 Into 之外,還有 TryFrom 和 TryInto,它們用於易出錯的類型轉換,返回值是 Result 類型。我們看一下 TryFrom 的定義:

trait TryFrom<T> {
    type Error;
    fn try_from(value: T) -> Result<Self, Self::Error>;
}

如果簡化一下,那麼就是這個樣子,我們需要實現 try_from 方法,並且要給某個類型起一個別名叫 Error。

// TryFrom 和 TryInto 需要先導入
use std::convert::TryFrom;
use std::convert::TryInto;

#[derive(Debug)]
struct IsAdult {
    age: u8
}

impl TryFrom<u8> for IsAdult {
    type Error = &'static str;
    fn try_from(item: u8) -> Result<Self, Self::Error> {
        if item >= 18 {
            Ok(IsAdult{age: item})
        } else {
            Err("未成年")
        }
    }
}

fn main() {
    let p1 = IsAdult::try_from(18);
    let p2 = IsAdult::try_from(17);
    println!("{:?}", p1);
    println!("{:?}", p2);
    /*
    Ok(IsAdult { age: 18 })
    Err("未成年")
    */

    // 實現了 TryFrom 也自動實現了 TryInto
    let p3: Result<IsAdult, &'static str> = 20.try_into();
    let p4: Result<IsAdult, &'static str> = 15.try_into();
    println!("{:?}", p3);
    println!("{:?}", p4);
    /*
    Ok(IsAdult { age: 20 })
    Err("未成年")
    */
}

最後再來介紹一個叫 ToString 的 trait,只要實現了這個 trait,那麼便可以調用 to_string 方法轉成字符串。因爲不管什麼類型的對象,我們都希望能將它打印出來。

use std::string::ToString;

struct IsAdult {
    age: u8
}
// ToString 不帶泛型參數
// 只有一個 to_string 方法,我們實現它即可
impl ToString for IsAdult {
    fn to_string(&self) -> String {
        format!("age = {}", self.age)
    }
}

fn main() {
    let p = IsAdult{age: 18};
    println!("{}", p.to_string());
    /*
    age = 18
    */
}

但很明顯,對於當前這個例子來說,即使我們不實現 trait、只是單純地實現一個方法也是可以的。

流程控制

任何一門編程語言都會包含流程控制,在 Rust 裏面有 if/else, for, while, loop 等等,讓我們來看一看它們的用法。

if / else

Rust 的 if / else 和其它語言類似,但 Rust 的布爾判斷條件不必使用小括號包裹,且每個條件後面都跟着一個代碼塊。並且 if / else 是一個表達式,所有分支都必須返回相同的類型。

fn degree(age: u8) -> String {
    if age > 90 {
        // &str 也實現了 ToString trait
        "A".to_string()
    } else if age > 80 {
        "B".to_string()
    } else if age > 60 {
        "C".to_string()
    } else {
        "D".to_string()
    }
    // if 表達式的每一個分支都要返回相同的類型
    // 然後執行的某個分支的返回值會作爲整個 if 表達式的值
}
fn main() {
    println!("{}", degree(87));
    println!("{}", degree(97));
    println!("{}", degree(57));
    /*
    B
    A
    D
    */
}

Rust 沒有提供三元運算符,因爲在 Rust 裏面 if 是一個表達式,那麼它可以輕鬆地實現三元運算符。

fn main() {
    let number = 107;
    let normailize = if number > 100 {100}
        else if number < 0 {0} else {number};
    println!("{}", normailize); // 100
}

以上就是 Rust 的 if / else。

loop 循環

Rust 提供了 loop,不需要條件,表示無限循環。想要跳出的話,需要在循環內部使用 break。

fn main() {
    let mut count = 0;
    loop {
        count += 1;
        if count == 3 {
            // countinue 後面加不加分號均可
            continue;
        }
        println!("count = {}", count);
        if count == 5 {
            println!("ok, that's enough");
            break;
        }
    }
    /*
    count = 1
    count = 2
    count = 4
    count = 5
    ok, that's enough
    */
}

最後 loop 循環有一個比較強大的功能,就是在使用 break 跳出循環的時候,break 後面的值會作爲整個 loop 循環的返回值。

fn main() {
    let mut count = 0;
    let result = loop {
        count += 1;
        if count == 3 {
            continue;
        }
        if count == 5 {
            break 1234567;
        }
    };
    println!("result = {}", result);
    /*
    result = 1234567
    */
}

這個特性還是很有意思的。

然後 loop 循環還支持打標籤,可以更方便地跳出循環。

fn main() {
    let mut count = 0;
    // break 和 continue 針對的都是當前所在的循環
    // 加上標籤的話,即可作用指定的循環
    let word = 'outer: loop {
        println!("進入外層循環");
        if count == 1 {
            // 這裏的 break 等價於 break 'outer
            println!("跳出外層循環");
            break "嘿嘿,結束了";
        }
        'inner: loop {
            println!("進入內層循環");
            count += 1;
            // 這裏如果只寫 continue
            // 那麼等價於 continue 'inner
            continue 'outer;
        };
    };
    /*
    進入外層循環
    進入內層循環
    進入外層循環
    跳出外層循環
    */

    println!("{}", word);
    /*
    嘿嘿,結束了
    */
}

注意一下標籤,和生命週期一樣,必須以一個單引號開頭。

for 循環

while 循環和其它語言類似,這裏不贅述了,直接來看 for 循環。for 循環遍歷的一般都是迭代器,而創建迭代器最簡單的辦法就是使用區間標記,比如 a..b,會生成從 a 到 b(不包含 b)、步長爲 1 的一系列值。

fn main() {
    let mut sum = 0;
    for i in 1..101 {
        sum += i;
    }
    println!("{}", sum);  // 5050

    sum = 0;
    // 如果是 ..=,那麼表示包含結尾
    for i in 1..=100 {
        sum += i;
    }
    println!("{}", sum);  // 5050
}

然後再來說一說迭代器,for 循環在遍歷集合的時候,會自動調用集合的某個方法,將其轉換爲迭代器,然後再遍歷,這一點和 Python 是比較相似的。那麼都有哪些方法,調用之後可以得到集合的迭代器呢?

首先是 iter 方法,在遍歷的時候會得到元素的引用,這樣集合在遍歷結束之後仍可以使用。

fn main() {
    let names = vec![
        "satori".to_string(),
        "koishi".to_string(),
        "marisa".to_string(),
    ];
    // names 是分配在堆上的,如果遍歷的是 names
    // 那麼遍歷結束之後 names 就不能再用了
    // 因爲在遍歷的時候,所有權就已經發生轉移了
    // 所以我們需要遍歷 names.iter()
    // 因爲 names.iter() 獲取的是 names 的引用
    // 而在遍歷的時候,拿到的也是每個元素的引用
    for name in names.iter() {
        println!("{}", name);
    }
    /*
    satori
    koishi
    marisa
    */

    println!("{:?}", names);
    /*
    ["satori""koishi""marisa"]
    */
}

循環結束之後,依舊可以使用 names。

然後是 into_iter 方法,此方法會轉移所有權,它和遍歷 names 是等價的。

我們看到在遍歷 names 的時候,會隱式地調用 names.into_iter()。如果後續不再使用 names,那麼可以調用此方法,讓 names 將自身的所有權交出去。當然啦,我們也可以直接遍歷 names,兩者是等價的。

最後是 iter_mut 方法,它和 iter 是類似的,只不過拿到的是可變引用。

fn main() {
    let mut numbers = vec![1, 2, 3];

    // numbers.iter() 獲取的是 numbers 的引用(不可變引用)
    // 然後遍歷得到的也是每個元素的引用(同樣是不可變引用)
    // numbers.iter_mut() 獲取的是 numbers 的可變引用
    // 然後遍歷得到的也是每個元素的可變引用

    // 既然拿到的是可變引用,那麼 numbers 必須要聲明爲 mut
    for number in numbers.iter_mut() {
        // 這裏的 number 就是 &mut i32
        // 修改引用指向的值
        *number *= 2;
    }

    // 可以看到 numbers 變了
    println!("{:?}", numbers);  // [2, 4, 6]
}

以上就是創建迭代器的幾種方式,最後再補充一點,迭代器還可以調用一個 enumerate 方法,能夠將索引也一塊返回。

fn main() {
    let mut names = vec![
        "satori".to_string(),
        "koishi".to_string(),
        "marisa".to_string(),
    ];

    for (index, name) in names.iter_mut().enumerate() {
        name.push_str(&format!(", 我是索引 {}", index));
    }
    println!("{:#?}", names);
    /*
    [
        "satori, 我是索引 0",
        "koishi, 我是索引 1",
        "marisa, 我是索引 2",
    ]
    */
}

調用 enumerate 方法之後,會將遍歷出來的值封裝成一個元組,其中第一個元素是索引。

match 匹配

Rust 通過 match 關鍵字來提供模式匹配,和 C 語言的 switch 用法類似。會執行第一個匹配上的分支,並且所有可能的值必須都要覆蓋。

fn main() {
    let number = 20;
    match number {
        // 匹配單個值
        1 => println!("number = 1"),
        // 匹配多個值
        2 | 5 | 6 | 7 | 10 ={
            println!("number in [2, 5, 6, 7, 10]")
        },
        // 匹配一個區間範圍
        11..=19 => println!("11 <= number <= 19"),
        // match 要求分支必須覆蓋所有可能出現的情況
        // 但明顯數字是無窮的,於是我們可以使用下劃線代表默認分支
        _ => println!("other number")
    }
    /*
    other number
    */

    let flag = true;
    match flag {
        true => println!("flag is true"),
        false => println!("flag is false"),
        // true 和 false 已經包含了所有可能出現的情況
        // 因此下面的默認分支是多餘的,但可以有
        _ => println!("unreachable")
    }
    /*
    flag is true
    */
}

對於數值和布爾值,我們更多用的是 if。然後 match 也可以處理更加複雜的結構,比如元組:

fn main() {
    let t = (1, 2, 3);
    match t {
        (0, y, z) ={
            println!("第一個元素爲 0,第二個元素爲 {}\
                     ,第三個元素爲 {}", y, z);
        },
        // 使用 .. 可以忽略部分選項,但 .. 只能出現一次
        // (x, ..) 只關心第一個元素
        // (.., x) 只關心最後一個元素
        // (x, .., y) 只關心第一個和最後一個元素
        // (x, .., y, z) 只關心第一個和最後兩個元素
        // (..) 所有元素都不關心,此時效果等價於默認分支
        (1, ..) ={
            println!("第一個元素爲 1,其它元素不關心")
        },
        (..) ={
            println!("所有元素都不關心")
        },
        _ ={
            // 由於 (..) 分支的存在,默認分支永遠不可能執行
            println!("默認分支")
        }
    }

    /*
    第一個元素爲 1,其它元素不關心
    */
}

然後是枚舉:

fn main() {
    enum Color {
        RGB(u32, u32, u32),
        HSV(u32, u32, u32),
        HSL(u32, u32, u32),
    }

    let color = Color::RGB(122, 45, 203);
    match color {
        Color::RGB(r, g, b) ={
            println!("r = {}, g = {}, b = {}",
                     r, g, b);
        },
        Color::HSV(h, s, v) ={
            println!("h = {}, s = {}, v = {}",
                     h, s, v);
        },
        Color::HSL(h, s, l) ={
            println!("h = {}, s = {}, l = {}",
                     h, s, l);
        }
    }
    /*
    r = 122, g = 45, b = 203
    */
}

接下來是結構體:

fn main() {
    struct Point {
        x: (u32, u32),
        y: u32
    }

    let p = Point{x: (1, 2), y: 5};
    // 之前說過,可以使用下面這種方式解構
    // let Point { x, y } = p
    // 對於使用 match 來說,也是如此
    match p {
        Point { x, y } ={
            println!("p.x = {:?}, p.y = {}", x, y);
        }
        // 如果不關心某些成員的話,那麼也可以使用 ..
        // 比如 Point {x, ..},表示你不關心 y
    }
    /*
    p.x = (1, 2), p.y = 5
    */
}

最後來看一下,如何對引用進行解構。首先要注意的是:解引用和解構是兩個完全不同的概念。解引用使用的是 *,解構使用的是 &。

fn main() {
    let mut num = 123;
    // 獲取一個 i32 的引用
    let refer = &mut num;
    // refer 是一個引用,可以通過 *refer 解引用
    // 並且在打印的時候,refer 和 *refer 是等價的
    println!("refer = {}, *refer = {}", refer, *refer);
    /*
    refer = 123, *refer = 123
    */

    // 也可以修改引用指向的值
    // refer 引用的是 num,那麼要想修改的話
    // num 必須可變,refer 也必須是 num 的可變引用
    *refer = 1234;
    println!("num = {}", num);
    /*
    num = 1234
    */

    // 字符串也是同理
    let mut name = "komeiji".to_string();
    let refer = &mut name;
    // 修改字符串,將首字母大寫
    *refer = "Komeiji".to_string();
    println!("{}", name);  // Komeiji
}

以上便是解引用,再來看看引用的解構。

fn main() {
    let num = 123;
    let refer = #

    match refer {
        // 如果用 &val 這個模式去匹配 refer
        // 相當於做了這樣的比較,因爲 refer 是 &i32
        // 而模式是 &val,那麼相當於將 refer 引用的值拷貝給了 val
        &val ={
            println!("refer 引用的值 = {}", val)
        }  // 如果 refer 是可變引用,那麼這裏的模式就應該是 &mut val
    };
    /*
    refer 引用的值 = 123
    */

    // 如果不想使用 &,那麼就要在匹配的時候解引用
    match *refer {
        val ={
            println!("refer 引用的值 = {}", val)
        }
    };
    /*
    refer 引用的值 = 123
    */
}

最後我們創建引用的時候,除了可以使用 & 之外,還可以使用 ref 關鍵字。

fn main() {
    let num = 123;
    // let refer = # 可以寫成如下
    let ref refer = num;
    println!("{} {} {}", refer, *refer, num);
    /*
    123 123 123
    */
    // 引用和具體的值在打印上是沒有區別的
    // 但從結構上來說,兩者卻有很大區別
    // 比如我們可以對 refer 解引用,但不能對 num 解引用

    // 創建可變引用
    let mut num = 345;
    {
        let ref mut refer = num;
        *refer = *refer + 1;
        println!("{} {}", refer, *refer);
        /*
        346 346
        */
    }
    println!("{}", num); // 346

    // 然後模式匹配也可以使用 ref
    let num = 567;
    match num {
        // 此時我們應該把 ref refer 看成是一個整體
        // 所以 ref refer 整體是一個 i32
        // 那麼 refer 是啥呢?顯然是 &i32
        ref refer => println!("{} {}", refer, *refer),
    }
    /*
    567 567
    */

    let mut num = 678;
    match num {
        // 顯然 refer 就是 &mut i32
        ref mut refer ={
            *refer = 789;
        }
    }
    println!("{}", num); // 789
}

以上就是 match 匹配,但是在引用這一塊,需要多體會一下。

另外在使用 match 的時候,還可以搭配衛語句,用於過濾分支,舉個例子:

fn match_tuple(t: (i32, i32)) {
    match t {
        // (x, y) 已經包含了所有的情況
        // 但我們又給它加了一個限制條件
        // 就是兩個元素必須相等
        (x, y) if x == y ={
            println!("t[0] == t[1]")
        },
        (x, y) if x > y ={
            println!("t[0] > t[1]")
        },
        // 此時就不需要衛語句了,該分支的 x 一定小於 y
        // 並且這裏加上衛語句反而會報錯,因爲加上之後
        // Rust 無法判斷分支是否覆蓋了所有的情況
        // 所以必須有 (x, y) 或者默認分支進行兜底
        (x, y) ={
            println!("t[0] < t[1]")
        },
    }
}

fn main() {
    match_tuple((1, 2));
    match_tuple((1, 1));
    match_tuple((3, 1));
    /*
    t[0] < t[1]
    t[0] == t[1]
    t[0] > t[1]
    */
}

總的來說,衛語句用不用都是可以的,我們完全可以寫成 (x, y),匹配上之後在分支裏面做判斷。

最後 match 還有一個綁定的概念,看個例子:

fn main() {
    let num = 520;
    match num {
        // 該分支一定可以匹配上
        // 匹配之後會將 num 賦值給 n
        n ={
            if n == 520 {
                println!("{} 代表 ❥(^_-)", n)
            } else {
                println!("意義不明的數字")
            }
        }
    }
    /*
    520 代表 ❥(^_-)
    */

    // 我們可以將 520 這個分支單獨拿出來
    match num {
        // 匹配完之後,會自動將 520 綁定在 n 上面
        n @ 520 => println!("{} 代表 ❥(^_-)", n),
        n => println!("意義不明的數字")
    }
    /*
    520 代表 ❥(^_-)
    */

    // 當然啦,我們還可以使用衛語句
    match num {
        n if n == 520 => println!("{} 代表 ❥(^_-)", n),
        n => println!("意義不明的數字")
    }
    /*
    520 代表 ❥(^_-)
    */
}

這幾個功能彼此之間都是很相似的,用哪個都可以。

if let

在一些簡單的場景下,使用 match 其實並不優雅,舉個例子。

fn main() {
    let num = Some(777);
    match num {
        Some(n) => println!("{}", n),
        // 因爲 match 要覆蓋所有情況,所以這一行必須要有
        // 但如果我們不關心默認情況的話,那麼就有點多餘了
        _ =()
    }
    /*
    777
    */
    // 所以當我們只關心一種情況,其它情況忽略的話
    // 那麼使用 if let 會更加簡潔
    if let Some(i) = num {
        println!("{}", i);
    }
    /*
    777
    */

    // 當然 if let 也支持 else if let 和 else
    let score = 78;
    if let x @ 90..=100 = score {
        println!("你的分數 {} 屬於 A 級", x)
    } else if let x @ 80..=89 = score {
        println!("你的分數 {} 屬於 B 級", x)
    } else if let 60..=79 = score {
        println!("你的分數 {} 屬於 C 級", score)
    }
    /*
    你的分數 78 屬於 C 級
    */

    // 顯然對於當前這種情況就不適合用 if let 了
    // 此時應該使用 match 或者普通的 if 語句
    // 總之:match 一般用來處理枚舉
    // 如果不是枚舉,那麼用普通的 if else 就好
    // 如果只關注枚舉的一種情況,那麼使用 if let
}

注意:if let 也可以搭配 else if 語句。

小結

以上我們就回顧了一下 Rust 的基礎知識,包括原生類型、自定義類型、變量綁定、類型系統、類型轉換、流程控制。下一篇文章我們來回顧 Rust 的函數和泛型。

Reference:

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/8Hyk9UMzUU2TN9_4wyNJxg