Rust 基础入门

  • rust 基础总结

    1
    2023-08-29 初始化

导语

工作原因,需要抱紧 Rust, 那来吧,那个传说中的入门曲线….名不虚传…😂😒

这并非教程,是个人刷 Rust 圣经的记录,以备回看.

开胃菜

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
// Rust 程序入口函数,还是 main,该函数目前无返回值
fn main() {
// 使用let来声明变量,进行绑定,a 是不可变的
// 编译器会推断类型:i32,有符号32位整数,rust 中编译器存在感真强
// 语句的末尾必须以分号结尾 c/java
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的值代入进来, 因为编译器非常智能,不需要 %d %f 这样标明类型的占位符了.
println!("( a + b ) + ( c + d ) = {}", e);
}

// 定义一个函数,输入两个i32类型的32位有符号整数,返回它们的和
fn add(i: i32, j: i32) -> i32 {
// 可以省略return | 这个很不一样,只在函数式编程中见过
i + j
}

还有另外几个需要注意的点

  • 字符串是双引号. 字符是单引号;
  • 占位符只有 {},具体类型 编译器会知道,无需再写 %d 啥的;

编译器行为

一些调试编写代码常用 trick

可以以注解形式,控制一些编译器行为, 这些行为方便了 原型时候 调试代码;

1
2
#![allow(unused_variables)]  允许未使用变量
#[allow(dead_code)] 允许死循环

_x 下划线开头的变量将被 rust 编译器忽略,方便编写初始代码;

derive 派生特征: 类似注解,为对象实现默认特征

  • #[derive(Debug)]: 见过很多了, println!("{:?}", s) 可以打印整个结构体;
  • Copy

(插入) 复杂结构的打印输出

1
2
3
4
5
6
7
8
9
10
11
12
struct Rectangle {
width: u32,
height: u32,
}

fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {}", rect1);
}

下面这个结构体是打印不出来的, 编译器: 结构体没有实现 Display.. 难怪,结构体完全不同,能默认实现 Display 才怪.

1
2
3
`Rectangle` doesn't implement `std::fmt::Display`
the trait `std::fmt::Display` is not implemented for `Rectangle`
in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead

