• 【Rust基础①】基本类型、所有权与借用、复合类型


    1 基本类型

    示例, 在rust中,变量是默认不可变的。要使变量可变,要用mut修饰

    // Rust 程序入口函数,跟其它语言一样,都是 main,该函数目前无返回值
    fn main() {
        // 使用let来声明变量,进行绑定,a是不可变的
        // 此处没有指定a的类型,编译器会默认根据a的值为a推断类型:i32,有符号32位整数
        // 语句的末尾必须以分号结尾
        let a = 10;
        // 主动指定b的类型为i32
        let b: i32 = 20;
        // 这里有两点值得注意:
        // 1. 可以在数值中带上类型:30i32表示数值是30,类型是i32
        // 2. c是可变的,mut是mutable的缩写
        let mut c = 30i32;
        // 还能在数值和类型中间添加一个下划线,让可读性更好
        let d = 30_i32;
        // 跟其它语言一样,可以使用一个函数的返回值来作为另一个函数的参数
        let e = add(add(a, b), add(c, d));
    
        // println!是宏调用,看起来像是函数但是它返回的是宏定义的代码块
        // 该函数将指定的格式化字符串输出到标准输出中(控制台)
        // {}是占位符,在具体执行过程中,会把e的值代入进来
        println!("( a + b ) + ( c + d ) = {}", e);
    }
    
    // 定义一个函数,输入两个i32类型的32位有符号整数,返回它们的和
    fn add(i: i32, j: i32) -> i32 {
        // 返回相加值,这里可以省略return
        i + j
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    1.1 数值类型

    Rust 每个值都有其确切的数据类型,总的来说可以分为两类:基本类型和复合类型。 基本类型意味着它们往往是一个最小化原子类型,无法解构为其它类型(一般意义上来说),由以下组成:

    • 数值类型: 有符号整数 (i8, i16, i32, i64, isize)、 无符号整数 (u8, u16, u32, u64, usize) 、浮点数 (f32, f64)、以及有理数、复数
    • 字符串:字符串字面量和字符串切片 &str
    • 布尔类型: true和false
    • 字符类型: 表示单个 Unicode 字符,存储为 4 个字节
    • 单元类型: 即 () ,其唯一的值也是 ()

    1.1.1 Rust 中的内置的整数类型:

    长度有符号类型无符号类型
    8 位i8u8
    16 位i16u16
    32 位i32u32
    64 位i64u64
    128 位i128u128
    视架构而定isizeusize

    每个有符号类型规定的数字范围是 − ( 2 n − 1 ) -(2^{n-1}) (2n1)~ 2 n − 1 − 1 2^{n-1}-1 2n11,其中 n 是该定义形式的位长度。因此 i8 可存储数字范围是 -(27) ~ 27 - 1,即 -128 ~ 127。无符号类型可以存储的数字范围是 0 ~ 2n - 1,所以 u8 能够存储的数字为 0 ~ 28 - 1,即 0 ~ 255。

    此外,isizeusize 类型取决于程序运行的计算机 CPU 类型: 若 CPU 是 32 位的,则这两个类型是 32 位的,同理,若 CPU 是 64 位,那么它们则是 64 位。

    整形字面量可以用下表的形式书写:

    数字字面量示例
    十进制98_222
    十六进制0xff
    八进制0o77
    二进制0b1111_0000
    字节 (仅限于 u8)b’A’

    1.1.2 浮点类型

    浮点类型数字 是带有小数点的数字,在 Rust 中浮点类型数字也有两种基本类型: f32f64,分别为 32 位和 64 位大小。默认浮点类型是f64,在现代的 CPU 中它的速度与 f32 几乎相同,但精度更高。

    当需要使用浮点数时,需遵守以下准则:

    • 避免在浮点数上测试相等性
    • 当结果在数学上可能存在未定义时,需要格外的小心

    例如:

    fn main() {
        let abc: (f32, f32, f32) = (0.1, 0.2, 0.3);
        let xyz: (f64, f64, f64) = (0.1, 0.2, 0.3);
    
        println!("abc (f32)");
        println!("   0.1 + 0.2: {:x}", (abc.0 + abc.1).to_bits());
        println!("         0.3: {:x}", (abc.2).to_bits());
        println!();
    
        println!("xyz (f64)");
        println!("   0.1 + 0.2: {:x}", (xyz.0 + xyz.1).to_bits());
        println!("         0.3: {:x}", (xyz.2).to_bits());
        println!();
    
        assert!(abc.0 + abc.1 == abc.2);
        assert!(xyz.0 + xyz.1 == xyz.2);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    运行该程序,输出如下:

    abc (f32)
       0.1 + 0.2: 3e99999a
             0.3: 3e99999a
    
    xyz (f64)
       0.1 + 0.2: 3fd3333333333334
             0.3: 3fd3333333333333
    
    thread 'main' panicked at 'assertion failed: xyz.0 + xyz.1 == xyz.2',
    ➥ch2-add-floats.rs.rs:14:5
    note: run with `RUST_BACKTRACE=1` environment variable to display
    ➥a backtrace
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    对于数学上未定义的结果,例如对负数取平方根-42.1.sqrt(),会产生一个特殊的结果:Rust 的浮点数类型使用 NaN (not a number)来处理这些情况。可以使用 is_nan() 等方法,可以用来判断一个数值是否是 NaN

    fn main() {
        let x = (-42.0_f32).sqrt();
        if x.is_nan() {
            println!("未定义的数学行为")
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    1.1.3 数学运算

    fn main() {
      // 编译器会进行自动推导,给予twenty i32的类型
      let twenty = 20;
      // 类型标注
      let twenty_one: i32 = 21;
      // 通过类型后缀的方式进行类型标注:22是i32类型
      let twenty_two = 22i32;
    
      // 只有同样类型,才能运算
      let addition = twenty + twenty_one + twenty_two;
      println!("{} + {} + {} = {}", twenty, twenty_one, twenty_two, addition);
    
      // 对于较长的数字,可以用_进行分割,提升可读性
      let one_million: i64 = 1_000_000;
      println!("{}", one_million.pow(2));
    
      // 定义一个f32数组,其中42.0会自动被推导为f32类型
      let forty_twos = [
        42.0,
        42f32,
        42.0_f32,
      ];
    
      // 打印数组中第一个值,并控制小数位为2位
      println!("{:.2}", forty_twos[0]);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    1.1.4 位运算

    Rust的运算基本上和其他语言一样

    运算符说明
    & 位与相同位置均为1时则为1,否则为0
    | 位或相同位置只要有1时则为1,否则为0
    ^ 异或相同位置不相同则为1,相同则为0
    ! 位非把位中的0和1相互取反,即0置为1,1置为0
    << 左移所有位向左移动指定位数,右位补0
    >> 右移所有位向右移动指定位数,带符号移动(正数补0,负数补1)

    1.1.5 序列(Range)

    Rust 提供了一个非常简洁的方式,用来生成连续的数值,例如 1..5,生成从 1 到 4 的连续数字,不包含 5 ;1..=5,生成从 1 到 5 的连续数字,包含 5.

    序列只允许用于数字或字符类型

    1.2 字符、布尔、单元类型

    1. Rust 的字符不仅仅是 ASCII,所有的 Unicode 值都可以作为 Rust 字符,包括单个的中文、日文、韩文、emoji 表情符号等等,都是合法的字符类型。由于 Unicode 都是 4 个字节编码,因此字符类型也是占用 4 个字节:
    fn main() {
        let x = '中';
        println!("字符'中'占用了{}字节的内存大小",std::mem::size_of_val(&x)); //4
    
        let your_character:char='8'; // What's your favorite character?
        // Try a letter, try a number, try a special character, try a character
        // from a different language than your own, try an emoji!
        if your_character.is_alphabetic() {
            println!("Alphabetical!");
        } else if your_character.is_numeric() {
            println!("Numerical!");
        } else {
            println!("Neither alphabetic nor numeric!");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    1. Rust 中的布尔类型有两个可能的值:truefalse,布尔值占用内存的大小为 1 个字节

    2. 单元类型就是 ()。main 函数就返回这个单元类型 (),不能说 main 函数无返回值,因为没有返回值的函数在 Rust 中是有单独的定义的:发散函数( diverge function ),顾名思义,无法收敛的函数。
      例如常见的 println!() 的返回值也是单元类型 ()。再比如,可以用 () 作为 map 的值,表示我们不关注具体的值,只关注 key。 可以作为一个值用来占位,但是完全不占用任何内存。

    1.3 语句和表达式

    Rust 的函数体是由一系列语句组成,最后由一个表达式来返回值,例如:

    fn add_with_extra(x: i32, y: i32) -> i32 {
        let x = x + 1; // 语句
        let y = y + 5; // 语句
        x + y // 表达式
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    语句会执行一些操作但是不会返回一个值,而表达式会在求值后返回一个值,因此在上述函数体的三行代码中,前两行是语句,最后一行是表达式。

    对于 Rust 语言而言,这种基于语句(statement)和表达式(expression)的方式是非常重要的,你需要能明确的区分这两个概念。基于表达式是函数式语言的重要特征,表达式总要返回值。

    调用一个函数是表达式,因为会返回一个值,调用宏也是表达式,用花括号包裹最终返回一个值的语句块也是表达式,总之,能返回值,它就是表达式:

    fn main() {
        let y = {
            let x = 3;
            x + 1
        };
    
        println!("The value of y is: {}", y);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    上面使用一个语句块表达式将值赋给 y 变量。表达式不能加分号!

    表达式如果不返回任何值,会隐式地返回一个 () 。

    fn main() {
        assert_eq!(ret_unit_type(), ())
    }
    
    fn ret_unit_type() {
        let x = 1;
        // if 语句块也是一个表达式,因此可以用于赋值,也可以直接返回
        // 类似三元运算符,在Rust里我们可以这样写
        let y = if x % 2 == 1 {
            "odd"
        } else {
            "even"
        };
        // 或者写成一行
        let z = if x % 2 == 1 { "odd" } else { "even" };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    if 是最基本的控制流语句,使用方法:

    pub fn foo_if_fizz(fizzish: &str) -> &str {
        if fizzish == "fizz" {
            "foo"
        } else if fizzish=="fuzz" {
            "bar"
        }else {
            "baz"
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    1.4 函数

    函数要点:

    • 函数名和变量名使用蛇形命名法(snake case),例如 fn add_two() -> {}
    • 函数的位置可以随便放,Rust 不关心我们在哪里定义了函数,只要有定义即可
    • 每个函数参数都需要标注类型

    Rust 是强类型语言,因此需要你为每一个函数参数都标识出它的具体类型。

    特殊返回类型 - 单元类型 (),是一个零长度的元组。它没啥作用,但是可以用来表达一个函数没有返回值:

    • 函数没有返回值,那么返回一个()
    • 通过;结尾的表达式返回一个 ()
    //显示返回()
    fn clear(text: &mut String) -> () {
      *text = String::from("");
    }
    
    • 1
    • 2
    • 3
    • 4

    当用!作函数返回类型的时候,表示该函数永不返回( diverge function ),特别的,这种语法往往用做会导致程序崩溃的函数。

    函数示例:

    fn main() {
        let original_price = 51;
        println!("Your sale price is {}", sale_price(original_price));
    }
    
    fn sale_price(price: i32) ->i32 {
        if is_even(price) {
            price - 10
        } else {
            price - 3
        }
    }
    
    fn is_even(num: i32) -> bool {
        num % 2 == 0
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    2 所有权与借用

    所有的程序都必须和计算机内存打交道,如何从内存中申请空间来存放程序的运行内容,如何在不需要的时候释放这些空间,成了重中之重,也是所有编程语言设计的难点之一。在计算机语言不断演变过程中,出现了三种流派:

    • 垃圾回收机制(GC),在程序运行时不断寻找不再使用的内存,典型代表:Java、Go
    • 手动管理内存的分配和释放, 在程序中,通过函数调用的方式来申请和释放内存,典型代表:C++
    • 通过所有权来管理内存,编译器在编译时会根据一系列规则进行检查

    其中 Rust 选择了第三种,最妙的是,这种检查只发生在编译期,因此对于程序运行期,不会有任何性能上的损失。

    2.1 栈(Stack)与堆(Heap)

    堆和栈是编程语言最核心的数据结构,核心目标就是为程序在运行时提供可供使用的内存空间

    栈按照顺序存储值并以相反顺序取出值,这也被称作后进先出

    与栈不同,对于大小未知或者可能变化的数据,我们需要将它存储在堆上。当向堆上放入数据时,需要请求一定大小的内存空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的指针, 该过程被称为在堆上分配内存,有时简称为 “分配”(allocating)。接着,该指针会被推入栈中,因为指针的大小是已知且固定的,在后续使用过程中,你将通过栈中的指针,来获取数据在堆上的实际内存位置,进而访问该数据。

    因此,入栈比在堆上分配内存要快,因为入栈时操作系统无需分配新的空间,只需要将新数据放入栈顶即可;栈数据往往可以直接存储在 CPU 高速缓存中,而堆数据只能存储在内存中。访问堆上的数据比访问栈上的数据慢,因为必须先访问栈再通过栈上的指针来访问内存。

    2.2 所有权原则

    Rust中关于所有权的规则:

    • Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
    • 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
    • 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)

    2.2.1 转移所有权

    String 类型是一个复杂类型,由存储在栈中的堆指针字符串长度字符串容量共同组成,其中堆指针是最重要的,它指向了真实存储字符串内容的堆内存,容量是堆内存分配空间的大小,长度是目前已经使用的大小。总之 String 类型指向了一个堆上的空间,这里存储着它的真实数据,例如:

    let s1 = String::from("hello");
    let s2 = s1;
    
    • 1
    • 2

    注意,当变量离开作用域后,Rust 会自动调用 drop 函数并清理变量的堆内存。不过由于两个 String 变量指向了同一位置。这就有了一个问题:当 s1 和 s2 离开作用域,它们都会尝试释放相同的内存。这是一个叫做 二次释放(double free) 的错误,也是之前提到过的内存安全性 BUG 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。

    Rust 这样解决该问题:当 s1 赋予 s2 后,Rust 认为 s1 不再有效,因此也无需在 s1 离开作用域后 drop 任何东西,这就是把所有权从 s1 转移给了 s2,s1 在被赋予 s2 后就马上失效了。拷贝指针、长度和容量而不拷贝数据听起来就像浅拷贝,但是又因为 Rust 同时使第一个变量 s1 无效了,因此这个操作被称为 移动(move),而不是浅拷贝。上面的例子可以解读为 s1 被移动到了 s2 中

    在这里插入图片描述
    Rust 永远也不会自动创建数据的 “深拷贝”。因此,任何自动的复制都不是深拷贝,可以被认为对运行时性能影响较小。

    2.2.2 克隆(深拷贝)

    如果我们确实需要深度复制 String 中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 clone 的方法。但是对于执行较为频繁的代码,使用 clone 会极大的降低程序性能。

    fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();
    
    println!("s1 = {}, s2 = {}", s1, s2);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2.2.3 拷贝(浅拷贝)

    Rust 有一个叫做 Copy 的特征,可以用在类似整型这样在栈中存储的类型。如果一个类型拥有 Copy 特征,一个旧的变量在被赋值给其他变量后仍然可用

    那么什么类型是可 Copy 的呢?可以查看给定类型的文档来确认,不过作为一个通用的规则: 任何基本类型的组合可以 Copy ,不需要分配内存或某种形式资源的类型是可以 Copy 的。如下是一些 Copy 的类型:

    • 所有整数类型,比如 u32
    • 布尔类型,bool,它的值是 true 和 false。
    • 所有浮点数类型,比如 f64
    • 字符类型,char
    • 元组,当且仅当其包含的类型也都是 Copy 的时候。比如,(i32, i32) 是 Copy 的,但 (i32, String) 就不是。
    • 不可变引用 &T ,例如转移所有权中的最后一个例子,但是注意: 可变引用 &mut T 是不可以 Copy的

    2.2.4 函数传值与返回

    将值传递给函数,一样会发生 移动 或者 复制,就跟 let 语句一样,下面的代码展示了所有权、作用域的规则:

    fn main() {
        let s1 = gives_ownership();         // gives_ownership 将返回值
                                            // 移给 s1
    
        let s2 = String::from("hello");     // s2 进入作用域
    
        let s3 = takes_and_gives_back(s2);  // s2 被移动到
                                            // takes_and_gives_back 中,
                                            // 它也将返回值移给 s3
    } // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
      // 所以什么也不会发生。s1 移出作用域并被丢弃
    
    fn gives_ownership() -> String {             // gives_ownership 将返回值移动给
                                                 // 调用它的函数
    
        let some_string = String::from("hello"); // some_string 进入作用域.
    
        some_string                              // 返回 some_string 并移出给调用的函数
    }
    
    // takes_and_gives_back 将传入字符串并返回该值
    fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
    
        a_string  // 返回 a_string 并移出给调用的函数
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    2.3 引用与借用

    Rust 通过获取变量的引用,称之为借用(borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来,当使用完毕后,也必须要物归原主。

    fn main() {
        let x = 5;
        let y = &x;
    
        assert_eq!(5, x);//错误,不同类型的比较
        assert_eq!(5, *y);//使用 *y 来解出引用所指向的值(也就是解引用)。一旦解引用了 y,就可以访问 y 所指向的整型值并可以与 5 做比较。
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    可变引用

    首先,声明 s 是可变类型,其次创建一个可变的引用 &mut s 和接受可变引用参数 some_string: &mut String 的函数。

    fn main() {
        let mut s = String::from("hello");
    
        change(&mut s);
    }
    
    fn change(some_string: &mut String) {
        some_string.push_str(", world");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    同一作用域,特定数据只能有一个可变引用,这种限制的好处就是使 Rust 在编译期就避免数据竞争,数据竞争可由以下行为造成:

    • 两个或更多的指针同时访问同一数据
    • 至少有一个指针被用来写入数据
    • 没有同步数据访问的机制

    可以通过手动限制变量的作用域:

    let mut s = String::from("hello");
    
    {
        let r1 = &mut s;
    
    } // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用
    
    let r2 = &mut s;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    可变引用与不可变引用不能同时存在,比如

    let mut s = String::from("hello");
    
    let r1 = &s; // 没问题
    let r2 = &s; // 没问题
    let r3 = &mut s; // 大问题
    
    println!("{}, {}, and {}", r1, r2, r3);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    其实这个也很好理解,正在借用不可变引用的用户,肯定不希望他借用的东西,被另外一个人莫名其妙改变了。多个不可变借用被允许是因为没有人会去试图修改数据,每个人都只读这一份数据而不做修改,因此不用担心数据被污染。

    注意,引用的作用域 s 从创建开始,一直持续到它最后一次使用的地方,这个跟变量的作用域有所不同,变量的作用域从创建持续到某一个花括号 }

    悬垂引用(Dangling References)

    悬垂引用也叫做悬垂指针,意思为指针指向某个值后,这个值被释放掉了,而指针仍然存在,其指向的内存可能不存在任何值或已被其它变量重新使用。在 Rust 中编译器可以确保引用永远也不会变成悬垂状态:当你获取数据的引用后,编译器可以确保数据不会在引用结束前被释放,要想释放数据,必须先停止其引用的使用。 例如:

    fn dangle() -> &String { // dangle 返回一个字符串的引用
    
        let s = String::from("hello"); // s 是一个新字符串
    
        &s // 返回字符串 s 的引用
    } // 这里 s 离开作用域并被丢弃。其内存被释放。
      // 危险!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    因为 s 是在 dangle 函数内创建的,当 dangle 的代码执行完毕后,s 将被释放, 但是此时我们又尝试去返回它的引用。这意味着这个引用会指向一个无效的 String,这可不对!

    其中一个很好的解决方法是直接返回 String:

    fn no_dangle() -> String {
        let s = String::from("hello");
    
        s
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这样就没有任何错误了,最终 String 的 所有权被转移给外面的调用者

    3 复合类型

    3.1 字符串与切片

    3.1.1 切片(slice)

    对于字符串而言,切片就是对 String 类型中某一部分的引用

    let s = String::from("hello world");
    
    let hello = &s[0..5];
    let world = &s[6..11];
    
    • 1
    • 2
    • 3
    • 4

    这就是创建切片的语法,使用方括号包括的一个序列:[开始索引…终止索引],其中开始索引是切片中第一个元素的索引位置,而终止索引是最后一个元素后面的索引位置,也就是这是一个 右半开区间。在切片数据结构内部会保存开始的位置和切片的长度,其中长度是通过 终止索引 - 开始索引 的方式计算得来的。

    对于 let world = &s[6..11]; 来说,world 是一个切片,该切片的指针指向 s 的第 7 个字节(索引从 0 开始, 6 是第 7 个字节),且该切片的长度是 5 个字节。
    在这里插入图片描述
    截取完整的String切片:

    let s = String::from("hello");
    
    let len = s.len();
    
    let slice = &s[0..len];
    let slice = &s[..];
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    字符串切片的类型标识是 &str,在对字符串使用切片语法时需要格外小心,切片的索引必须落在字符之间的边界位置,也就是 UTF-8 字符的边界,例如中文在 UTF-8 中占用三个字节,下面的代码就会崩溃:

     let s = "中国人";
     let a = &s[0..2];
     println!("{}",a);
    
    • 1
    • 2
    • 3

    因为我们只取 s 字符串的前两个字节,但是本例中每个汉字占用三个字节,因此没有落在边界处,也就是连 字都取不完整,此时程序会直接崩溃退出,如果改成 &s[0..3],则可以正常通过编译。

    因为切片是对集合的部分引用,因此不仅仅字符串有切片,其它集合类型也有,例如数组:

    let a = [1, 2, 3, 4, 5];
    
    let slice = &a[1..3];
    
    assert_eq!(slice, &[2, 3]);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    该数组切片的类型是 &[i32],数组切片和字符串切片的工作方式是一样的,例如持有一个引用指向原始数组的某个元素和长度。

    字符串是由字符组成的连续集合,Rust 中的字符是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是在字符串中不一样,字符串是 UTF-8 编码,也就是字符串中的字符所占的字节数是变化的(1 - 4),这样有助于大幅降低字符串所占用的内存空间。Rust 在语言级别,只有一种字符串类型: str,它通常是以引用类型出现 &str,也就是上文提到的字符串切片。但在标准库中String 则是一个可增长、可改变且具有所有权的 UTF-8 编码字符串,当 Rust 用户提到字符串时,往往指的就是 String 类型和 &str 字符串切片类型,这两个类型都是 UTF-8 编码

    String&str类型之间的转换:

    fn main() {
        let s = String::from("hello,world!");
        //等同于 let s="hello,world!".to_string()
        say_hello(&s); //String转&str
        say_hello(&s[..]);
        say_hello(s.as_str());
    }
    
    fn say_hello(s: &str) {
        println!("{}",s);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    3.1.2 字符串操作

    追加(push)

    在字符串尾部可以使用 push() 方法追加字符 char,也可以使用 push_str() 方法追加字符串字面量。这两个方法都是在原有的字符串上追加,并不会返回新的字符串。由于字符串追加操作要修改原来的字符串,则该字符串必须是可变的,即字符串变量必须由 mut 关键字修饰。示例代码如下:

    fn main() {
        let mut s = String::from("Hello ");
        s.push('r'); //Hello r
    
        s.push_str("ust!"); //Hello rust!
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    插入(insert)

    可以使用 insert() 方法插入单个字符 char,也可以使用 insert_str() 方法插入字符串字面量,与 push() 方法不同,这俩方法需要传入两个参数,第一个参数是字符(串)插入位置的索引,第二个参数是要插入的字符(串),索引从 0 开始计数,如果越界则会发生错误。由于字符串插入操作要修改原来的字符串,则该字符串必须是可变的,即字符串变量必须由 mut 关键字修饰。示例代码如下:

    fn main() {
        let mut s = String::from("Hello rust!");
        s.insert(5, ','); //Hello, rust!
        
        s.insert_str(6, " I like"); //Hello, I like rust!
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    替换(replace)

    1、replace

    该方法可适用于 String&str 类型。replace() 方法接收两个参数,第一个参数是要被替换的字符串,第二个参数是新的字符串。该方法会替换所有匹配到的字符串。该方法是返回一个新的字符串,而不是操作原来的字符串。示例代码如下:

    fn main() {
        let string_replace = String::from("I like rust. Learning rust is my favorite!");
        let new_string_replace = string_replace.replace("rust", "RUST");
        dbg!(new_string_replace);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    代码运行结果:

    new_string_replace = "I like RUST. Learning RUST is my favorite!"
    
    • 1

    2、replacen

    replacen方法可适用于 String&str 类型。replacen() 方法接收三个参数,前两个参数与 replace() 方法一样,第三个参数则表示替换的个数。该方法是返回一个新的字符串,而不是操作原来的字符串

    示例代码如下:

    fn main() {
        let string_replace = "I like rust. Learning rust is my favorite!";
        let new_string_replacen = string_replace.replacen("rust", "RUST", 1);
        dbg!(new_string_replacen);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    代码运行结果:

    new_string_replacen = "I like RUST. Learning rust is my favorite!"
    
    • 1

    3、replace_range

    该方法仅适用于 String 类型。replace_range 接收两个参数,第一个参数是要替换字符串的范围(Range),第二个参数是新的字符串。该方法是直接操作原来的字符串,不会返回新的字符串。该方法需要使用 mut 关键字修饰

    示例代码如下:

    fn main() {
        let mut string_replace_range = String::from("I like rust!");
        string_replace_range.replace_range(7..8, "R");
        dbg!(string_replace_range);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    代码运行结果:

    string_replace_range = "I like Rust!"
    
    • 1
    删除(delete)

    1、 pop —— 删除并返回字符串的最后一个字符

    该方法是直接操作原来的字符串。但是存在返回值,其返回值是一个 Option 类型,如果字符串为空,则返回 None。 示例代码如下:

    fn main() {
        let mut string_pop = String::from("rust pop 中文!");
        let p1 = string_pop.pop();
        let p2 = string_pop.pop();
        dbg!(p1);
        dbg!(p2);
        dbg!(string_pop);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    代码运行结果:

    p1 = Some(
       '!',
    )
    p2 = Some(
       '文',
    )
    string_pop = "rust pop 中"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    2、 remove —— 删除并返回字符串中指定位置的字符

    该方法是直接操作原来的字符串。但是存在返回值,其返回值是删除位置的字符串,只接收一个参数,表示该字符起始索引位置。remove() 方法是按照字节来处理字符串的,如果参数所给的位置不是合法的字符边界,则会发生错误。

    示例代码如下:

    fn main() {
        let mut string_remove = String::from("测试remove方法");
        println!(
            "string_remove 占 {} 个字节",
            std::mem::size_of_val(string_remove.as_str())
        );
        // 删除第一个汉字
        string_remove.remove(0);
        // 下面代码会发生错误
        // string_remove.remove(1);
        // 直接删除第二个汉字
        // string_remove.remove(3);
        dbg!(string_remove);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    代码运行结果:

    string_remove 占 18 个字节
    string_remove = "试remove方法"
    
    • 1
    • 2

    3、truncate —— 删除字符串中从指定位置开始到结尾的全部字符

    该方法是直接操作原来的字符串。无返回值。该方法 truncate() 方法是按照字节来处理字符串的,如果参数所给的位置不是合法的字符边界,则会发生错误。

    示例代码如下:

    fn main() {
        let mut string_truncate = String::from("测试truncate");
        string_truncate.truncate(3);
        dbg!(string_truncate);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    代码运行结果:

    string_truncate = "测"
    
    • 1

    4、clear —— 清空字符串

    该方法是直接操作原来的字符串。调用后,删除字符串中的所有字符,相当于 truncate() 方法参数为 0 的时候。

    示例代码如下:

    fn main() {
        let mut string_clear = String::from("string clear");
        string_clear.clear();
        dbg!(string_clear);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    代码运行结果:

    string_clear = ""
    
    • 1
    连接(concatenate)

    1、使用 + 或者 += 连接字符串

    使用 + 或者 += 连接字符串,要求右边的参数必须为字符串的切片引用(Slice)类型。其实当调用 + 的操作符时,相当于调用了 std::string 标准库中的 add() 方法,这里 add() 方法的第二个参数是一个引用的类型。因此我们在使用 +, 必须传递切片引用类型。不能直接传递 String 类型。++= 都是返回一个新的字符串。所以变量声明可以不需要 mut 关键字修饰

    let s1 = String::from("tic");
    let s2 = String::from("tac");
    let s3 = String::from("toe");
    
    // String = String + &str + &str + &str + &str
    let s = s1 + "-" + &s2 + "-" + &s3;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    String + &str返回一个 String,然后再继续跟一个 &str 进行 + 操作,返回一个 String 类型,不断循环,最终生成一个 s,也是 String 类型。

    s1 这个变量通过调用 add() 方法后,所有权被转移到 add() 方法里面, add() 方法调用后就被释放了,同时 s1 也被释放了。再使用 s1 就会发生错误。

    2、使用 format! 连接字符串

    format! 这种方式适用于 String&strformat! 的用法与 print! 的用法类似,详见格式化输出

    示例代码如下:

    fn main() {
        let s1 = "hello";
        let s2 = String::from("rust");
        let s = format!("{} {}!", s1, s2);
        println!("{}", s);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    代码运行结果:

    hello rust!
    
    • 1
    遍历字符串

    1、按字符遍历,如果你想要以 Unicode 字符的方式遍历字符串,最好的办法是使用 chars 方法,例如:

    for c in "中国人".chars() {
        println!("{}", c);
    }
    
    • 1
    • 2
    • 3

    输出如下

    中
    国
    人
    
    • 1
    • 2
    • 3

    2、按字节遍历,返回字符串的底层字节数组表现形式:

    for b in "中国人".bytes() {
        println!("{}", b);
    }
    
    • 1
    • 2
    • 3

    输出如下:

    228
    184
    173
    229
    155
    189
    228
    186
    186
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    3.2 元组

    元组是由多种类型组合到一起形成的,因此它是复合类型,元组的长度是固定的,元组中元素的顺序也是固定的。

    可以通过以下语法创建一个元组:

    fn main() {
        let tup: (i32, f64, u8) = (500, 6.4, 1);
    }
    
    • 1
    • 2
    • 3

    变量 tup 被绑定了一个元组值 (500, 6.4, 1),该元组的类型是 (i32, f64, u8)

    用模式匹配解构元组

    用同样的形式把一个复杂对象中的值匹配出来。

    fn main() {
        let tup = (500, 6.4, 1);
    
        let (x, y, z) = tup;
        
        println!("The value of y is: {}", y);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    . 来访问元组

    模式匹配可以让我们一次性把元组中的值全部或者部分获取出来,如果只想要访问某个特定元素,那模式匹配就略显繁琐,对此,Rust 提供了 . 的访问方式,元组的索引从 0 开始:

    fn main() {
        let x: (i32, f64, u8) = (500, 6.4, 1);
    
        let five_hundred = x.0;
    
        let six_point_four = x.1;
    
        let one = x.2;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    可以通过使用元组使函数返回多个值:

    fn main() {
        let s1 = String::from("hello");
    
        let (s2, len) = calculate_length(s1);
    
        println!("The length of '{}' is {}.", s2, len);
    }
    
    fn calculate_length(s: String) -> (String, usize) {
        let length = s.len(); // len() 返回字符串的长度
    
        (s, length)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    3.3 结构体

    3.3.1 结构体语法

    定义结构体

    一个结构体由几部分组成:

    • 通过关键字 struct 定义
    • 一个清晰明确的结构体 名称
    • 几个有名字的结构体 字段
    struct User {
        active: bool,
        username: String,
        email: String,
        sign_in_count: u64,
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    该结构体名称是 User,拥有 4 个字段,且每个字段都有对应的字段名及类型声明,例如 username 代表了用户名,是一个可变的 String 类型。

    创建结构体实例

    创建 User 结构体的实例

        let mut user1 = User {
            email: String::from("someone@example.com"),
            username: String::from("someusername123"),
            active: true,
            sign_in_count: 1,
        };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    有几点值得注意:

    1. 初始化实例时,每个字段都需要进行初始化
    2. 初始化时的字段顺序不需要和结构体定义时的顺序一致
    访问结构体字段

    必须要将结构体实例声明为可变的,才能修改其中的字段,Rust 不支持将某个结构体某个字段标记为可变。

     user1.email = String::from("anotheremail@example.com");
    
    • 1
    简化结构体创建

    当函数参数和结构体字段同名时,可以直接使用缩略的方式进行初始化

    fn build_user(email: String, username: String) -> User {
        User {
            email,
            username,
            active: true,
            sign_in_count: 1,
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    根据已有的结构体实例,创建新的结构体实例,例如根据已有的 user1 实例来构建 user2,Rust 为我们提供了 结构体更新语法

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

    因为 user2 仅仅在 email 上与 user1 不同,因此我们只需要对 email 进行赋值,剩下的通过结构体更新语法 ..user1 即可完成。

    .. 语法表明凡是我们没有显式声明的字段,全部从 user1 中自动获取。需要注意的是 ..user1 必须在结构体的尾部使用。

    结构体更新语法跟赋值语句 = 非常相像,因此在上面代码中,user1 的部分字段所有权被转移到 user2 中:username 字段发生了所有权转移,作为结果,user1 无法再被使用。值得注意的是:username 所有权被转移给了 user2,导致了 user1 无法再被使用,但是并不代表 user1 内部的其它字段不能被继续使用:

    let user1 = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };
    let user2 = User {
        active: user1.active,
        username: user1.username,
        email: String::from("another@example.com"),
        sign_in_count: user1.sign_in_count,
    };
    println!("{}", user1.active);
    // 下面这行会报错
    println!("{:?}", user1);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    即,把结构体中具有所有权的字段转移出去后,将无法再访问该字段,但是可以正常访问其它的字段

    3.3.2 元组结构体(Tuple Struct)

    结构体必须要有名称,但是结构体的字段可以没有名称,这种结构体长得很像元组,因此被称为元组结构体,例如:

        struct Color(i32, i32, i32);
        struct Point(i32, i32, i32);
    
        let black = Color(0, 0, 0);
        let origin = Point(0, 0, 0);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    元组结构体在希望有一个整体名称,但是又不关心里面字段的名称时将非常有用。例如上面的 Point 元组结构体,众所周知 3D 点是 (x, y, z) 形式的坐标点,因此我们无需再为内部的字段逐一命名为:x, y, z

    3.3.3 单元结构体(Unit-like Struct)

    单元结构体没有任何字段和属性,但是好在,它还挺有用。如果你定义一个类型,但是不关心该类型的内容, 只关心它的行为时,就可以使用 单元结构体

    struct AlwaysEqual;
    
    let subject = AlwaysEqual;
    
    // 我们不关心 AlwaysEqual 的字段数据,只关心它的行为,因此将它声明为单元结构体,然后再为它实现某个特征
    impl SomeTrait for AlwaysEqual {
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    如果想在结构体中使用一个引用,就必须加上生命周期。

    3.3.4 使用 #[derive(Debug)] 来打印结构体的信息

    #[derive(Debug)]
    struct Rectangle {
        width: u32,
        height: u32,
    }
    
    fn main() {
        let rect1 = Rectangle {
            width: 30,
            height: 50,
        };
    
        println!("rect1 is {:?}", rect1);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    此时运行程序,就不再有错误,输出如下:

    $ cargo run
    rect1 is Rectangle { width: 30, height: 50 }
    
    • 1
    • 2

    当结构体较大时,我们可能希望能够有更好的输出表现,此时可以使用 {:#?} 来替代 {:?},输出如下:

    rect1 is Rectangle {
        width: 30,
        height: 50,
    }
    
    • 1
    • 2
    • 3
    • 4

    还有一个简单的输出 debug 信息的方法,那就是使用 dbg!,它会拿走表达式的所有权,然后打印出相应的文件名、行号等 debug 信息,当然还有我们需要的表达式的求值结果。除此之外,它最终还会把表达式值的所有权返回!

    dbg!` 输出到标准错误输出 `stderr`,而 `println!` 输出到标准输出 `stdout
    
    • 1

    下面的例子中清晰的展示了 dbg! 如何在打印出信息的同时,还把表达式的值赋给了 width:

    #[derive(Debug)]
    struct Rectangle {
        width: u32,
        height: u32,
    }
    
    fn main() {
        let scale = 2;
        let rect1 = Rectangle {
            width: dbg!(30 * scale),
            height: 50,
        };
    
        dbg!(&rect1);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    最终的 debug 输出如下:

    $ cargo run
    [src/main.rs:10] 30 * scale = 60
    [src/main.rs:14] &rect1 = Rectangle {
        width: 60,
        height: 50,
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3.4 枚举

    3.4.1 枚举的语法

    枚举(enum 或 enumeration)允许通过列举可能的成员来定义一个枚举类型枚举类型是一个类型,它会包含所有可能的枚举成员, 而枚举值是该类型中的具体某个成员的实例。 如:

    enum PokerSuit {
      Clubs,
      Spades,
      Diamonds,
      Hearts,
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    结构体实现带值的枚举成员:

    enum PokerSuit {
        Clubs,
        Spades,
        Diamonds,
        Hearts,
    }
    
    struct PokerCard {
        suit: PokerSuit,
        value: u8
    }
    
    fn main() {
       let c1 = PokerCard {
           suit: PokerSuit::Clubs,
           value: 1,
       };
       let c2 = PokerCard {
           suit: PokerSuit::Diamonds,
           value: 12,
       };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    直接将数据信息关联到枚举成员上,可省去近一半的代码,例如:

    enum PokerCard {
        Clubs(u8),
        Spades(u8),
        Diamonds(char),
        Hearts(char),
    }
    
    fn main() {
       let c1 = PokerCard::Spades(5);
       let c2 = PokerCard::Diamonds('A');
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    任何类型的数据都可以放入枚举成员中: 例如字符串、数值、结构体甚至另一个枚举。以下代码:

    enum Message {
        Quit,
        Move { x: i32, y: i32 },
        Write(String),
        ChangeColor(i32, i32, i32),
    }
    
    fn main() {
        let m1 = Message::Quit;
        let m2 = Message::Move{x:1,y:1};
        let m3 = Message::ChangeColor(255,255,0);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    该枚举类型代表一条消息,它包含四个不同的成员:

    • Quit 没有任何关联数据
    • Move 包含一个匿名结构体
    • Write 包含一个 String 字符串
    • ChangeColor 包含三个 i32

    3.4.2 Opention枚举处理空值

    Option 枚举包含两个成员,一个成员表示含有值:Some(T), 另一个表示没有值:None,定义如下:

    enum Option<T> {
        Some(T),
        None,
    }
    
    • 1
    • 2
    • 3
    • 4

    其中 T 是泛型参数,Some(T)表示该枚举成员的数据类型是 T,换句话说,Some 可以包含任何类型的数据。OptionT(这里 T 可以是任何类型)是不同的类型。

    为了使用 Option 值,需要编写处理每个成员的代码。match 表达式就是这么一个处理枚举的控制流结构:它会根据枚举的成员运行不同的代码,这些代码可以使用匹配到的值中的数据。

    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }
    
    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    plus_one 通过 match 来处理不同 Option 的情况。

    3.5 数组

    在 Rust 中,最常用的数组有两种,第一种是速度很快但是长度固定的 array,第二种是可动态增长的但是有性能损耗的 Vector。这两个数组的关系跟 &strString 的关系很像,前者是长度固定的字符串切片,后者是可动态增长的字符串。其实,在 Rust 中无论是 String 还是 Vector,它们都是 Rust 的高级类型:集合类型。数组的具体定义很简单:将多个类型相同的元素依次组合在一起,就是一个数组。数组声明格式为:[类型; 长度]。结合上面的内容,可以得出数组的三要素:

    • 长度固定
    • 元素必须有相同的类型
    • 依次线性排列

    这里说的数组是 Rust 的基本类型,是固定长度的,这点与其他编程语言不同,其它编程语言的数组往往是可变长度的,与 Rust 中的动态数组 Vector 类似

    3.5.1 创建数组

    fn main() {
        //let a = [1, 2, 3, 4, 5]; 或者	
        let a: [i32; 5] = [1, 2, 3, 4, 5]; //包含5个i32类型的数
        let b = [3; 5]; //数组b中包含5个3
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3.5.2 数组越界访问

    下面是一个接收用户的控制台输入,然后将其作为索引访问数组元素的例子:

    use std::io;
    
    fn main() {
        let a = [1, 2, 3, 4, 5];
    
        println!("Please enter an array index.");
    
        let mut index = String::new();
        // 读取控制台的输出
        io::stdin()
            .read_line(&mut index)
            .expect("Failed to read line");
    
        let index: usize = index
            .trim()
            .parse()
            .expect("Index entered was not a number");
    
        let element = a[index];
    
        println!(
            "The value of the element at index {} is: {}",
            index, element
        );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    使用 cargo run 来运行代码,因为数组只有 5 个元素,如果我们试图输入 5 去访问第 6 个元素,则会访问到不存在的数组元素,最终程序会崩溃退出:

    Please enter an array index.
    5
    thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 5', src/main.rs:19:19
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
    
    • 1
    • 2
    • 3
    • 4

    这就是数组访问越界,访问了数组中不存在的元素,导致 Rust 运行时错误。

    当你尝试使用索引访问元素时,Rust 将检查你指定的索引是否小于数组长度。如果索引大于或等于数组长度,Rust 会出现 *panic*。这种检查只能在运行时进行,比如在上面这种情况下,编译器无法在编译期知道用户运行代码时将输入什么值,这种就是 Rust 的安全特性之一。

    数组元素为非基础类型时,应调用std::array::from_fn

    let array: [String; 8] = core::array::from_fn(|i| String::from("rust is good!"));
    
    println!("{:#?}", array);
    
    • 1
    • 2
    • 3

    3.5.3 数组切片

    切片允许你引用集合中的部分连续片段,而不是整个集合,对于数组也是,数组切片允许我们引用数组的一部分:

    let a: [i32; 5] = [1, 2, 3, 4, 5];
    
    let slice: &[i32] = &a[1..3];
    
    assert_eq!(slice, &[2, 3]);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    上面的数组切片 slice 的类型是&[i32],与之对比,数组的类型是[i32;5],简单总结下切片的特点:

    • 切片的长度可以与数组不同,并不是固定的,而是取决于你使用时指定的起始和结束位置
    • 创建切片的代价非常小,因为切片只是针对底层数组的一个引用
    • 切片类型[T]拥有不固定的大小,而切片引用类型&[T]则具有固定的大小,因为 Rust 很多时候都需要固定大小数据类型,因此&[T]更有用,&str字符串切片也同理

    数组综合示例:

    fn main() {
      // 编译器自动推导出one的类型
      let one             = [1, 2, 3];
      // 显式类型标注
      let two: [u8; 3]    = [1, 2, 3];
      let blank1          = [0; 3];
      let blank2: [u8; 3] = [0; 3];
    
      // arrays是一个二维数组,其中每一个元素都是一个数组,元素类型是[u8; 3]
      let arrays: [[u8; 3]; 4]  = [one, two, blank1, blank2];
    
      // 借用arrays的元素用作循环中
      for a in &arrays {
        print!("{:?}: ", a);
        // 将a变成一个迭代器,用于循环
        // 你也可以直接用for n in a {}来进行循环
        for n in a.iter() {
          print!("\t{} + 10 = {}", n, n+10);
        }
    
        let mut sum = 0;
        // 0..a.len,是一个 Rust 的语法糖,其实就等于一个数组,元素是从0,1,2一直增加到到a.len-1
        for i in 0..a.len() {
          sum += a[i];
        }
        println!("\t({:?} = {})", a, sum);
      }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    运行结果:

    [1, 2, 3]: 	1 + 10 = 11	2 + 10 = 12	3 + 10 = 13	([1, 2, 3] = 6)
    [1, 2, 3]: 	1 + 10 = 11	2 + 10 = 12	3 + 10 = 13	([1, 2, 3] = 6)
    [0, 0, 0]: 	0 + 10 = 10	0 + 10 = 10	0 + 10 = 10	([0, 0, 0] = 0)
    [0, 0, 0]: 	0 + 10 = 10	0 + 10 = 10	0 + 10 = 10	([0, 0, 0] = 0)
    
    • 1
    • 2
    • 3
    • 4

    数组虽然很简单,但是其实还是存在几个要注意的点:

    • 数组类型容易跟数组切片混淆,[T;n]描述了一个数组的类型,而[T]描述了切片的类型, 因为切片是运行期的数据结构,它的长度无法在编译期得知,因此不能用[T;n]的形式去描述
    • [u8; 3][u8; 4]是不同的类型,数组的长度也是类型的一部分
    • 在实际开发中,使用最多的是数组切片[T],我们往往通过引用的方式去使用&[T],因为后者有固定的类型大小
  • 相关阅读:
    【Mysql】Mysql内置函数介绍
    Avalonia常用小控件Menu
    wqs二分学习笔记
    C++ Reference: Standard C++ Library reference: C Library: cmath
    平均回复在5s内的快捷短语
    centos下编译安装各个版本的python
    计算机视觉|投影与三维视觉
    linux网络编程(四)多路I/O转接服务器
    Go-Python-Java-C-LeetCode高分解法-第七周合集
    java进行系统的限流实现--Guava RateLimiter、简单计数、滑窗计数、信号量、令牌桶
  • 原文地址:https://blog.csdn.net/qq_51601649/article/details/128346582