Rust基础入门(1)

258 阅读25分钟

环境安装使用

安装rustup

curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

出现下面信息为安装成功

Rust is installed now. Great!

安装C编译器

xcode-select --install

Cargo

cargo 提供了一系列的工具,从项目的建立、构建到测试、运行直至部署,为 Rust 项目的管理提供尽可能完整的手段。同时,与 Rust 语言及其编译器 rustc 紧密结合。

cargo check

当项目大了以后 build

Cargo.toml和Cargo.lock

Cargo.toml 和 Cargo.lock 是 cargo 的核心文件,它的所有活动均基于此二者。

package

package 中记录了项目的描述信息

[package]
name = "hello_world"
version = "0.1.0"
edition = "2021"

定义项目依赖项

在 Cargo.toml 中,主要通过各种依赖段落来描述该项目的各种依赖项:

  • 基于 Rust 官方仓库 crates.io,通过版本说明来描述
  • 基于项目源代码的 git 仓库地址,通过 URL 来描述
  • 基于本地项目的绝对路径或者相对路径,通过类 Unix 模式的路径来描述
[dependencies] 
rand = "0.3" 
hammer = { version = "0.5.0"} 
color = { git = "https://github.com/bjz/color-rs" } 
geometry = { path = "crates/geometry" }

创建新项目

cargo new hello_world

目录结构

├── Cargo.toml
└── src
    └── main.rs

运行项目

执行cargo run

   Compiling hello_world v0.1.0 (/Users/yuefeidu/Documents/rust/hello_world)
    Finished dev [unoptimized + debuginfo] target(s) in 2.22s
     Running `target/debug/hello_world`
Hello, world!

编译项目

刚刚直接使用cargo run 其实是执行了编译然后再运行,现在我们使用cargo build先编译在运行。

// 编译完成提示
Finished dev [unoptimized + debuginfo] target(s) in 0.03s

运行编译结果./target/debug/hello_world,发现目录结构有debug,在debug模式下编译速度会变快运行速度会变慢。

编译release版本

cargo build --release,发现新生成了release目录运行高性能程./target/release/hello_world

小知识

::是调用的意思 比如String::from就是调用String中的from。

基础入门

变量绑定与解构

1. 变量绑定

使用let设置变量,在rust中let变量分为可变与不可变。

不可变就是比如设置了一个let a = 4在后续代码中a = 8在编译中会抛出错误。

可变的使用方式为let mut a = 4在后续代码中改变可正常执行。

不可变带来的就是安全性,可变带来的就是更灵活。

2. 忽略未使用的变量

使用let _a = 4下划线忽略未使用变量否则在编译时会报错。

3. 变量解构

let 表达式不仅仅用于变量的绑定,还能进行复杂变量的解构:从一个相对复杂的变量中,匹配出该变量的一部分内容

fn main() { 
let (a, mut b): (bool,bool) = (true, false);
// a = true,不可变; b = false,可变 
println!("a = {:?}, b = {:?}", a, b); b = true; assert_eq!(a, b); }

在 Rust 1.59 版本后,我们可以在赋值语句的左式中使用元组、切片和结构体模式了。

struct Struct {
    e: i32
}

fn main() {
    let (a, b, c, d, e);

    (a, b) = (1, 2);
    // _ 代表匹配一个值,但是我们不关心具体的值是什么,因此没有使用一个变量名而是使用了 _这里同go
    [c, .., d, _] = [1, 2, 3, 4, 5];
    Struct { e, .. } = Struct { e: 5 };

    assert_eq!([1, 2, 1, 4, 5], [a, b, c, d, e]);
}

4. 常量

一说到常量可能都会想到constant/const。

在rust中:

  • 常量不允许使用 mut常量不仅仅默认不可变,而且自始至终不可变,因为常量在编译完成后,已经确定它的值。
  • 常量使用 const 关键字而不是 let 关键字来声明,并且值的类型必须标注。
  • 常量一般用大写。一般使用在需要硬编码的参数上。
const MAX_POINTS: u32 = 100_000;

5. 变量遮蔽

用下面代码来讲述

fn main(){
let a = 1;
let a = 1 + 2;

{
    let a = 5;
    println!("print a = {}", a);

}
println!("print a = {}", a);

}

上面执行代码结果为 5, 3. 后面的变量会替换前面的变量不同作用域变量互不干扰。

基本类型

数值类型

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

  • 数值类型: 有符号整数 (i8i16i32i64isize)、 无符号整数 (u8u16u32u64usize) 、浮点数 (f32f64)、以及有理数、复数
  • 字符串:字符串字面量和字符串切片 &str
  • 布尔类型: truefalse
  • 字符类型: 表示单个 Unicode 字符,存储为 4 个字节
  • 单元类型: 即 () ,其唯一的值也是 ()

