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 两个关键字来自定义类型:

  • 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 (r, g, b) = ... 即可
    // 但元组结构体则不行,不是说它不支持解构,而是在解构的时候要指定结构体名称,像下面这样
    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 会抛出警告,我们可以在没有使用的变量前面加上下划线,来消除警告。

可变变量

#[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 }
    */
}

所以要改变变量的值有两种方式:

  • 1)给变量赋一个新的值,这是所有变量都支持的,比如 let mut t = (1, 2),如果想将第一个元素改成 11,那么 t = (11, 2) 即可;
  • 2)针对元组、数组、结构体等,如果你熟悉 Python 的话,会发现这类似于 Python 里的可变对象。也就是不赋一个新的值,而是对当前已有的值进行修改,比如 let mut t = (1, 2),如果想将第一个元素改成 11,那么 t.0 = 11 即可。

但不管是将变量的值整体替换掉,还是对已有的值进行修改,本质上都是在改变变量的值。如果想改变,那么变量必须声明为 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 的时候还要指定泛型的具体类型
// 所以这里就是为结构体 Number 实现 From<i32> trait
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<u8>,它实现的是 From<u16>,除非我们再单独实现一个 From<u8>

然后除了 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
    */

    // let Point { x, y } = p 等价于 let Point { x: x, y: y } = p
    // 表示新创建的变量 x、y,并赋上对应成员的值,只是在变量名和结构体成员名相同的时候,可以简写
    // 在 match 表达式中也是如此,比如我们将 p.x 赋值给变量 a
    match p {
        Point { x: a, y } => {
            println!("p.x = {:?}, p.y = {}", a, 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 = &num;

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

补充:如果对一个引用进行解引用,那么引用指向的值必须是可 Copy 的。

对引用进行解引用的时候,会将值拷贝一份,于是值必须是可 Copy 的。如果不可 Copy,那么只能转移所有权,因为 Rust 默认不会拷贝堆上数据,但这会导致原有的变量不可用,因此 Rust 要求值必须是可 Copy 的。当然,如果不涉及解引用,只是普通的变量赋值,那么是会转移所有权的。

最后我们创建引用的时候,除了可以使用 & 之外,还可以使用 ref 关键字。

fn main() {
    let num = 123;
    // let refer = &num; 可以写成如下
    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 Some(i) == num 可不可以,答案是不行,因为 i 没有定义
    // 而 if let Some(i) = num,是判断 Some(i) 能否匹配上 num
    // 如果可以,就将 Some 里面的 777 赋值给变量 i
    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 的函数和泛型。

posted @ 2023-10-16 17:12  古明地盆  阅读(285)  评论(0编辑  收藏  举报