rust 学习笔记

1. 入门指南

  • 安装 & 更新 & 卸载
    • 安装指令 curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    • 默认启动 export PATH="$HOME/.cargo/bin:$PATH
    • 更新 rustup update
    • 卸载 rustup self uninstall
  • 文件编译 & 执行
    • 编译 rustc xxx.rs 生成二进制文件直接执行
  • 项目编译 & 执行
    • 项目构建 cargo new xxx
    • 项目编译 cargo build 在 debug 文件下生成二进制文件
    • 项目执行 cargo run 可以直接执行
    • 项目检查 cargo check 比 build 快,方便检查
    • release 版本 cargo build --release 以及 cargo run --release 正式编译

2. 猜数游戏

1
2
3
4
5
6
use std::io;
fn main() {
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("failed to read line");
println!("you guessed: {}", guess);
}
  • rust 变量默认是不可变的
    • 不可变变量声明 let,可变变量声明 let mut
  • rust 引用默认也是不可变的,引用访问同一份代码,没有拷贝开销
    • 不可变引用 &sth,可变引用 $mut sth
  • expect 为了处理 io::Result 的结果 Err
  • println! 中通过 {} 实现占位
  • Cargo 最主要的功能是管理、使用第三方库
    • Cargo.toml 中的 [dependencies] 下可以添加依赖
    • cargo build 执行太慢换源
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
[source.crates-io]
registry = "https://github.com/rust-lang/crates.io-index"
# 指定镜像
replace-with = 'tuna'

# 清华大学
[source.tuna]
registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"

# 中国科学技术大学
[source.ustc]
registry = "git://mirrors.ustc.edu.cn/crates.io-index"

# 上海交通大学
[source.sjtu]
registry = "https://mirrors.sjtug.sjtu.edu.cn/git/crates.io-index"

# rustcc社区
[source.rustcc0]
registry = "https://code.aliyun.com/rustcc/crates.io-index.git"

[source.rustcc1]
registry="git://crates.rustcc.cn/crates.io-index"

[source.rustcc2]
registry="git://crates.rustcc.com/crates.io-index"
  • 没有修改重复使用 cargo build 很快,因为 Cargo.lock 文件确保构建可重现
  • 升级包版本使用 cargo update,但是遵从语义化版本规则,跨版本升级需要手动修改 [dependencies]
  • 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
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
let secret_number = rand::thread_rng().gen_range(1, 101);
println!("secret number is: {}", secret_number);
loop {
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("type a number");
continue;
},
};
println!("you guessed: {}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println!("samll"),
Ordering::Greater => println!("big"),
Ordering::Equal => {
println!("win");
break;
},
}
}
}

3. 通用编程概念

  • 标量类型:整数、浮点数、布尔值、字符
  • 复合类型:元组、数组
  • rust 使用蛇形命名snake case作为函数、变量名称的风格,全小写、下划线分割
  • rust 中语句 statement 和表达式 expression 有区别
    • 语句执行操作但是没有返回值,比如 let x = 5;
    • 表达式会计算并产生一个值作为结果,比如 {}
1
2
3
4
5
6
7
fn main() {
let y = {
let x = 3;
x + 1
};
println!("{}", y);
}
  • rust 在函数中通过 -> 表明返回类型,函数中返回值等同于函数中最后一个表达式的值
  • rust 中语句没有没有返回值,所以上面的大括号里的 x + 1 如果带上 ; 反而会报错
  • rust 中的条件表达式会返回一个 bool 结果
1
2
3
4
5
6
7
8
9
fn main() {
let condition = true;
let number = if condition {
5
} else {
6
};
println!("{}", number);
}
  • rust 中的 break 后可以跟返回结果
1
2
3
4
5
6
7
8
9
10
fn main() {
let mut count = 0;
let result = loop {
count += 1;
if count == 10 {
break count * 2;
}
};
println!("{}", result);
}
  • for 循环遍历 list 时,可以通过 list.iter() 遍历集合
1
2
3
4
5
fn main() {
for i in (1..4).rev() {
println!("{}", i);
}
}
  • 可以通过 .. 补充集合,其中 rev() 反转集合