数值类型

整数是没有小数部分的数字。之前使用过的 i32 类型,表示有符号的 32 位整数( i 是英文单词 integer 的首字母,与之相反的是 u,代表无符号 unsigned 类型)。下表显示了 Rust 中的内置的整数类型:

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

类型定义的形式统一为:有无符号 + 类型大小(位数)无符号数表示数字只能取正数和0,而有符号则表示数字可以取正数、负数还有0。就像在纸上写数字一样:当要强调符号时,数字前面可以带上正号或负号;然而,当很明显确定数字为正数时,就不需要加上正号了。有符号数字以补码形式存储。

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

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

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

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

这么多类型,有没有一个简单的使用准则?答案是肯定的, Rust 整型默认使用 i32,例如 let i = 1,那 i 就是 i32 类型,因此你可以首选它,同时该类型也往往是性能最好的。isize 和 usize 的主要应用场景是用作集合的索引。

整型溢出 举个例子u8是0-255 如果设置值256在debug时编译会报错,在--release下不会报错但是256=0 257 =1。 显示处理溢出: 要显式处理可能的溢出,可以使用标准库针对原始数字类型提供的这些方法:

  • 使用 wrapping_* 方法在所有模式下都按照补码循环溢出规则处理,例如 wrapping_add
  • 如果使用 checked_* 方法时发生溢出,则返回 None 值
  • 使用 overflowing_* 方法返回该值和一个指示是否存在溢出的布尔值
  • 使用 saturating_* 方法,可以限定计算后的结果不超过目标类型的最大值或低于最小值,例如:

浮点型

f32和f64的区别在于32位系统或者64位系统,目前在rust中两种类型的速度差不多。

使用浮点型做对比的时候需要注意,浮点型计算后精度会不一致,导致抛出错误。

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);
}

输出如下

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

NAN

所有跟nan交互都会返回nan,且nan不可以比较否则会报错,可以使用is_nan()方法。

数值运算

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]);
}

位运算

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

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

序列Range

在rust中可以使用1..5会生成1-5,'a'..='z'生成a到z,常用于循环。且序列只允许用于数字或字符类型。


for i in 1..=5 {
    print!("{}",i);
}
// 1,2,3,4,5

有理数和复数

Rust 的标准库相比其它语言,准入门槛较高,因此有理数和复数并未包含在标准库中:

  • 有理数和复数
  • 任意大小的整数和任意精度的浮点数
  • 固定精度的十进制小数,常用于货币相关的场景

好在社区已经开发出高质量的 Rust 数值库:num

按照以下步骤来引入 num 库:

  1. 创建新工程 cargo new complex-num && cd complex-num
  2. 在 Cargo.toml 中的 [dependencies] 下添加一行 num = "0.4.0"
  3. 将 src/main.rs 文件中的 main 函数替换为下面的代码
  4. 运行 cargo run
use num::complex::Complex;

 fn main() {
   let a = Complex { re: 2.1, im: -1.2 };
   let b = Complex::new(11.1, 22.2);
   let result = a + b;

   println!("{} + {}i", result.re, result.im)
 }

总结

  • Rust 拥有相当多的数值类型. 因此你需要熟悉这些类型所占用的字节数,这样就知道该类型允许的大小范围以及你选择的类型是否能表达负数
  • 类型转换必须是显式的. Rust 永远也不会偷偷把你的 16bit 整数转换成 32bit 整数
  • Rust 的数值上可以使用方法. 例如你可以用以下方法来将 13.14 取整:13.14_f32.round(),在这里我们使用了类型后缀,因为编译器需要知道 13.14 的具体类型

字符、布尔、单元、函数

字符(char):

Rust 的字符不仅仅是 ASCII,所有的 Unicode 值都可以作为 Rust 字符,包括单个的中文、日文、韩文、emoji 表情符号等等,都是合法的字符类型。Unicode 值的范围从 U+0000 ~ U+D7FF 和 U+E000 ~ U+10FFFF

布尔

就是true和false占1字符。

单元 单元类型就是 ()

函数

与其他语言函数基本没什么区别。

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

函数返回可以用return提前返回 也可以用表达式(x + y)。

特殊返回:

无返回值:

  • 函数没有返回值,那么返回一个 ()
  • 通过 ; 结尾的语句返回一个 ()
use std::fmt::Debug;

fn report<T: Debug>(item: T) {
  println!("{:?}", item);

}