按照提示换用 {:#?},编译器继续

1
2
3
`Rectangle` doesn't implement `Debug`
the trait `Debug` is not implemented for `Rectangle`
add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`

手动 impl Debug or 添加 #[derive(Debug)], 那就添加; 能输出了;

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

如果默认格式不满意就只能自行实现 Display 了;

还有另一种是 dbg!(xxx): 打印出 表达式值 文件名 行号 等信息;

  • 输出到标准错误输出 stderr,而 println! 输出到标准输出 stdout
1
2
3
4
5
dbg!(rect1);
// output
[src/main.rs:13] rect1 = Rectangle {
width: 60,
height: 50,

变量绑定与解构

let a = 1 语法上等价于 赋值语句,这里意味着 a 与 1 的值绑定

  • 涉及到了 所有权 的概念

默认 rust 变量不可变, 可变 -> let mut a = 2.

  • 不可变带来了内存安全和性能的提升
  • 和 kotlin 的不可变 val 有些类似 一点也不像

_x 下划线开头的变量将被 rust 编译器忽略,方便编写初始代码;

变量解构: 与 py 中相同的概念,从变量中直接拆出部分内容;

1
2
3
4
5
6
7
8
9
// 与 py 类似的语法
let (a, mut b): (bool,bool) = (true, false);

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

常量… rust 居然还有这个概念..

1
2
3
// const 声明, 命名习惯上 全大写字母 下划线
// 作用域 相当于全局
const MAX_POINTS: u32 = 100_000;
  • 不可变 变量 和 常量还是有点区别,后面再详聊;
    • 常量 作用域是整个程序 or 模块. 常量值 和 类型 必须在编译时就确定;

变量遮蔽: 允许多个 let 声明同名变量, 后面会顶替前面; 前后是完全不同的变量,只是同名而已.

  • 还有个作用域的事, { } 内继承外部定义的变量,但内部定义变量作用域仅在 内部; 最后 println! 输出的还是 let x = x+1 定义的 x;
1
2
3
4
5
6
7
8
9
10
11
fn main() {
let x = 5;
// 在main函数的作用域内对之前的x进行遮蔽
let x = x + 1;
{
// 在当前的花括号作用域内,对之前的x进行遮蔽
let x = x * 2;
println!("The value of x in the inner scope is: {}", x);
}
println!("The value of x is: {}", x);
}

对应 练习

基本类型

rust 不是动态语言,且因为所有权等概念的引入导致 类型相当相当重要;;;

  • 必须指明变量类型,大部分情况下编译器足够聪明,不需要操心..但是总有二般
  • let guess = "42".parse().expect("Not a number!"); 从字符串 "42" 解析出整数, 编译器要能推断出这个,都能自己写程序了.
  • 这里必须显式补充上,要不然直接报错: let guess: i32 = "42".parse().expect("Not a number!");

有 3 点 rust 与 c/c++ 完全不同

  • 数值类型相当多,且泾渭分明. 数字后面可以跟着类型 let a = 8i8
  • 不存在隐式类型转换,所有转换必须是显式;
  • 数值可以直接调用方法…这一点又非常像 py.. a.is_nan

基本类型

  • 数值类型:
    • 有符号整数 (i8, i16, i32, i64, isize)
    • 无符号整数 (u8, u16, u32, u64, usize)
    • 浮点数 (f32, f64)
    • 有理数、复数
  • 字符串:字符串字面量 和 字符串切片 &str
  • 布尔类型: truefalse
  • 字符类型: 表示单个 Unicode 字符,存储为 4 个字节
  • 单元类型: 即 () ,其唯一的值也是 () ^21ffc3
    • 这是最新鲜的…似乎是类似 c/c++ 的 void

整数

(i8, i16, i32, i64, isize) / (u8, u16, u32, u64, usize) 没了

  • 长度直接写在脸上了, 除了 isize / usize 是架构决定
  • 默认 i32 一把梭子

十进制 98_200 十六进制 0x 八进制 0o 二进制 0b

整数溢出问题: 编译器 –debug 模式会检查,但 –release 就直接按照补码溢出来操作了…

  • 处理这些情况,使用标准库的拓展即可:
  • wrapping_* 一切按照补码溢出操作; checked_* 溢出直接 None (rust 居然也有这个值); overflowing_* 是否溢出,返回 bool; saturating_* 值达到最大/最小值;

浮点数

f32 f64 类似于 float double

  • 不能当作 hashmap 的 key,这个还是有点不一样

3 倍注意: 浮点数精度问题, C 中也常见,但 rust 要求更加严格;

1
2
3
4
5
6
7
fn main() {
let abc: (f32, f32, f32) = (0.1, 0.2, 0.3);
let xyz: (f64, f64, f64) = (0.1, 0.2, 0.3);

assert!(abc.0 + abc.1 == abc.2);
assert!(xyz.0 + xyz.1 == xyz.2);
}

都是 0.1+0.2 与 0.3 比较, f32 就相等, f64 就不相等;

  • f32 下.加完都是 3e99999a
  • f64 下精度高太多了, 3fd33333333333343fd3333333333333 的比较.

防御性编程: (0.1_f64 + 0.2 - 0.3).abs() < 0.00001 是最好的解决办法;

NaN

数学上未定义结果统一 NaN… NaN != 0,无法用于比较,比较直接 painc;

x.is_nan() 防御性编程;

运算

普通运算 + - * / %

位运算 & | ^ ! << >>

序列

1..5 生成 1 到 4; 1..=5 生成 1 到 5;

字符同理 'a'..'z''a'..='z'

  • 这是 py 吗…

有理数 复数

rust 标准库并没有支持有理数 复数,而是需要第三方库 num.

1
cargo add num

字符 布尔 单元

来个 rust 震惊: 所有的 Unicode 值都可以作为 Rust 字符, 包括单个的中文 日文 韩文 emoji 表情符号等等,都是合法的字符类型.

  • 👽 我也是

4 字节. '' 单引号

bool: true false, 1 字节 (不是 1bit,看清楚)

单元类型; () 有且仅有这个 () ^e508df

  • 看似无返回的函数返回的就是 ()
  • 占位但不占用任何内存, map 不关注值 只关注 key 时候,() 可以作为值; (类似 go 的 struct{})

语句 表达式

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

一般编程语言中并没有特别区分 语句 和 表达式, rust 则是分的很开.

表达式是要返回值; 语句是纯执行没有返回值;

  • 函数就是表达式
  • 表达式不能包含分号

这一块的概念触及了更深层次,新东西太多,作者省略了一些细节.具体请看 语句和表达式

  • 似乎概念上 表达式的写法更加简洁,暂时没有更多实感.

函数

语法上 rs 的函数与带类型标注的 py 很像.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn add(i: i32, j: i32) -> i32 {
i + j
}

fn plus(x: i32) -> i32 {
if x > 5 {
return x - 5;
}
return x + 5;
}
fn plus2(x: i32) -> i32 {
if x > 5 {
x - 5
} else {
x + 5
}
}
  • 返回可以不同 (表达式); plus plus2 等价;
  • 关键词 fn 不同

一般是蛇形命名 (全小写 下划线);

类似 c 中的 void 无返回的函数,实际上返回的是 [[#^21ffc3|单元类型]]

发散函数

类似 void 的还是会返回 (),但 发散函数 是真的一去不回….

使用 ! 作为函数的返回类型

1
2
// 真 dead 函数
fn dead() -> ! {}

所有权 与 借用

堆 和 栈 的区别, 入栈要求固定大小,速度快; 堆则较为无序,速度慢;

粗看下来,有些类似 指针 + 所有权; 作用域 = 生命周期 ; 有些类似 面向对象语言中, 对象 生命周期的概念..通过编译器 严格遵守 生命周期 和 所有权, 避免了 对象 (?) GC..

感觉有点像 对象生命周期管理 的思想 用来管理 内存里的一切;;


先看一段非常典型的 c 内存错误..类似的炸弹会存在在任何地方..

1
2
3
4
5
6
int* foo() {
int a; // 变量a的作用域开始
a = 100;
char *c = "xyz"; // 变量c的作用域开始
return &a;
} // 变量a和c的作用域结束

^fb6442

  • 程序返回了一个指向局部变量的指针,局部变量销毁后就成了 野指针;

所有权和借用 还涉及到了 堆 和 栈

  • 栈 出栈入栈要求长度类型确定,速度很快,长度确定因此找某个值也很快;
  • 堆 不定长,不定类型,存入访问速度比栈慢,但能处理类型多很多.

c/c++ 中堆上数据跟踪非常复杂,但 rust 要全管理起来 -> 所有权 和 生命周期

所有权

3 个规则

  • 每个值只有一个所有者;
  • 一个值的所有者只能是 一个;
  • 所有者离开作用域,值就 drop (drop!)
1
2
3
4
// x 拥有所有权
let x = String::from("hello");
let y = x;
println!("{}", x); // 报错

let 会将 x 的所有权转移到 y,因此 x 不再有效, println 就会报错; –> 所有者仅 1 个

1
2
3
4
// 但基本类型无碍
let x = 1;
let y = x; // 栈上的拷贝非常快
println!("{}", x); //正常

同样的逻辑下, 基本类型却没事… –> 基本类型是在 栈 中, 栈内复制非常快,因此 let 并没有发生所有权转移,而是复制了一份 x 的副本给 y;

  • 其实是 Copy 特征
1
2
3
let x: &str = "hello, world";
let y = x;
println!("{},{}",x,y); // 正常

但是字符串不是在堆中吗 😵… 这里引出了 引用 的概念.. x 是指向字符串的引用,也在栈中,因此可以直接复制..

rust 永远不会自行创建 深拷贝,但又 x.clone() 手动执行深拷贝 (重复执行,对性能影响很大)

上面说的基本类型可以自动拷贝 (栈上的复制), 并不限于基本类型; –> 任何 不需要分配内存或某种形式的资源 就可以 自动拷贝 (也叫实现了 Copy);

  • 基本类型
  • 元组, 当且 包含类型也只有 基本类型时;
  • 不可变 引用

变量传递给函数,函数返回值赋予变量 也有所有权的转移; // 这真没想到;

1
2
3
4
5
6
7
let s = String::from("hello");  // s 进入作用域
takes_ownership(s); // s 的值移动到函数里,下面将无法使用 s

let s2 = String::from("hello"); // s2 进入作用域
let s3 = takes_and_gives_back(s2); // s2 被移动到
// takes_and_gives_back 中,
// 它也将返回值移给 s3

这样来回传递当然非常麻烦 –> 借用和引用

  • 非常类似 权限控制的指针,或者说就是;

let 的操作,对 复杂类型是 移动所有权, 对简单类型就是完整拷贝;

引用 和 借用

语法和 c/c++ 指针完全相同 & * ;

为了安全性, 默认引用是不能修改值的; –> 可变引用; let r = &mut s

还是为了完全性, 类似读者写者问题的解决方案:

  • 不可变引用 与 可变引用 只能同时存在一个; ^xlg6a4
  • 不可变引用数量 可以有多个, 可变引用 只能有一个;

引用的作用域 呃, 非常独特..(⊙﹏⊙)..

最开始时候, 引用作用域判定规则和变量一致,这没啥..

但是叠加上: 变量释放时必须没有任何引用; 要了命了… 写代码时候要时刻注意 变量 + 引用的范围,随时加中括号..//这谁受得了///

最新编译器中: 引用作用域等于最后一次使用该引用的位置 == 解放

1
2
3
4
5
6
7
8
9
10
fn main() {
let mut s = String::from("hello");

let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2); // 新编译器中,r1,r2 作用域到此为止

let r3 = &mut s; // 老编译器下,r1 r2 有效,触发 不能同时可变和不可变引用
println!("{}", r3); // 新编译器下 r3 作用域结束
}

对于这样的 引用作用域,还有个专门的名称: Non-Lexical Lifetimes(NLL)

悬垂引用/悬垂指针 类似 [[#^fb6442]] 的情况, 变量被 drop 了,引用/指针 还存在; rust 编译器会阻止这一切;

复合类型

复合类型这里会提到 字符串/切片 元组 结构体 枚举 还有 数组, 大都见过,但是 rust 中会非常不一样.

字符串 (和 Go 很像)

语法层面上 rust 只有一种字符串类型: str,但 str 不可修改,所有权变化很乱, –> 字符串的切片 &str (跟 go 一模一样), 可变就交给了 标准库的 String 类型

  • 标准库还有更多类型 ( xxStr ),但常用就上面俩

rust 中字符是 unicode 4 字节,那么 字符串 肯定也是 4*字符量 的长度吧…并不是, 字符串是 utf-8.. 字符串中字符的长度是 1-4 变长…

  • 这个特性使得字符串处理与其他语言完全不同 –> 禁止字符串索引访问,虽然有切片..
  • 同时所有权等概念, 让这一切更加复杂

切片 (go 基本相同): 本质上不可变引用

  • let hello = &s[0..5];
  • &s[0..2];&s[..2]; | &s[4..len]&s[4..];
  • &s[0..len]&s[..];
  • 特别的一点: let s = "Hello, world!"; 字符串字面量 s 是切片 &str 类型

String 类型

1
2
3
let s1 = String::new(); // 空的 String 对象
let s2 = "Hello, Rust!".to_string(); // 包含字符串字面量的 String 对象
let s3 = String::from("Hello, world!"); // 包含字符串字面量的 String 对象

String&str 转换

  • 上面新建 String 里就是 String::from("hello,world")"hello,world".to_string()
  • 转换回去就直接取引用 (切片);
    • 这里有隐式的类型转换 Deref 解引用

禁止字符串索引访问, 即使切片也得注意索引范围;

  • utf8 变长,索引取到中间值无意义,直接报错;
  • 性能问题,期望性能始终为 O(1) 但始终难以保证

字符串操作

s.push('!'); s.push_str("rust"); s.insert(5, ','); s.replace_range(7..8, "R");

  • 操作原字符串, 需要 mut; 仅适用于 String

s.replace("rust", "RUST") s.replacen("rust", "RUST", 1)

  • 返回新字符串,不需要 mut; 适用于 String &str

删除: 都需要 mut ,仅适用于 String

  • pop 删除最后一个字符;
  • remove(5): 删除单个字节
  • truncate(3): 删除索引到尾部 (字节)
  • clear(): 清空

连接:

  • + += : 相当于标准库的 std::sting 的 add 方法; 要求第二个必须是 &str 类型,返回 String 类型;
  • format: 类似 print, 适用于 String&str: format!("{} {}!", s1, s2);

小坑

字符串内: 所有权的坑

调用方法 == 使用了引用, 非常容易出现 可变不可变 同时存在

1
2
3
4
5
6
7
8
9
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // <- 这里出错
println!("the first word is: {}", word);
}
fn first_word(s: &String) -> &str {
&s[..1]
}

代码看似挺正常的… 但是 s.clear() 就会出错….

  • 传入 first_word 的是一个不可变引用, println! 调用的 2
  • 但是但是, s.clear() 是个方法,对自身修改,需要的 可变引用
  • 可变引用 + 不可变引用 同时存在 + ![[Rust 基础入门#^xlg6a4]] == bong !

任何直接消费 非实现 copy 类型,都有所有权的转移

1
2
3
4
5
6
7
8
9
fn main() {
let s1 = String::from("hello,");
let s2 = String::from("world!");
// 在下句中,s1的所有权被转移走了,因此后面不能再使用s1
let s3 = s1 + &s2; // 任何直接消费 非实现 copy 类型,都有所有权的转移
assert_eq!(s3,"hello,world!");
// 下面的语句如果去掉注释,就会报错
// println!("{}",s1);
}

s1 在 let s3 = s1 + &s2; 就失效了

  • + 相当于 add 函数, s1 又是 String 非基本类型,所有权转移到了 add 内部 (相当于给函数传递了 变量 本身,所有权转移进去了)
  • add 执行完, s1 就被 drop 了…

其他

转义

1
2
\x73 直接转成 ascII or unicode 输出
\\x73 原样 \\x73 输出

utf-8

1
2
3
4
5
6
7
8
9
// 以 unicode 遍历字符串
// 输出 中 国 人
for c in "中国人".chars() {
println!("{}", c);
}
// 这样是纯字节了
for b in "中国人".bytes() {
println!("{}", b);
}

以 utf-8 获取子串,标准库无能为力 –> utf8_slice

元组

任意类型 () 常用于返回值 (这一点挺像 py)

访问: 解构 or x.2

1
2
3
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
let x = tup.0;

结构体

语法和 C 很像, 但没有指针和分号; struct , 分割 声明类型;

1
2
3
4
5
6
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}

创建实例时,每个字段都得初始化, 但 顺序无所谓.

1
2
3
4
5
6
let user1 = User {
email: String::from("[email protected]"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};

访问结构体成员 x.a; 修改成员要求 实例必须是 mut 才行

1
2
// user1 必须声明成 mut 
user1.email = String::from("[email protected]");

结构体初始化和创建,每一个字段都得精心维护,太繁琐了,有一些简便写法

1
2
3
4
5
6
7
8
9
// 函数参数与字段名 重名,可省略一个声明;
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}

已有实例创建新实例 (可能有 所有权转移)

  • 所有权一旦转移, use1 就可能 一部分字段可访问,一部分无效;
1
2
3
4
5
6
// ..展开已有实例;
// 千万注意: ..user1 的成员有可能会发生所有权转移
let user2 = User {
email: String::from("[email protected]"),
..user1
};

rust 结构体肯定不止 C 那样,还有几类特殊的结构体;

1
2
3
4
5
6
// 省略字段名, 按照索引号访问 -> 也叫元组结构体
struct Color(i32, i32, i32);
// [[#^e508df|单元类型]] 是个占位符(描述行为的),后面特征(类似接口)会用到
struct AlwaysEqual;
// 甚至 struct 也不是必须的 | 这也是元组结构体
let person = (String::from("John"), 30);

以上都没涉及到一个情况: 字段取到 引用 类型…. 在 学习生命周期前暂时不涉及

  • 应该 引用 要在 变量本身生命周期 内, 而又套上 结构体 就更复杂了…

枚举

将一类的值全包起来,不限制类型;

  • rust 枚举 可以关联其他类型 (这一点拓展了很多使用场景)
1
2
3
4
5
6
7
8
enum PokerSuit {
Clubs(u8),
Spades(u8),
Diamonds(u8),
Hearts(u8),
}
let c1 = PokerCard::Spades(5);
let c2 = PokerCard::Diamonds(13);

组织数据结构,屡试不爽…

1
2
3
4
5
6
7
8
9
10
11
12
// 枚举嵌套结构体... 有点干了泛型的活..
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
// 标准库的例子,两个后续处理流程显示的 结构
enum Websocket {
Tcp(Websocket<TcpStream>),
Tls(Websocket<native_tls::TlsStream<TcpStream>>),
}

Null 处理

java 中最恨这个,其次是 getter/setter; 一不留神就程序没了; 所以 rust 直接不要 null 了..

rust 中任何可能为 null 的类型,得套上 Option 枚举, Option<T>T 不能直接运算,得配合 null 的处理才行. 相当于强制用户必须处理 null ..(习惯了,习惯了..吸氧..)

1
2
3
4
5
6
7
8
9
10
enum Option<T> {
Some(T),
None,
}
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}

^bce572

数组 (不可变数组 array)

前排提醒 这里数组特指 不可变数组 array; rust 中 可变数组 是 Vector ;

array 中括号 []; 不可变; 定长; 类型一致; –> 在栈中;

  • 定长,所以能 索引 访问了; 越界会直接 pannic…
  • 快捷声明仅对 栈上 能直接 copy 的类型生效..
1
2
3
let a = [1,1,1,1,1]
// 快捷声明 1,重复 5 次 ,但有坑
let b = [1; 5]

不能直接 Copy 的类型有要用 –> std::array::from_fn

1
2
3
4
5
6
7
use std::array::from_fn;
fn main() {
let arr = from_fn(|i| i * 2);
// 有个坑? 把 assert_eq 去掉,则 arr 声明有误..
// arr 长度来源于比较的数组, 确实不能带着其他语言的思维惯性 推测 rust 编译器行为
assert_eq!(arr, [0, 2, 4, 6, 8]);
}

当然集合类型都有切片

  • array 类型是 [T;n] , 切片是 [T] 这俩千万别混淆了.
  • 切片创建代价非常小
  • 切片类型 [T] 大小不固定, 切片引用类型 &[T] 大小固定 (只是个引用), 因此引用更常见.
1
2
3
let a: [i32; 5] = [1, 2, 3, 4, 5];
let slice: &[i32] = &a[1..3];
assert_eq!(slice, &[2, 3]);

流程控制

if - else while loop 以及 continue 和 break

  • 关键字还都蛮熟悉的, loop 是个死循环,在哪里见过呢,似乎是 vb..
  • 关键是 rust 融合了很多高级语言的特性,本身定位又是系统编程..高低混合..这个乱啊..
  • continuebreak 多了一个标签 控制 (另类的 goto?)
1
2
3
4
5
6
// 最常见
if condition == true {// A…
} else if {// B…}
else { // final }
// 也有赋值的做法, 这里是个表达式 一定要有 `;`
let number = if condition { 5 } else { 6 } ;

for 循环有个大坑 (所有权), 迭代集合 (非 copy 类型) 一定要传入引用, 除非你之后不打算再使用这个集合了..

1
2
3
4
5
6
for x in XS {} // 集合迭代
for x in &XS {} // 传入引用,一般用这个
for x in &mux XS {} // 传入可变引用
for (i, v) in a.iter() {} // 索引访问, x.iter
for i in 1..5 {} // 带索引循环
for _ in 1..5 {} // 循环特定次数, `_` 代替了 i
1
2
while true { } // 一切如常
loop { } // 一定记着 break

标签: 标签名 : 流程, break or continue 可以指定 标签

  • 这可比只能控制当前流程 好太多了
1
2
3
4
5
6
7
'outer: for i in 0. {
'inner: for j in 0. {
if i * j > 30 {
break 'outer;
}
continue 'inner'
}}

模式匹配

Match / if Let

match 类似 switch,但更像 py 中的 switch;

  • 模式绑定: 取出枚举类绑定的值; –> [[#null 处理]] 中 None 值的实际处理,一般就是配合 match ,在分支中进行.
1
2
3
4
5
6
7
8
9
10
match target {
模式1 => 表达式1,
模式2 | 模式3 => { },
_ => 表达式3 or other => 表达3(other)
}
// 也能用来赋值
let ip_str = match ip1 {
IpAddr::Ipv4 => "127.0.0.1",
_ => "::1",
};

解模式绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState), // 25美分硬币
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}!", state);
25
},}}

当只想匹配一个条件时候 match –> if let

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let v = Some(3u8);
match v {
Some(3) => println!("three"),
_ => (),
}
if let Some(3) = v { // 乍一看和 if 写一行判断 没啥区别,但 好歹是模式匹配,能解开模式绑定
println!("three");
}
// 这里相比直接写 if 就节省了一行
//
let optional_number = Some(42);
// 这一行 `=` 并非是赋值语句, 而是模式匹配的一部分
// if let 一行,干了 匹配? 匹配成功则赋值,两件事.
if let Some(number) = optional_number {
println!("The number is {}", number);
} else {
println!("There is no number");
}

matches!(value, pattern => epression) value 值, pattern 是模式. 最后返回 bool 值.

1
2
3
4
let foo = 'f';
assert!(matches!(foo, 'A'..='Z' | 'a'..='z'));
let bar = Some(4);
assert!(matches!(bar, Some(x) if x > 2));

变量遮蔽: 模式和模式匹配后的表达式是一个新的代码块, 在这个作用域内 同名变量 会发生 变量遮蔽 ^8963d3

解构 Option

1
2
3
4
5
6
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}

Some(i) 会匹配到 [[#^bce572|Option]] 的 Some(T) 并且将 i 赋予具体的绑定值;

Some(4) 有具体值的,就只能匹配到确切的值;

模式适用场景

这一节是模式匹配,终于谈到 模式了 2333 😂

模式本身就是 rust 的特殊的语法, 用于匹配和解构数据结构的语法; 一般可由下面内容混合

  • 字面值 | 解构的数组 枚举 结构体 或 元组 | 变量 | 通配符 | 占位符

哪里用到了模式

match 的分支, 每个分支就是一个模式.最后用 _ 兜底

if let 单一模式

while let 循环单一模式 使用 loop + iflet or match 更繁琐;

1
2
3
4
5
6
7
8
9
10
11
12
// Vec是动态数组
let mut stack = Vec::new();

// 向数组尾部插入元素
stack.push(1);
stack.push(2);
stack.push(3);

// stack.pop从数组尾部弹出元素
while let Some(top) = stack.pop() {
println!("{}", top);
}

for (index, value) in v.iter().enumerate() 也是一种匹配模式,元组匹配迭代器

甚至 let a = 123 也是一种模式匹配… 匹配的值 绑定到 变量上…

  • 就说闹不闹

类似的解构元组 let (x,y,z) = (1,2,3); 也是模式匹配….这里模式还包括了 变量个数.

函数的参数也是模式..我去, 就是个筐是吧..

if let 这里特殊一点, if let 允许不完全的匹配条件. -> 可驳模式匹配

1
2
3
4
let Some(x) = some_option_value; // 可能还有 None,因此匹配失败
if let Some(x) = some_option_value { // 这个会成功, 即使没有考虑 None 的情况
println!("{}", x);
}

全模式列表

纯罗列..

字面值: 很简单 match 当 switch 来用;

命名变量: 前面提到的 [[#^8963d3|变量遮蔽]] 就是匹配命名变量时候遇到的.

  • 命名变量就是 Some(y) 这样…值被赋予了 y,要是前面还有同名变量,就会有变量遮蔽;
1
Some(y) => println!("Matched, y = {:?}", y),

单分支多模式: 多个模式 |

序列匹配模式: 模式直接使用 1..=5 'a'..='j'

  • 只有 数字 和 字符 可以这样用, 适当使用节省了大量模板代码

解构

前文中使用 解构直接拆分 结构体 元组 数组 引用等等,但是还不够,得加大药量.

1
2
3
let Point { x: a, y: b } = p; // 解构结构体, 变量名 其实可以和结构体不一致
Point { x, y: 0 } => // 命名变量 和 值可以混用
Point { x: 0, y } =>

解构也没有层级限制,唯一的限制是你能不能看的下去..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}

enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(Color), // 绑定了另一个枚举
}
fn main() {
let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));

match msg {
Message::ChangeColor(Color::Rgb(r, g, b)) => { // 解构 枚举 嵌套 枚举
println!("Change the color to red {}, green {}, and blue {}",r, g,b)
}
Message::ChangeColor(Color::Hsv(h, s, v)) => {
println!("Change the color to hue {}, saturation {}, and value {}",h,s,v)
}
_ => ()
}
}

解构数组: 定长不定长..

#todo

方法

又到了另一个与 go 相似的地方 /o(* ̄▽ ̄*)ブ

方法隶属于 object, 封装了对 属性的操作. rust 中

  • object 定义 属性与方法分离, impl 关键词, 多个 impl 可组合.
  • 方法内访问自身 &self (第一个参数,又很像 py)
  • object 可以是: 结构体 / 枚举 / 特征 (类似接口)
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
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

impl Rectangle {
// 关联函数 不带 *self 的
// 这是个特例.约定俗成 new 是新建一个实例
fn new(x: u32, y: u32) -> Rectangle {
Rectangle {width: x,height: y,}
}
// 真正方法, &self 引用指向自身
fn area(&self) -> u32 {
self.width * self.height
}
}

impl Rectangle { // impl 可以有多个
fn area2(&self, other: &Rectangle) -> bool { // 方法本质也是函数啊, 多个参数完全没问题
self.area() > other.area()
}
}

fn main() {
let rect1: Rectangle = Rectangle {width: 30,height: 50,};
let rect2 = Rectangle::new(31, 51); // 关联函数调用 object`::`函数名
println!("rect1.area2(&rect2) is {}", rect1.area2(&rect2)); // 方法则是 实例.方法名
}

关联函数: 方法本身可以算带 *self 的关联函数, 关联函数调用上 观感与类方法更接近.

*self 其实是语法糖, 方法也是函数,也有所有权 生命周期 的问题

  • self &self &mut self 3 种, 多用 &self
  • 执意使用 self 那么所有权直接转入,然后实例本身会被释放掉…这一点很坑,千万注意.

还有一种用法是 方法与结构体字段名相同,实现 getter…/(ㄒoㄒ)/~~

  • 结构体字段可设置为私有,增加安全性.
1
2
3
4
5
impl Rectangle {
fn width(&self) -> bool {
self.width > 0
}
}

泛型和特征

泛型


和 go 的泛型有得一拼.. 但 rust 又是那种精心设计而非为了兼容妥协的,又比 c++ 精简多了..所以现在就是 👽👽👽…


几个印象

  • rust 泛型范围要比其他语言更广,结构体枚举都能用,甚至有 const 泛型.
  • 0 成本,完全不会牺牲性能,总用代价吧? –> 单态化,为每种可能类型都实现一遍,程序大小 ++

一个示例: 动态数组最大值

  • 符号还是熟悉的 T,要提前声明在 函数名 <T>
  • 面向对象中的 extend 和 super 限制 T 的种类,rust 中则基于 特征 (类似于接口) 限定.
  • 其他 T 的使用,没有太多差别.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn lagest<T: std::cmp::PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
return largest;
}

fn main() {
let number_list = vec![34, 50, 25, 100, 65];
print!("The largest number is {}", lagest(&number_list));
}

结构体泛型

  • 结构体名 <T>,一样得提前声明;
  • T 只能代表一种类型, 多种类型 就声明多个呗. 即使是这样 `struct Woo<T,U,V,W,X>
1
2
3
4
5
6
7
struct Point<T, U> {
x: T,
y: U,
}

let a = Point { x: 0, y: 0 };
let b = Point { x: 0, y: 0.0 };

枚举泛型: 大明湖畔的 Option 已经见过了

  • 语法与结构体泛型基本一致
1
2
3
4
enum Option<T> {
Some(T),
None,
}

方法泛型: 方法也是函数,自然也能用泛型.

  • 一个声明中泛型的来源就是 impl 后面的<>中, Point<xx> 都是具体的类型
  • 方法泛型中还可夹带 函数泛型,互不影响;
  • 重点: 方法可以限定具体类型, 仅针对该类型实现方法; ^pkn22t
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
impl<T> Point<T, T> { // Point<T, T> 是使用 T 不是声明
fn new(x: T, y: T) -> Self { // 一切如常
Point { x, y }
}
fn X(&self) -> &T {
&self.x
}
}
// 声明时 可以使用具体类型. 仅方法一家;
impl<T, i32> Point<T, i32> {
fn Y(&self) -> &i32 {
&self.y
}
fn zero<V, W>(x: V, y: W) -> Point<V, W> { // 函数泛型 互不干扰
Point { x: x, y: y }
}
}

Const 泛型

: 值的泛型

  • 上面种种泛型,T 代指的总是某个类型, 但在 rust 中还有一点特殊情况…需要待指某个具体值;

数组 [i32; 2][i32; 5] 是不同的类型, 这一点仅在 rust,其他语言似乎都是同一个类型.这是个数组类型就好了..

这个问题导致了,明明都是 i32 的数组,只是长度不同,就得编写基本相同的函数处理..导致大量重复代码;

  • 改用数组切片,传入引用可以解决大部分,但并不是全部.
  • 以前有的数组库限定长度 32,就是为每个长度实现一遍..简直…

值的泛型: 加上关键词 const; 仅支持 整数 bool 和 char 类型

  • 这个实际上在编译时就能确认 N 的具体值, 归根结底算是个语法糖.
  • 类型的泛型,编译时无法确定到底是什么类型; 只能为每个可能都实现一遍;
1
2
3
4
5
6
7
8
// N 是一个常量
fn test<T: std::fmt::Debug, const N: usize>(arr: [T; N]) { // 如同常量一般使用 N
println!("{:?}", arr);
}
fn main() {
let arr = [1, 2, 3, 4, 5];
test(arr);
}

还有 const 表达式 和 const 函数指针; 但书里暂时没有更多介绍

特征

特征 (类似 接口) 定义一组方法 的集合 // 会有类似 面向对象 鸭子类型的感觉…

  • 真正定义的是一组 方法的签名 // 非常非常的 java 接口… 也会有类似 java 接口的用法

定义 trait X 特征名

1
2
3
pub trait Summary  {
fn summarize(&self) -> String; // 方法的签名
}

实现 impl X for m, 调用 实例.x

1
2
3
4
5
6
7
impl Summary for M {
fn summarize(&self) -> String {
format!("{}: {}", self.name, self.x)
}
}
let m = M {name: String::from("M"),x: 1,};
println!("{}", m.summarize());

允许定义默认实现 (和 实现一个方法 没有区别)

1
2
3
4
5
6
7
pub trait Summary {
fn summarize(&self) -> String {
return String::from("default");
}
}
impl Summary for O {} // 可以不废话直接大括号了
o.summarize // 调用

孤儿规则?? #todo ^bd02aa

特征也能用作 函数 入参 or 返回值 // 这不就是 java 接口吗…

入参

也叫特征约束

  • fn notify(item: &impl Summary)fn notify2<T: Summary>(item: &T) 等价,前者是语法糖,但是异常好读.
1
2
3
4
5
6
7
8
9
// &impl Summary 异常好读
fn notify(item: &impl Summary) {
print!("{}", item.summarize());
}
fn notify2<T: Summary>(item: &T) { // 这才是实际的展开
print!("{}", item.summarize());
}
// 限制入参类型 item1 item2 相同,就不能用语法糖了
fn notify<T: Summary>(item1: &T, item2: &T) {}

多重约束: 几个特征一起来

1
2
3
// 两者等价
pub fn notify(item: &(impl Summary + Display)) {}
pub fn notify<T: Summary + Display>(item: &T) {}

要是泛型参数再多几个,每个的特征约束也再来几个…一行不敢看了.. –> Where 约束,换个写法

1
2
3
4
5
6
7
8
9
10
11
// 将泛型的约束 另起一行
pub fn notify<T: Summary + Display>(item: &T) {}
pub fn notify<T>(item: &T)
where T: Summary + Display
{}
// 参数越多越有效
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {}
fn some_function<T, U>(t: &T, u: &U) -> i32
where T: Display + Clone,
U: Clone + Debug
{}

当特征 遇到 [[Rust 基础入门#^pkn22t|方法泛型]] = 为指定类型 指定特征 实现 方法

  • 这样的方式要比 java 接口更细致
1
2
3
4
5
6
// 只有 T 同时实现 Display 和 PartialOrd 特征时才可以调用 cmp_display
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
...
}
}

返回

当作 返回值

  • 有一个限制: 即使 M N 都实现了 Summary,但返回两个类型时候 编译器还是提示报错;
  • 这里得用 –> [[#特征对象]]
1
2
3
4
5
6
7
8
9
10
11
fn return_test(flag: bool) -> impl Summary {
M {name: String::from("M"),x: 1,}
}
// 返回两个类型会报错
fn return_test(flag: bool) -> impl Summary {
if flag {
M {name: String::from("M"),x: 1,}
} else {
N {name: String::from("N"),x: 2,}
}
}

其他

derive 派生特征: 类似注解,为对象实现默认特征

  • #[derive(Debug)]: 见过很多了, println!("{:?}", s) 可以打印整个结构体;
  • Copy
  • 更多的参考 –> 派生特征 trait

调用方法时,需要特定 特征, as 又有很大限制 –> TryInto

  • 尝试将类型转换为另一个类型, 返回 Result 类型;
  • 转换成功就包含值,转换失败就包含失败信息;

例子

特征这里和其他概念交叉太多了,还是例子更好理解;

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
use std::ops::Add;

// 为Point结构体派生Debug特征,用于格式化输出
#[derive(Debug)]
struct Point<T: Add<T, Output = T>> { //限制类型T必须实现了Add特征,否则无法进行+操作。
x: T, y: T,
}

impl<T: Add<T, Output = T>> Add for Point<T> {
type Output = Point<T>;

fn add(self, p: Point<T>) -> Point<T> {
Point{x: self.x + p.x,y: self.y + p.y,}
}
}

fn add<T: Add<T, Output=T>>(a:T, b:T) -> T {
a + b
}

fn main() {
let p1 = Point{x: 1.1f32, y: 1.1f32};
let p2 = Point{x: 2.1f32, y: 2.1f32};
println!("{:?}", add(p1, p2));

let p3 = Point{x: 1i32, y: 1i32};
let p4 = Point{x: 2i32, y: 2i32};
println!("{:?}", add(p3, p4));
}

特征对象

动态分发 / 动态分配

特征对象: 是一个引用, 指向 实现了 特征的 实例

  • 参数的关键词是 dyn 实际上是动态分发关键词
  • 真正创建时: 对实例取 & or Box<T>
    • Box<T> 涉及智能指针, 被包裹的 T 会强制分配在 堆 上.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
trait Draw { // 特征
fn draw(&self) -> String {
return "Draw default".to_string();
}
}
impl Draw for u8 { // 为已有类型实现
fn draw(&self) -> String {
return format!("Draw u8: {}", self);
}
}
fn draw1(x: Box<dyn Draw>) { // Box | 声明时需要 dyn
x.draw(); // 这一步 Box 智能指针会自动解引用
}
fn draw2(x: &dyn Draw) { // & | 声明时需要 dyn
x.draw();
}

fn main() {
let x = 1.1_f64; // 实例
draw1(Box::new(x)); // 创建
draw2(&x);// &实例 不再需要 dyn
}

特征对象数组调用: components 元素仅实现了 Draw 特征,嗯 接口数组…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}

impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw(); // 调用特征实现
}
}
}
// 泛型实现 限定数组仅有一种元素
pub struct Screen<T: Draw> {
pub components: Vec<T>,
}
impl<T> Screen<T>
where T: Draw {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}