4. 所有权

  • rust 通过包含特定规则的所有权系统来管理系统,在编译过程中完成,不会产生运行时开销
  • rust 中的每一个都有一个对应的变量作为它的所有者
    • 在同一时间内,有且仅有一个所有者
    • 所有者离开自己的作用域时,它持有的就会被释放掉
  • rust 中为了避免二次释放内存,堆上如果有两个指针指向这片内存,被复制的指针(栈)会被废弃,因此以下代码会报错,这个不是浅拷贝,而是 move
1
2
3
4
5
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1);
}
  • rust 永远不会自动地创建数据的深拷贝,确实需要深拷贝堆数据时,可以使用 clone() 方法,以下代码不会报错
1
2
3
4
5
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{}", s1);
}
  • 拥有 Copy 这种 trait 的类型可以在赋值其他变量后保持可用性,但是如果有 Drop 这种 trait 不能实现 Copy
    • 整数、bool、char、浮点、所有字段都可 Copy 的元组
  • 堆变量进入函数以后,就离开了作用域,不再有效
1
2
3
4
5
6
7
8
9
fn main() {
let s1 = String::from("hello");
println!("main1{}", s1);
test(s1);
// println!("main2{}", s1); // s1 已经离开了作用域
}
fn test(s: String) {
println!("test{}", s);
}
  • rust 中将一个值赋值给另一个变量时就会转移所有权,当一个持有堆数据的变量离开作用域时,它的数据就会被 Drop 清理回收,除非这些数据的所有权移动到了另一个变量上
  • rust 中可以通过 & 引用变量,而同时不交出所有权,但是同时也不能在引用中修改变量本身,因为引用默认是不可变的
1
2
3
4
5
6
7
8
9
fn main() {
let s1 = String::from("hello");
println!("main1{}", s1);
test(&s1);
println!("main2{}", s1);
}
fn test(s: &String) {
println!("test{}", s);
}
  • rust 中可变引用,需要变量本身是可变的,同时在作用域内,一个变量只能声明一个可变引用,因此在作用域里一个变量超过一个可变引用会报错
    • 此外,在拥有不可变引用的情况下,不能创建可变引用
  • 假设当前有某个引用,在这个引用被销毁前,编译器会确保堆空间不被销毁,也就不会出现悬垂引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn main() {
let s1 = String::from("hello world");
let word = first_word(&s1);
println!("{}", word);
}
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i]
}
}
&s[..]
}
  • rust 的字符串切片是专们用于字符串的,还有其他通用切片,比如数组切片

5. 结构体

  • 在变量名与字段名相同时可以使用简化版的字段初始化方法
1
2
3
4
5
6
7
8
fn build_user(emial: String, user_name: String) -> User {
User {
email,
user_name,
active: true,
sign_in_count : 1,
}
}
  • 使用其他实例创建新的实例,.. 可以表达剩下的未被显式赋值的字段都使用相同的值
1
2
3
4
5
let user2 = User {
email: String::from("test"),
user_name: String::from("test"),
..user1
}
  • 元组结构体依然使用 struct 关键字开头,并由结构体名称及元组类型定义组成
1
2
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
  • {:#?} 告知 println! 当前的结构体需要使用名为 Debug 的格式化输出,#[derive(Debug)] 添加注解来派生 Debug trait
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("{}", area(&rect1));
println!("{:#?}", rect1);
}
fn area(rect: &Rectangle) -> u32 {
rect.width * rect.height
}
  • 通过 impl 块可以增加方法实现,&self 代替了 &Rectangle
1
2
3
4
5
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
  • Rust 会自动为调用者 object 添加 & &mut * 以符合方法的签名
  • impl 块中允许定义不接收 self 作为参数的函数,他们被称为 关联函数,常被用作构造器,可以通过 :: 调用
  • Rust 中允许有多个 impl

6. 枚举

  • rust 中枚举和结构体都可以通过 impl 定义结构体的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#[derive(Debug)]
enum Message {
Quit,
Move {x: i32, y: i32},
Write(String),
ChangeColor(i32, i32, i32),
}
impl Message {
fn call(&self) {
println!("{:#?}", &self);
}
}
fn main() {
let m = Message::Write(String::from("test"));
m.call();
}
  • rust 的 ifmatch 的区别在于 if 后只能是 bool,但是 match 后可以是任何类型
  • rust 中的 match 支持绑定被匹配对象的部分值,我们可以据此从枚举变体中提取值