永不返回的发散函数:

fn dead_end() -> ! { panic!("崩溃!"); }

所有权和借用

Rust 之所以能成为万众瞩目的语言,就是因为其内存安全性。在以往,内存安全几乎都是通过 GC 的方式实现,但是 GC 会引来性能、内存占用以及 Stop the world 等问题,在高性能场景和系统编程上是不可接受的,因此 Rust 采用了与众不同的方式:所有权系统。

栈按照顺序存储值并以相反顺序取出值,这也被称作后进先出。想象一下一叠盘子:当增加更多盘子时,把它们放在盘子堆的顶部,当需要盘子时,再从顶部拿走。不能从中间也不能从底部增加或拿走盘子!

增加数据叫做进栈,移出数据则叫做出栈

因为上述的实现方式,栈中的所有数据都必须占用已知且固定大小的内存空间,假设数据大小是未知的,那么在取出数据时,你将无法取到你想要的数据。

与栈不同,对于大小未知或者可能变化的数据,我们需要将它存储在堆上。

当向堆上放入数据时,需要请求一定大小的内存空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的指针, 该过程被称为在堆上分配内存,有时简称为 “分配”(allocating)。

接着,该指针会被推入中,因为指针的大小是已知且固定的,在后续使用过程中,你将通过栈中的指针,来获取数据在堆上的实际内存位置,进而访问该数据。

由上可知,堆是一种缺乏组织的数据结构。想象一下去餐馆就座吃饭: 进入餐馆,告知服务员有几个人,然后服务员找到一个够大的空桌子(堆上分配的内存空间)并领你们过去。如果有人来迟了,他们也可以通过桌号(栈上的指针)来找到你们坐在哪。

性能区别

在栈上分配内存比在堆上分配内存要快,因为入栈时操作系统无需进行函数调用(或更慢的系统调用)来分配新的空间,只需要将新数据放入栈顶即可。相比之下,在堆上分配内存则需要更多的工作,这是因为操作系统必须首先找到一块足够存放数据的内存空间,接着做一些记录为下一次分配做准备,如果当前进程分配的内存页不足时,还需要进行系统调用来申请更多内存。 因此,处理器在栈上分配数据会比在堆上分配数据更加高效。

所有权与堆栈

当你的代码调用一个函数时,传递给函数的参数(包括可能指向堆上数据的指针和函数的局部变量)依次被压入栈中,当函数调用结束时,这些值将被从栈中按照相反的顺序依次移除。

因为堆上的数据缺乏组织,因此跟踪这些数据何时分配和释放是非常重要的,否则堆上的数据将产生内存泄漏 —— 这些数据将永远无法被回收。这就是 Rust 所有权系统为我们提供的强大保障。

小结: 栈中保存的都是有已知大小且不可变的数据,不知大小且可变的数据先进入堆 堆做一些处理后分配内存给数据再存到栈。栈性能比堆好 因为堆需要处理内存空间分配以及为下一次准备。当代码调用函数时包括入参依次被压入栈中当函数调用结束后这些值将被从栈中按相反的顺序移除。

所有权规则

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

变量绑定和背后的数据交互

先看代码

let x = 5;
let y = x;

上面的代码没有造成所有权转移,在Rust中基本类型都是固定值所以存在栈中不需要在堆中分配,所以上面代码是x 赋值 5 y自动拷贝x的值并储存在栈中,所以不会造成所有权转移,下面再来看一段代码。

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

上面代码中String不是基本类型是复杂类型,所以指向了堆的一个空间 这里储存着他真实的数据,上面代码会出现两种情况。

  1. 拷贝 String 和存储在堆上的字节数组 如果该语句是拷贝所有数据(深拷贝),那么无论是 String 本身还是底层的堆上数据,都会被全部拷贝,这对于性能而言会造成非常大的影响
  2. 只拷贝 String 本身 这样的拷贝非常快,因为在 64 位机器上就拷贝了 8字节的指针8字节的长度8字节的容量,总计 24 字节,但是带来了新的问题,还记得我们之前提到的所有权规则吧?其中有一条就是:一个值只允许有一个所有者,而现在这个值(堆上的真实字符串数据)有了两个所有者:s1 和 s2

下面假设s2拷贝了s1的值后打印s1会出现报错为什么呢?因为上面说了Rust为了内存安全防止内存泄漏、内存污染一个变量只对应一个所有者 在作用域运行完成后s1和s2都会释放内存 但是他俩内存地址相同所有会二次释放也就是内存污染,在Rust中防止了这个问题出现 在s2=s1后 Rust会认为s1失效。相当于是s1把所有权交给了s2.