动态分发

在运行时才能确定到底是那个实例的实现 –> 动态分发

  • 相对的是静态分发, 类似于泛型的处理,编译时就完成了;

具体就不展开了, 有点搞不懂…

Self and Self

Self 代指 特征 or 方法类型 (class) | self 代指 实例

1
2
3
4
5
6
// Self 代表的是 Button 类型
impl Draw for Button {
fn draw(&self) -> Self {
return self.clone()
}
}

能够实现特征对象的 特征 == 要求 特征是 对象安全; 要求所有方法 (除了方法还有别的要求):

  • 方法返回类型不能是 Self
  • 方法没有任何泛型参数

特征 2

特征内容实在太多了,这是第二个章节, 其实是很多其他特性 与 特征 的结合使用

关联类型

特征中叠加进 类型; 比泛型声明更加精简;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pub trait Iterator {
type Item; // 关联类型
fn next(&mut self) -> Option<Self::Item>; // Self::Item 返回定义的关联类型
}
impl Iterator for Counter {
type Item = u32; // 关联类型实例化 👽
fn next(&mut self) -> Option<Self::Item> { // 真正使用关联类型地方
// --snip--
}
}
// 泛型能做到相同的事情
pub trait Iterator<Item> {
fn next(&mut self) -> Option<Item>;
}