1
2
3
4
5
6
7
8
9
10
11
12
13
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
_ => None,
}
}
fn main() {
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
println!("{:#?}", six);
println!("{:#?}", none);
}
  • rust 中的匹配是穷尽的,我们需要确保代码是合法的
  • rust 中可以使用 if let 作为 match 的语法糖,它只在值满足某一特定模式时运行,忽略其他可能,此外可以配合 else 使用
1
2
3
4
5
6
7
8
fn main() {
let some_value = Some(3);
if let Some(3) = some_value {
println!("three");
} else {
println!("other");
}
}

7. 包&模块

  • rust 提供了一系列功能管理代码
    • package: 用于构建、测试单元包的 cargo 功能
    • crate: 用于生成库或可执行文件的树形模块
    • module & use: 控制文件结构、作用域、路径的私有性
    • path: 一种用于命名条目的方法
  • 一个包中只能拥有最多一个单元包(库单元包或二进制单元包)
  • cargo 默认将 src/main.rs 视作一个二进制单元包的根节点,将 src/lib.rs 视作库单元包的根节点,它们拥有与包相同的名称
  • 我们可以在路径 src/bin 下添加源文件来创建更多二进制单元包,这个路径下每个源文件被视为单独的二进制单元包
  • 使用关键字 mod 可以定义一个模块,其内可以继续定义其他模块、结构体、枚举、常量、trait 或函数
  • 模块结构组成模块树模块间有父子关系,整个模块树被放在名为 crate 的隐式根模块下
  • rust 中路径有两种形式,路径的标识符之间使用 :: 分隔
    • 绝对路径:使用单元包或字面量 crate 从根节点开始
    • 相对路径:使用 self、super 或内部标识符开始从当前模块开始
  • rust 中所有条目默认是私有的,通过 pub 可以暴露路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
pub fn eat_at_restaurant() {
crate::front_of_house::hosting::add_to_waitlist();
}
  • super 关键字可以找到父模块
1
2
3
4
5
6
7
8
fn serve_order() {}
mod back_of_house {
fn fix_order() {
cook_order();
super::serve_order();
}
fn cook_order() {}
}
  • 结构体为公共时,字段保持了私有,需要逐一决定是否公开;枚举为公共时,所有的变体都自动变为公共状态
  • 当结构体有非公共字段时,需要提供一个公共的关联函数,否则无法构建实例
  • 使用 use 可以引入作用域,在 user 后使用相对路径需要从 self 开始,而不是从当前作用域开始
  • 使用 as 配合 use 可以给指定内容取别名
  • 可以使用 pub use 将当前引入内容重导出

8. 通用集合

  • vector 连续存储多个值,string 字符集合,hash map 关联特定的键
  • 可以通过 let v: Vec<i32> = Vec::new(); 创建动态数组,还可以通过宏创建动态数组 let v = vec![1, 2, 3];
  • 可以通过 push 将元素放入 vector 中
  • vector 被销毁时,其内元素也会被销毁
  • 使用 &[] 会直接获得元素的引用,使用 get 会获得 Option<&T>
  • vector 会遵循『不能同时拥有可变引用与不变边引用』的规则
  • 需要使用解引用 * 来获得 vector 绑定的值
1
2
3
4
5
6
fn main() {
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
}
  • 可以通过 +format!push_str()push() 拼接 String,其中 push_str() 接收的是一个字符串切片,push 接收单个字符
  • 字符串使用 + 会默认调用 add 方法,fn add(self, s: &str) -> String
1
2
3
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // 这里 s1 已经被移动,不能再使用
  • 这里 s2 的类型是 &String,但是符合方法 &str 是因为使用了强制转换,即 &s2[..],由于 self 没有引用,所以调用结束后 s1 失效
  • String 是基于 Vec<u8> 的封装,随着编码不同所需长度不同,所以 len 不能表达内容长度,实际表达的是字节长度,使用下标会报错
  • Stringchars() 会遍历各个 char 值,bytes() 会返回每个字节,合法的 Unicode 可能会占用 1 字节以上
1
2
3
4
5
6
use std::collections::HashMap;
fn main() {
let teams = vec![String::from("blue"), String::from("yellow")];
let initial_scores = vec![10, 50];
let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
}
  • 实现了 Copy trait 的类型,他们的值会被复制到哈希映射中,但是比如 String 这种持有所有权的值,其值、所有权都会转移到哈希映射
  • HashMap 提供了 entry(),它返回一个枚举检查键值是否存在,如果不存在可以使用 or_insert() 将参数作为新值插入,并把新值的可变引用返回