深拷贝和浅拷贝

深拷贝

在Rust中永远不会自动深拷贝,所有自动拷贝的都不是深拷贝如果需要用深拷贝在String::from中有clone()方法可以进行深拷贝不过需要注意少量的深拷贝可以接受大量或者重复深拷贝需要注意性能,深拷贝非常耗性能。

浅拷贝

浅拷贝只发生在栈上因此性能很高 使用场景很频繁。基本类型比如整型这种很简单的类型浅拷贝=深拷贝虽然没有使用clone但是依然与深拷贝一样。因为在编译时已知大小所以拷贝速度非常快所这里可以理解为栈上的深拷贝。

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

可以被copy的类型:

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

函数传值与返回

fn main() {
    let s = String::from("hello");  // s 进入作用域

    takes_ownership(s);             // s 的值移动到函数里 ...
                                    // ... 所以到这里不再有效

    let x = 5;                      // x 进入作用域

    makes_copy(x);                  // x 应该移动函数里,
                                    // 但 i32 是 Copy 的,所以在后面可继续使用 x

} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
  // 所以不会有特殊操作

fn takes_ownership(some_string: String) { // some_string 进入作用域
    println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放

fn makes_copy(some_integer: i32) { // some_integer 进入作用域
    println!("{}", some_integer);
} // 这里,some_integer 移出作用域。不会有特殊操作
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 并移出给调用的函数
}

所有权很强大,避免了内存的不安全性,但是也带来了一个新麻烦: 总是把一个值传来传去来使用它。 传入一个函数,很可能还要从该函数传出去,结果就是语言表达变得非常啰嗦,幸运的是,Rust 提供了新功能解决这个问题。

复合类型

切片

String

由于大部分字符串是可变的 所有String是储存在堆上的。

字符串的操作

追加(push)

在字符串尾部可以使用 push() 方法追加字符 char,也可以使用 push_str() 方法追加字符串字面量。

插入(insert)

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

替换(replace)

  1. replace: 方法适用于String与&str,replace接收两个参数第一个参数为替换的字符串,第二个为新字符串,该方法会返回一个新的字符串,而不是操作的字符串。
  2. replcen: 方法适用于String与&str,replcen接收三个参数,前两个参数与replace一致,第三个参数为需要替换的个数,该方法会返回一个新的字符串而不是操作的字符串。
  3. replace_range:方法适用于String,接收两个参数,第一个参数为范围range,第二个参数为替换的新字符串,该方法不返回新的字符串会改变原有字符串所有需要类型为mut。

删除(delete)

  1. pop(): 删除最后一个元素返回删除的元素 返回值类型为Option。
  2. remove(): 有一个参数为删除的索引位置,会返回删除掉的字符串,该方法是按照字节来删除的如果给的索引不是合法边界会报错。
  3. truncate():删除接收到索引值后面的全部内容,该方法按照字节来删除且操作的是原字符串无返回值。
  4. clear(): 该方法操作的是原字符串无返回值,清空字符串内容相当于truncate(0)。

连接(concatenate)

  1. 使用+、+=拼接字符串,相当于调用了add()方法 fn add(self, s: &str) -> String,self相当于是String类型的字符串。
  2. 使用format!("{}{}",s1,s2);同样可以实现字符串拼接。

元组

元组是由多种类型组合到一起形成的,因此它是复合类型,元组的长度是固定的,元组中元素的顺序也是固定的。下面用代码举例子说明。


fn main() {
    let tup: (i32,u32,&str) = (-32,32,"abc");
    let (x,y,z) = tup;
    println!("{}", tup.0); // -32
    println!("x = {} y = {} z = {}", x, y , z); // x = -32 y = 32 z = abc

}

使用场景


fn main() {
    let s = String::from("abc");
    let (str, len) = get_str_len(s);
    println!("{} {}", str, len); // abc 3
}

fn get_str_len(s: String) -> (String, usize) {
    let length = s.len();

    (s, length)
}

结构体

struct类似其他语言中的object、record,可以更清晰的定义对象,struct初始化实例是需要注意下面两点。

  • 初始化实例时,每个字段都需要进行初始化。
  • 初始化时的字段顺序不需要和结构体定义时的顺序一致。
  • 使用.操作符访问内部字段。 代码介绍一下。
// 定义一个用户结构体
struct User {
    username: String,
    age: u16,
    avatar: String,
    sex: u8,
}

let user1 = User { 
     username: String::from("name"),
     age: 26,
     avatar: String::from("https://xxxx.com"),
     sex: 0,
}

// 创建可变实例