关联类型 在多个参数时候, 能比泛型节约模板代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// A B 泛型必须深入到每个地方...
trait Container<A,B> {
fn contains(&self,a: A,b: B) -> bool;
}
fn difference<A,B,C>(container: &C) -> i32
where
C : Container<A,B> {…}
// 关联类型 仅在 特征内部, 其他实现中不再需要 大量泛型声明
trait Container{
type A;
type B;
fn contains(&self, a: &Self::A, b: &Self::B) -> bool;
}
fn difference<C: Container>(container: &C) {}

似乎关联类型可以使用泛型了…已经是稳定版了..

默认泛型类型参数

这个和特征无关,任何用到泛型的地方都能用.

给泛型一个默认值 (T=i32), 在未指定泛型时,编译器使用默认值;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
trait Add<RHS=Self> { // 给 RHS 来个默认值
type Output;
fn add(self, rhs: RHS) -> Self::Output; // 这里使用了 泛型
}
#[derive(Debug, PartialEq)]
struct Point {x: i32,y: i32,}

impl Add for Point { // 注意这里并没有声明 泛型RHS, 那编译器直接用了 Self
type Output = Point;
fn add(self, other: Point) -> Point {
Point {x: self.x + other.x,y: self.y + other.y,
}
}
}
assert_eq!(Point { x: 1, y: 0 } + Point { x: 2, y: 3 },Point { x: 3, y: 3 });