9. 错误

  • Rust 错误分为『可恢复』与『不可恢复』,针对『可恢复』错误提供了 Result<T, E>,『不可恢复错误』提供了中止运行的 panic!
  • panic! 发生时,Rust 沿着调用栈的反向顺序遍历所有函数并清理数据,也可以立即中止程序不进行清理,需要在配置中使用中止模式
  • 使用 RUST_BACKTRACE=1 cargo run 可以查看回溯表
  • 可以使用 match 匹配错误不同的情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("s.txt");
let f = match f {
Ok(file) => file,
Err(e) => match e.kind() {
ErrorKind::NotFound => match File::create("s.txt") {
Ok(fc) => fc,
Err(err) => panic!("{:#?}", err),
},
other_error => panic!("{:#?}", other_error)
},
};
}
  • 这与下面的方法等价,可以少写不少 match
1
2
3
4
5
6
7
8
9
10
11
12
13
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("s.txt").map_err(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("s.txt").unwrap_or_else(|error| {
panic!("{:#?}", error)
})
} else {
panic!("{:#?}", error)
}
});
}
  • unwrap 当 Result 是 Ok 时,自动解析结果,否则调用 panic!
  • expectunwrap 的基础上附带错误信息,推荐使用这个
  • Rust 中 ? 是传播错误的快捷方式,此外 ? 只适用于返回 Result 的函数
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 f = File::open("s.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
  • Rust 中的 ? 还可以链式处理
1
2
3
4
5
6
7
8
9
use std::io;
use std::io::Read;
use std::fs::File;

fn read_user_name_from_file() -> Result<String, io::Error> {
let mut s = String::new();
File::open("s.txt")?.read_to_string(&mut s)?;
Ok(s)
}
  • 除了以上这些直接,还可以使用 std::fs 下的 fs::read_to_string() 它已经做好了封装
  • main 的返回类型除了 () 还可以是 Result<T, E>Box<dyn Error> 被称为 trait 对象,表示任何可能的错误类型
1
2
3
4
5
6
7
8
use std::error::Error;
use std::fs;

fn main() -> Result<(), Box<dyn Error>> {
let f = fs::read_to_string("s.txt")?;
println!("{}", f);
Ok(())
}
  • panic! 对外抛出异常,应该优选选择 Result
  • 测试时,应该使用 unwrapexpect 作为占位符,测试时以 panic! 作为标记
  • 如果代码处于『损坏』状态,应该使用 panic!;在自定义类型中,也可以配合 panic! 防止代码出现『损坏』状态的情况

10. 泛型&trait&生命周期

  • 抽取相同功能作为函数,抽象逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("{}", result);
}
fn largest(list:
[i32]) -> i32 {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
  • 与此同时希望抽象类型,引入泛型,如果需要对所有类型实现判断 > 判断还需要实现 std::cmp::PartialOrd 这个 trait
1
2
3
4
5
6
7
8
9
10
11
12
13
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T>{
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point{x: 5, y: 10};
println!("p.x={}", p.x());
}
  • 在方法中使用泛型,需要在方法名后立即使用泛型的定义,如上方法 x 所示,注意这里的 <T> 是为了后续的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Point<T, U> {
x: T,
y: U,
}
impl<T, U> Point<T, U>{
fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
Point {
x: self.x,
y: other.y,
}
}
}
fn main() {
let p1 = Point{x: 5, y: 10.4};
let p2 = Point{x: "hello", y: 'c'};
let p3 = p1.mixup(p2);
println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}
  • 如上的泛型方法中,方法 mixup 的类型 V, W 仅与当前方法有关,而结果显然是 T, W,这里被类、方法的泛型共同定义
  • rust 中泛型方法与具体类型的代码不会有任何速度上的差异,这是编译时完成的单态化,泛型被编译器生成了特定的定义,下面两段代码,第二段是编译器自动解析的
1
2
let integer = Some(5);
let float = Some(5.0);
1
2
3
4
5
6
7
8
9
10
11
12
enum Option_i32{
Some(i32),
None,
}
enum Option_f64{
Some(f64),
None,
}
fn main() {
let integer = Option_i32::Some(5);
let float = Option_f64::Some(5.0);
}
  • trait 是 rust 编译器描述某些特定类型拥有的且能够被其他类型共享的功能。它与 interface 类似,但是不尽相同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub head: String,