let mut user2 = User { 
     username: String::from("name"),
     age: 26,
     avatar: String::from("https://xxxx.com"),
     sex: 0,
}
user2.age = 27;

// 与ts一样直接简单引入user1 引用后user1的所有权规user3所以后续user1无法使用但是可以使用user1中基本类型比如user1.age
let user3 = User {
age: 16,
..user1
}

枚举

与其他语言枚举形式一样,用来统一说明类型。

enum Option<T>{
    Type1(T),
    Type2(i32),
    Type3
}

数组

在rust中分为数组和动态数组,数组是储存在栈上的性能非常高,是静态值,动态数字顾名思义可以随意设置,是储存在堆上的。

数组

在rust中编译时会提示下标越界的问题。

  • 创建一个数组 let array = [1,2,3,4,5]
  • 创建一定数量同样值的数组 let array = [3; 8]
  • 创建Stirng类型重复值 let array: [String; 8] = std::array::from_fn(|_i| String::from("hello"))

总结:

  • 数组类型容易跟数组切片混淆,[T;n]描述了一个数组的类型,而[T]描述了切片的类型, 因为切片是运行期的数据结构,它的长度无法在编译期得知,因此不能用[T;n]的形式去描述
  • [u8; 3][u8; 4]是不同的类型,数组的长度也是类型的一部分
  • 在实际开发中,使用最多的是数组切片[T] ,我们往往通过引用的方式去使用&[T],因为后者有固定的类型大小

流程控制

if / else 就不赘述了。

if true {} else if c == 1 {} else{}

for循环

for 元素 in 集合 {}

使用方法等价使用方式所有权
for item in collectionfor item in IntoIterator::into_iter(collection)转移所有权
for item in &collectionfor item in collection.iter()不可变借用
for item in &mut collectionfor item in collection.iter_mut()可变借用
// 第一种
let collection = [1, 2, 3, 4, 5];
for i in 0..collection.len() {
  let item = collection[i];
  // ...
}

// 第二种
for item in collection {

}

  • 性能:第一种使用方式中 collection[index] 的索引访问,会因为边界检查(Bounds Checking)导致运行时的性能损耗 —— Rust 会检查并确认 index 是否落在集合内,但是第二种直接迭代的方式就不会触发这种检查,因为编译器会在编译时就完成分析并证明这种访问是合法的
  • 安全:第一种方式里对 collection 的索引访问是非连续的,存在一定可能性在两次访问之间,collection 发生了变化,导致脏数据产生。而第二种直接迭代的方式是连续访问,因此不存在这种风险( 由于所有权限制,在访问过程中,数据并不会发生变化)。

while

与其他语言一致

loop

无限循环可以使用break跳出。

模式匹配

match

与其他语言switch类似,其中_类似default。

  • match 的匹配必须要穷举出所有可能,因此这里用 _ 来代表未列出的所有可能性
  • match 的每一个分支都必须是一个表达式,且所有分支的表达式最终返回值的类型必须相同
  • X | Y,类似逻辑运算符 ,代表该分支可以匹配 X 也可以匹配 Y,只要满足一个即可
enum Direction {
    East,
    West,
    North,
    South,
}

fn main() {
    let dire = Direction::South;
    match dire {
        Direction::East => println!("East"),
        Direction::North | Direction::South => {
            println!("South or North");
        },
        _ => println!("West"),
    };
}



match target {
    模式1 => 表达式1,
    模式2 => {
        语句1;
        语句2;
        表达式2
    },
    _ => 表达式3
}

if let匹配

当值需要匹配一个值的时候用。

    let v = Some(3u8);
    match v {
        Some(3) => println!("three"),
        _ => (),
    }
  

matches!宏

enum MyEnum {
    Foo,
    Bar
}

fn main() {
    let v = vec![MyEnum::Foo,MyEnum::Bar,MyEnum::Foo];
    v.iter().filter(|x| matches!(x, MyEnum::Foo));

}

let foo = 'f';
assert!(matches!(foo, 'A'..='Z' | 'a'..='z'));

let bar = Some(4);
assert!(matches!(bar, Some(x) if x > 2));


当你只要匹配一个条件,且忽略其他条件时就用 if let ,否则都用 match

注意变量遮蔽类似下面这样 最好不要用同名命名。

fn main() {
   let age = Some(30);
   println!("在匹配前,age是{:?}",age);
   if let Some(age) = age {
       println!("匹配出来的age是{}",age);
   }

   println!("在匹配后,age是{:?}",age);
}

// 运行结果

在匹配前,age是Some(30)
匹配出来的age是30
在匹配后,age是Some(30)