节省了很多模板代码… 仅此而已.

完全限定语法 (调用同名函数/方法)

结构体 实现 多个特征 和 方法, 这里每个结构都带有一个签名完全相同的函数..(闲的…)

  • 调用顺序是 结构体的方法
  • 其他特征的方法需要显式调用 –> 完全限定语法

完全限定语法: <Path>::<Function>(args) 显式指明函数/方法/关联函数的完整路径

  • 特征的方法 Pilot::fly(&person);
  • 多个特征的关联函数 <Dog as Animal>::baby_name() (注意 as 关键词)

大部分情况下无需如此, rust 的编译器完全值得信赖.

特征的特征约束

泛型中能用 特征 作为约束条件, 那 特征也能作为 特征 的约束条件…// 直接说 类似 接口继承就完事了…

1
2
3
4
5
6
7
8
use std::fmt::Display;

trait OutlinePrint: Display {
fn outline_print(&self) {
let output = self.to_string();
xxx
}
}

OutlinePrin 依赖于 Display 特征, 任何实现 OutlinePrin 的 类型 必须先实现 Display

Newtype

^d732f3

没有什么是加一层解决不了的,如果不行那就再加一层.. from 鲁迅没说过

[[#^bd02aa|孤儿规则]] 的存在使得未在本作用域 定义的 特征 和 类型, 没法做啥文章, 但 newtype 绕过了这个限制.. –> 如直面意思,再包一层 (类型)

  • Vec<T>Display 均定义在标准库,无法为 Vec<T> 实现 Display
  • 那可以 Wrapper 包含成员 Vec<T>, 为 Wrapper 实现 Display 啊.
1
2
3
4
5
6
7
8
9
10
11
use std::fmt;

struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}

let w = Wrapper(vec![String::from("hello"), String::from("world")]);
println!("w = {}", w);
  • 这样的麻烦就是访问时候,还得 x.a 访问成员才行 –> Deref 解引用

集合类型

动态数组 Vector

这个才是其他编程语言中的数组…

Vetor: 单一类型; 动态扩容; 关键词 Vec;

声明: Vec::new() vec![]

  • 已知数组大小 -> Vec::with_capacity(capacity) 可以避免动态扩容的损耗
1
2
3
4
5
let v1: Vec<i32> = Vec::new(); // 最 rust 的声明
let mut v2 = Vec::new(); //
v2.push(5);// 没这一句, 上面要报错 | 上文也有类似情况,到比较时候才能推定具体类型
// 声明兼初始化
let v3: Vec<i32> = vec![1, 2, 3];

单个访问: 索引 和 get

  • get 返回 Option<&T> 还需要 match 解才行, 索引倒是直接返回值,但遇空 可能会 挂掉
  • rust 意思是,反正我都给了,爱用那个用那个.
1
2
3
4
5
6
let third: &i32 = &v3[2];
println!("The third element is {}", third);
match v3.get(2) {
Some(third) => println!("The third element is {third}"),
None => println!("There is no third element."),
}
  • 这里有个新的格式化输出, {third} 嗯又来 python 了 →_→

更新: mut 还是必须的

1
2
3
let mut v = Vec::new();
v.push(1);
v[0] = 2;

多个数组元素访问

1
2
3
4
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {first}");
  • 这里肯定报错了 first 作用域内,出现了 v.push(6); 可变引用;
  • 一方面是语法上限制,令一方面 Vec 动态数组 可能会动态扩容: 拷贝 再更新地址; 这样 first 可能指向无效内存….

遍历: 自然是可以通过索引 来, 也能通过 迭代方式来,不用每次检查索引 还更快.

1
2
3
4
let v = vec![1, 2, 3];
for i in &v {
println!("{i}");
}

存储不同类型: Vec 还是只能存储单一类型 –> 枚举 or 特征对象

  • 特征对象要繁琐一些, 但更加灵活 更为常见
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 枚举
#[derive(Debug)]
enum IpAddr {
V4(String),
V6(String)
}
let v = vec![IpAddr::V4("127.0.0.1".to_string()),IpAddr::V6("::1".to_string())];
// 特征对象 明显要繁琐很多
trait IpAddr {
fn display(&self);
}
struct V4(String);
impl IpAddr for V4 {
fn display(&self) {
println!("ipv4: {:?}",self)
}
}
struct V6(String);
impl IpAddr for V6 {
fn display(&self) {
println!("ipv6: {:?}",self)
}
}
let v: Vec<Box<dyn IpAddr>> = vec![Box::new(V4("127.0.0.1".to_string())),Box::new(V6("::1".to_string())),];

排序

sort_unstable 稳定 sort_unstable_by 非稳定; 非稳定更快, 稳定还需要额外空间

1
2
3
4
5
6
let mut vec = vec![1, 5, 10, 2, 15];    
vec.sort_unstable();
// 浮点数
let mut vec = vec![1.0, 5.6, 10.3, 2.0, 15f32];
vec.sort_unstable();// 直接报错 浮点数有 NAN 值无法比较
vec.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap()); // 确认不含 NAN , 传入 partial_cmp 自定义比较