pub location: String,
pub authoer: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("head:{}, location:{}, authoer:{}, content: {}", self.head, self.location, self.authoer, self.content)
}
}
fn main() {
let xx = NewsArticle {
head: String::from("h"),
location: String::from("l"),
authoer: String::from("a"),
content: String::from("c"),
};
println!("{}", xx.summarize());
}
  • 上面是一个实现 trait 的例子,只有当 trait 与类型定义在我们的库中时,才能实现对应的 trait,但是我们不能在『外部类』中实现『外部 trait』,这被称为孤儿规则
  • trait 方法可以有默认的实现
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
pub trait Summary {
fn summarize(&self) -> String {
String::from("Read more...")
}
}
pub struct NewsArticle {
pub head: String,
pub location: String,
pub authoer: String,
pub content: String,
}
impl Summary for NewsArticle {
// fn summarize(&self) -> String {
// format!("head:{}, location:{}, authoer:{}, content: {}", self.head, self.location, self.authoer, self.content)
// }
}
fn main() {
let xx = NewsArticle {
head: String::from("h"),
location: String::from("l"),
authoer: String::from("a"),
content: String::from("c"),
};
println!("{}", xx.summarize());
}
  • trait 可以作为参数传递,比如 pub fn notify(item: impl Summary),这里的 impl Summary 表示传入的参数必须实现 Summary 这个 trait;同时它仅仅是一个语法糖,换一种写法 pub fn notify<T: Summary>(item: T) 效果相同,这里的 T 可以被多次使用,比如 fn notify(item1: T, item2: T),这里的 T 是同一个类型
  • 通过 + 可以指定多个 trait 约束,比如 pub fn notify(item: impl Summary + Display),这里的 item 必须实现 SummaryDisplay 这两个 trait;同样,它也可以被写作 pub fn notify<T: Summary + Display>(item: T),这里的 T 必须实现 SummaryDisplay 这两个 trait
  • 这样的指定比较繁琐,可以使用 where 关键字,比如 pub fn notify<T>(item: T) where T: Summary + Display,这里的 T 必须实现 SummaryDisplay 这两个 trait
  • 返回值也可以是 trait,比如 fn returns_summarizable() -> impl Summary,这里的返回值必须实现 Summary 这个 trait,但是注意只能返回一个类型,不能返回两个不同的类型,比如 fn returns_summarizable(flag: bool) -> impl Summary { if flag { NewsArticle{} } else { Tweet{} } } 这样是不行的,因为 NewsArticleTweet 是不同的类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter(){
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("{}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("{}", result);
}
  • 通过 trait 的方式实现泛型,可以避免代码重复,比如上面的 largest 函数,如果不使用泛型,那么就需要写两个函数,一个是 largest_i32,一个是 largest_char,这样就会有很多重复的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self {
x, y
}
}
}
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("x is larger");
} else {
println!("y is larger");
}
}
}
  • 上面的例子中,Pair 结构体有一个泛型 T,它有一个 new 方法,这个方法可以创建一个 Pair 实例,同时 Pair 还有一个 cmp_display 方法,这个方法可以比较 xy 的大小,但是这个方法有一个限制,就是 T 必须实现 DisplayPartialOrd 这两个 trait,这样的限制可以保证 cmp_display 方法可以使用 xyDisplay 方法,同时可以使用 xyPartialOrd 方法
  • 普通泛型可以确保类型拥有期望的行为,『生命周期』可以确保引用在我们的使用过程中一直有效
1
2
3
4
5
6
7
{
let r;
{
let x = 5;
r = &x;
}
}
  • 上面的代码将会报错,因为 x 的生命周期比 r 的生命周期短,所以 r 引用的 x 已经被释放了,所以 r 引用的是一个无效的值
1
2
3
4
{
let x = 5;
let r = &x;
}
  • 上面的代码是可以正常运行的,因为 x 的生命周期比 r 的生命周期长,所以 r 引用的 x 一直有效