结构体: 可以传入比较函数 or 实现 Ord 特征

  • Ord 特征依赖于 Ord Eq PartialEq PartialOrd –> derive 注解
  • 注解要求 结构体所有属性 均已经实现了 Ord 特征,否则报错
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
30
31
32
struct Person {
name: String,
age: u32,
}
impl Person {
fn new(name: String, age: u32) -> Person {
Person { name, age }
}
}
let mut people = vec![
Person::new("Zoe".to_string(), 25),Person::new("Al".to_string(), 60),Person::new("John".to_string(), 1),
];
// 定义一个按照年龄倒序排序的对比函数
people.sort_unstable_by(|a, b| b.age.cmp(&a.age));

// 实现 `Ord` 特征 好在有注解
#[derive(Debug, Ord, Eq, PartialEq, PartialOrd)]
struct Person {
name: String,
age: u32,
}
impl Person {
fn new(name: String, age: u32) -> Person {
Person { name, age }
}
}

let mut people = vec![
Person::new("Zoe".to_string(), 25),Person::new("Al".to_string(), 60),Person::new("Al".to_string(), 30),
Person::new("John".to_string(), 1),Person::new("John".to_string(), 25),
];
people.sort_unstable();

HashMap

HashMap: 不在 prelude 中,因此需要 use xxx

1
2
3
use std::collections::HashMap;
let mut my_gems = HashMap::new();
my_gems.insert("红宝石", 1);// 将宝石类型和对应的数量写入表中

#todo

生命周期

→_→ →_→ 终于到了号称最难的部分了 ←_← ←_←

  • 其实也还好, 类似于对象生命周期概念, 所有权 引用 作用域 搞清楚了,就没啥了..

生命周期 = 实例的有效作用域,大部分情况下 rust 编译器已经足够了,用不到我们操心..但总有二般..

  • 多种类型存在,编译器会犯糊涂,此时就需要 手动标注,以方便编译器理解.

生命周期标注 就是糊弄编译器,让其给代码开个通关文牒. 不会改变任何引用的实际作用域 !

  • 标注: 'a (单引号 +a 开始)
  • &'a str'& 后面
  • 结构体和方法内 与 泛型位置基本一致
1
2
3
4
5
6
struct ImportantExcerpt<'a> {
part: &'a str, // 结构体中使用引用类型
}
impl<'a> ImportantExcerpt<'a> { //`impl` 使用结构体的完整名称 包括 `<'a>`,生命周期标注也是结构体类型的一部分!
fn level(&self) -> i32 { 3 }
}

一般情况下编译器会智能推断生命周期 == 自动加上生命周期标记 -> 生命周期消除 的规则 (第一条输入, 23 条是输出) ^17576a

  • 每个输入参数 标注上独立的生命周期:
    • fn foo(x: &i32, y: &i32) -> fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
  • 只有一个输入,其生命周期自动赋予所有输出:
    • fn foo(x: &i32) -> &i32 仅有一个输入,则 fn foo<'a>(x: &'a i32) -> &'a i32 x 的生命周期赋予 返回值
  • 输入中存在 &self or &mut self,所有输出默认与 &self 一致
    • 方法狂喜,狂喜.

编译器按照生命周期消除规则 加上 标注, 还不过就报错.

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {x} else {y}
}
// 第一个规则 -> fn longest<'a,'b'>(x: &'a str, y: &'b str) -> &str
// 第2 3规则不满足略过,最终是 fn longest<'a,'b'>(x: &'a str, y: &'b str) -> &str
// 编译器无法确定返回值的生命周期, 报错

impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
xxx
}}
// 第一个规则 -> fn announce_and_return_part(&'a self, announcement: &'b str) -> &str
// 第2个不满足,第3个 -> fn announce_and_return_part(&'a self, announcement: &'b str) -> &'a str
// 没啥问题,通过

生命周期标注也有类似 泛型 约束的写法, 包括 where

1
2
3
4
5
6
7
8
9
10
11
impl<'a: 'b, 'b> ImportantExcerpt<'a> { // 'a: 'b 代表 'a 必须比 'b 活得久
fn announce_and_return_part(&'a self, announcement: &'b str) -> &'b str {
self.part
}
}
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part<'b>(&'a self, announcement: &'b str) -> &'b str
where
'a: 'b,
{ self.part }
}

总有些无解的生命周期吧 –> static 我会活的和程序一样长

  • 存在即合理吧, 最后的解决就这个
  • &'static: 字符串常量 特征对象
  • T: 'static: 泛型 or 一个奇迹

最后来个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display,
{
println!("Announcement! {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}

错误处理

这里就和 go 没啥关系了;; 放心没有 if err != nil {}

Panic

做内核相关肯定不想看到这个…(⓿_⓿)

panic! 直接输出恐慌

1
2
3
4
5
fn main() {
panic!("just for test")
}
// runing
//thread 'main' panicked at 'just for test', src/main.rs:2:5
  • 恐慌信息: 代码的位置; 那个函数; 很正常

$env:RUST_BACKTRACE=1 ; cargo run 带 backtrace 展开调用的堆栈,方便追踪到底哪里出错了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
let v = vec![1, 2, 3, 4, 5];
v[99]; // panic!
}
// vsc 调试直接带了 stack backtrace | 这里直接 cargo run
//thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 99', src/main.rs:3:5
//note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
// $env:RUST_BACKTRACE=1 ; cargo run 呢?
thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 99', src/main.rs:3:5
stack backtrace:
0: rust_begin_unwind
at /rustc/90c541806f23a127002de5b4038be731ba1458ca/library/std/src/panicking.rs:578:5
xxx
6: hello_remote_world::main
at ./src/main.rs:3:5
7: core::ops::function::FnOnce::call_once
at /rustc/90c541806f23a127002de5b4038be731ba1458ca/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

panic 后,程序有两种方式善后:

  • 栈展开 (默认): 追踪哪里出错了,并打印详细信息.
  • 直接终止: 直接退出,善后交给操作系统…
  • Cargo.toml[profile.release] panic = 'abort'

善后完成后, panic 的线程就终止了,但不影响其他线程, 因此不要在 main 线程 堆积过多逻辑;

panic 的包装 unwrapexpect

  • 两者都会解析返回值 Result 类型, 遇到错误直接 panic
  • expect 可以自定义错误信息,unwrap 则抛出默认错误信息
  • 这俩一般仅在原型 / 调试 时候使用,有更好的办法处理 Result 类型
1
2
3
4
5
6
7
enum Result<T, E> {
Ok(T),
Err(E),
}

let home: IpAddr = "127.0.0.1".parse().unwrap();
let f = File::open("hello.txt").expect("Failed to open hello.txt"); // 自定义错误信息

Result 和 ?

unwrapexpect 比直接 panic 好了一点,但还是会 panic, 配合 match

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use std::fs::File;
use std::io::ErrorKind;

fn main() {
let f = File::open("hello.txt");

let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error => panic!("Problem opening the file: {:?}", other_error),
},
};
}

更好的做法 –> match 匹配,传播错误; error 别 panic 犯不上.. 但是..繁琐..繁琐..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
// 打开文件,f是`Result<文件句柄,io::Error>`
let f = File::open("hello.txt");

let mut f = match f {
// 打开文件成功,将file句柄赋值给f
Ok(file) => file,
// 打开文件失败,将错误返回(向上传播)
Err(e) => return Err(e),
};
// 创建动态字符串s
let mut s = String::new();
// 从f文件句柄读取数据并写入s中
match f.read_to_string(&mut s) {
// 读取成功,返回Ok封装的字符串
Ok(_) => Ok(s),
// 将错误向上传播
Err(e) => Err(e),
}
}

那么 ? 来了/ 嘿嘿 / 少了一大半代码… 作用完全一致.

1
2
3
4
5
6
7
8
9
10
use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}

? 是一个宏定义, 配合 Result 类型, ok 就立刻返回, 错误就向上抛出.

  • 一般在返回值是 Rsult 的 函数/方法 中使用 (Option 其实也行)
  • 可以处理 错误类型转换 和 链式调用
1
2
3
4
5
6
7
8
9
use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?; // 先打开文件 没错误 再读取 没错误
Ok(s)
}
  • 始终注意 ? 的限制条件,需要一个变量来承载正确值; (不行就展开成 match 看看对不对)

包与模块

有不同的等级

  • Packages \ Crate \ Module

Packages 顶级

  • 独享的 Cargo.toml 文件
  • 3 种类型
    • Binary Package: 即使 Package 嵌套也仅一个,入口一般是 src/main.rs,编译后是一个二进制文件.
    • Library Package: Package 嵌套时可以有多个,不能独立运行,只能供其他调用. 创建时会在 src/package_name.rs
    • Tool Package: 辅助,代码生成,自动化测试等等.

Crate 层级更小一点,常见 pub (crate) 仅在包内公开

Module 更像 py 的 __int__.py

  • mod xxx 创建新模块,其他地方能引用的.
1
2
3
4
5
6
7
8
// 绝对路径引用
crate::front_of_house::hosting::add_to_waitlist();
// 相对路径引用
front_of_house::hosting::add_to_waitlist();
// 还是相对路径,但是 父module
super::serve_order
// 同 module 下
self::back_of_house::cook_order()

rust 中子 Module 都是对 父 Module 隐藏的…

  • pub 结构体 字段还是隐藏的///
  • pub 枚举 成员就全部公开…

文件夹作为模块:

  • 目录下创建 mod.rs,再指定 哪些模块是公开暴露的.
  • 另一种方式是 创建于文件夹名.rs ,内容同上, 不过可以避免大量的 mod.rs.

Use

不管那个文件,先 use

1
2
3
4
5
6
7
8
// 绝对路径 | 模块
use crate::front_of_house::hosting;
// 相对路径 | 函数
// 函数与模块仅颗粒度不同
use front_of_house::hosting::add_to_waitlist;
// 同名可以通过模块名区分
// 也能通过 as 别名
use std::io::Result as IoResult;
1
2
3
4
5
6
7
// 导入后的模块/函数 都是私有的,如果需要二次暴露, 得
pub use crate::front_of_house::hosting;

// {} 可以省略部分重复
use std::io::{self, Write};
// 一次性导入全部函数
use std::collections::*;

可见性 pub

1
2
3
4
5
pub 意味着可见性无任何限制
pub(crate) 表示在当前包可见
pub(self) 在当前模块可见
pub(super) 在父模块可见
pub(in <path>) 表示在某个路径代表的模块中可见,其中 path 必须是父模块或者祖先模块

注释 / 文档

按照分级

  • 代码注释: // or /* */
  • 文档注释: /// or /** */ 甚至包括测试用例 (和 py 很像)
  • 包和模块注释

文档注释

  • 需要位于 lib 类型的包中,例如 src/lib.rs
  • 被注释的对象需要使用 pub 对外可见
  • 可以使用 markdown
  • 文档注释是给用户看的,内部实现细节不应该被暴露出去

包和模块注释

文档测试
#todo