1
2
3
4
5
6
7
8
9
10
11
12
13
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), &string2);
println!("The largest string is {}", result);
}
  • 上面的代码中使用了 'a 显式声明声明周期,这里的 'a 表示 xy 的生命周期一样长,所以返回值的生命周期也是 'a,这样就可以保证返回值的引用一直有效(如果不加,我们就不知道 xy 哪一个被返回,也就不知道他们的生命周期的长度,将会报错),上面的代码 'a 的意思是返回值生命周期会使用 xy 中生命周期较短的那个
1
2
3
4
5
6
7
8
9
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {}", result);
}
  • 上面的代码将会报错,因为 string2 的生命周期比 result 的生命周期短,所以 result 引用的 string2 已经被释放了,所以 result 引用的是一个无效的值
  • 『生命周期』语法就是用来关联一个函数中不同参数及返回值的生命周期的。一旦它们形成了某种联系,Rust 就获得了足够的信息来支持保障内存安全的操作,并阻止那些可能会导致悬垂指针或其他违反内存安全的行为的代码
  • 在么有显示标注的情况下,编译器目前有 3 种规则计算引用的生命周期:
    • 一,每一个引用参数都会拥有自己的生命周期参数
    • 二,当只有一个输入生命周期参数时,这个生命周期会被赋给所有输出生命周期参数
    • 三,当拥有多个输入生命周期参数,其中一个是 &self&mut self 时,self 的生命周期会被赋给所有输出生命周期参数
  • 有一种特殊的生命周期 'static,表示整个程序的执行期
1
2
3
4
5
6
7
8
9
fn longest_with_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
}
}
  • 上面的代码中,longest_with_announcement 函数有三个参数,其中 ann 参数是泛型,这个泛型必须实现 Display trait,这样才能使用 Displayfmt 方法,这个泛型没有生命周期,所以不需要加 'a,但是 xy 参数都有生命周期,所以需要加 'a,这样才能保证返回值的引用一直有效

11. 测试

  • rust 中将需要测试的函数头中增加 #[test] 标注,然后使用 cargo test 命令进行测试,测试函数中使用 assert! 宏进行断言,如果断言失败,测试将会失败,如果断言成功,测试将会成功
  • assertt_eq!assert_ne! 宏可以用来比较两个值是否相等或不相等,后者在不相等时通过;如果是自定义类型,需要自行实现 PartialEqDebug 这两个 trait,可以再定义的上方添加 #[derive(PartialEq, Debug)] 注解来自动实现这两个 trait
  • assert 中还可以增加自定义的错误信息,当断言失败时,可以根据错误信息来判断是哪里出错了
  • 可以在 #[test] 下方使用 #[shold_panic] 表示预期出现 panic,如果没有出现 panic,测试将会失败,同时参数 expect 表示预期中的 panic 信息,如果 panic 信息不一致,测试也将会失败
  • 此外还可以使用 Result<T, E> 编写测试,此时不要加上 #[shold_panic],而是应该直接返回一个 Err 值,同时可以使用 ? 运算符来简化代码
  • cargo test 默认使用并行执行,可能因此出现测试错误,可以使用 cargo test -- --test-threads=1 控制执行线程数
  • 使用 cargo test -- --nocapture 可以显示测试函数中的打印信息
  • 可以使用 cargo test name 中只要包含 name 的测试函数将会被执行
  • 可以在代码中通过 #[ignore] 来忽略某些测试函数,此外可以通过 cargo test -- --ignored 来执行被忽略的测试函数
  • 集成测试代码被放在 src 目录同级的 tests 文件中,集成测试中需要使用 use xxx 这是因为集成测试中每一个文件都是一个独立的包,同时不需要使用 [cfg(test)] 因为 rust 对 tests 目录做了特殊处理
  • 因为集成测试中的代码都是独立的包,所以需要一个库来引入其他包,如果没有 src/lib.rs,我们可以创建一个空的 src/lib.rs 文件,然后在 Cargo.toml 中增加 [[bin]] 来创建一个二进制包,这样就可以在 tests 中引入这个二进制包了

12. 命令行程序

  • std::env 可以获取输入信息,入参 &x[0] 存储的是程序的名称,&x[1] 存储的是第一个参数,以此类推
  • 将程序拆分成 main.rslib.rs 做到关注点分离,main.rs 中只有一些简单的逻辑,而 lib.rs 中包含所有的业务逻辑,这样可以方便测试

13. 函数式

14. Cargo

15. 指针

16. 并发

17. 面向对象

18. 模式匹配

19. 高级特性

20. web 服务器