Rust
0x01 准备
(1)安装 Rust
- 安装 Rust:
- Windows 系统在官网下载 Rust
- 版本检测:
rustc --version
- 查看文档:
rustup doc
- 更新 Rust:
rustup update
- 卸载 Rust:
rustup self uninstall
在 Clion 中使用 Rust 开发,环境配置参考《在Windows上搭建Rust开发环境——Clion篇 | CSDN-swanmy》
(2)Hello, World!
-
编写
// filename: main.rs fn main() { println!("Hello, world!"); }
-
编译:
rustc main.rs
-
运行:
./main.exe
- 分析上述程序:
fn main(){}
是主函数,是每个 Rust 可执行程序最先运行的代码- Rust 的缩进是 4 个空格
println!
是一个 Rust macro[^宏],如果是函数时就没有!
- 代码行以
;
结尾
(3)Cargo
-
Cargo 是 Rust 的构建系统和包管理工具
-
创建项目:
cargo new [projectname]
-
项目结构:
graph TB A(Project)-->src -->main.rs A-->Cargo.toml A-->.gitignore- Cargo.toml
- TOML(Tom's Obvious Minimal Language)格式是 Cargo 的配置格式
[package]
是一个区域标题,表示下方内容是用来配置包的name
:项目名version
:项目版本authors
:项目作者edition
:使用 Rust 的版本
[dependencies]
表示另一个区域的开始,会列出项目的依赖项- 代码的包称作 crate
- src
- 保存全部源代码
- 将所有代码移到 src 目录下,创建 Cargo.toml 文件并填写配置信息,从而将项目转换为 Cargo 项目
- Cargo.toml
-
构建项目:
cargo build
- 生成可执行文件:target\debug[projectname].exe
- 生成 cargo.lock
- 用于追踪项目依赖的精确版本
-
运行项目:
cargo run
-
编译检查:
cargo check
-
发布版本:
cargo build --release
-
0x02 猜数游戏
-
Cargo.toml
[package] name = "Project" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] rand = "0.8.5"
-
src/main.rs
use std::io; use rand::Rng; use std::cmp::Ordering; fn main() { println!("猜数游戏"); // 生成随机数 let random_number = rand::thread_rng().gen_range(1..101); loop { println!("猜测一个数:"); let mut guess = String::new(); // 输入字符串 io::stdin().read_line(&mut guess).expect("无法读取"); // 字符串转数字及异常处理 let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => { println!("输入错误"); continue; }, }; println!("猜测了 {}", guess); // 比较 match guess.cmp(&random_number) { Ordering::Less => println!("small"), Ordering::Greater => println!("BIG"), Ordering::Equal => { println!("Win"); break; }, } } }
0x03 基本概念
(1)变量与可变性
-
声明变量使用
let
关键字- 默认情况下,变量不可变
- 声明变量时,在变量前面加上
mut
使得变量可变
-
常量(constant)在绑定值后不可变
-
不可以使用
mut
修饰 -
声明常量使用
const
,其类型编写被标注 -
常量可以在任何作用域内进行声明,包括全局作用域
-
常量只可以绑定到常量表达式
-
在程序运行期间,常量在其声明的作用域内永久有效
-
常量命名规范:全大写,单词间下划线分隔
const MAX_SIZE:u32 = 100_000;
-
-
隐藏(shadowing)
-
可以使用相同的名字声明新的变量,新的变量就会隐藏之前声明的同名变量
-
报错写法
let x = 1; x = x + 1;
-
使用隐藏
let x = 1; let x = x + 1;
-
-
与
mut
的区别-
如果不使用
let
关键字那么重新给非mut
的变量赋值会导致编译时错误 -
使用
let
声明的同名新变量依然不可变 -
使用
let
声明的同名新变量可能类型不同fn main() { let str = "abcde"; println!("{}", str); // output: abcde let str = str.len(); println!("{}", str); // output: 5 }
-
-
(2)数据类型
-
分为标量、复合类型
-
Rust 是静态编译语言,在编译时必须知道所有变量的类型
-
基于使用的值,编译器通常能推断出它的具体类型。但当可能的类型比较多时,就必须添加类型标注
-
报错写法
fn main() { let guess = "12345".parse().expect("Error"); println!("{}", guess); }
-
正确写法
fn main() { let guess:u32 = "12345".parse().expect("Error"); println!("{}", guess); }
-
-
a. 标量类型
-
一个标量类型代表一个单个的值
-
Rust 有四个主要的标量类型
-
整数类型
-
没有小数部分
-
u32
代表占据 32 位空间的无符号整数 -
i32
代表占据 32 位空间的有符号整数 -
isize
与usize
- 以上类型的位数由程序运行的计算机架构所决定
- 一般在对某种集合进行索引操作时使用
-
整数字面值
- 除了
byte
类型外,所有数值字面值都允许使用类型后缀,如1234u8
,默认后缀为i32
Number Literals Example Decimal 123_456
Hex 0xdef
Octal 0o777
Binary 0b1100_1000
Byte (u8 only) b'A'
- 除了
-
整数溢出
- Debug 时,会 panic
- Release 时,不会 panic,会将数值递归减去范围最大值,直至结果在范围内
-
-
浮点类型
f32
,32 位,单精度f64
,64 位,双精度- 使用了 IEEE-754 标准描述
f64
为默认类型
-
布尔类型
- 符号为
bool
- 取值分别为
true
和false
- 大小为 1 byte
- 符号为
-
字符类型
- 符号为
char
- 大小为 4 byte、
- 字面值使用单引号
- 编码方式为 Unicode
- 符号为
-
b. 复合类型
-
复合类型可以将多个值放在一个类型里
-
Rust 有两种基础的复合类型
-
元组(Tuple)
-
可以将多个类型的多个值放在一个类型里
-
一旦声明就无法改变元组的长度
-
创建 Tuple
fn main() { let tup:(i32, f64, u8) = (32, 6.4, 1); }
-
获取 Tuple 的元素值
fn main() { let tup:(i32, f64, u8) = (32, 6.4, 1); let (x, y, z) = tup; println!("{}, {}, {}", x, y, z); }
-
访问 Tuple 的元素
fn main() { let tup:(i32, f64, u8) = (32, 6.4, 1); println!("{}, {}, {}", tup.0, tup.1, tup.2); }
-
-
数组
-
其中每个元素类型相同
-
长度固定
- 数组类型:
[类型; 长度]
,如[i32; 5]
- 数组类型:
-
声明数组
-
方法一
fn main () { let a = [1, 2, 3, 4, 5]; }
-
方法二:
[初始值; 数组长度]
fn main () { let a = [3; 5]; }
-
-
对比
- 优点
- 将数据存放在 stack 而非 heap上
- 保证固定数量的元素
- 缺点
- 不如 Vector 灵活
- 优点
-
访问元素
-
数组是 Stack 上分配的单个块的内存
-
可以使用索引来访问数组元素
fn main () { let a = [3; 5]; println!("{}", a[3]); // output: 3 }
-
索引超出范围时,编译通过,而运行时 panic
-
-
-
(3)函数与注释
a. 函数
-
使用
fn
关键字声明函数 -
命名采用 snake case 方式(全部小写、下划线分隔)
-
参数
-
parameters/arguments:形参/实参
-
在函数签名里必须声明每个参数的类型
fn main () { function(3, 4); } fn function(x: i32, y: i32) { println!("x={}", x); println!("y={}", y); }
-
-
函数体中的语句与表达式
- 函数体由一系列语句组成,可选的由一个表达式结束
- Rust 是基于表达式的语言
- 语句是执行一些动作的指令
- 表达式会计算产生一个值
- 函数的定义也是语句
- 语句不返回值,因此不可使用
let
将一个语句赋给一个变量
fn main () { let y = { let x = 3; x + 1 }; println!("{}", y); }
-
函数的返回值
- 在
->
符号后声明函数的返回值类型,但不可以为返回值命名 - Rust 中返回值就是函数体中最后一个表达式的值
- 若想提取返回,则需使用
return
关键字,并指定一个返回值
fn main () { let x = 3; let y = function(x); println!("{}", y); } fn function (x: i32) -> i32 { return 10; }
- 在
b. 注释
- 单行注释:
// [content]
- 多行注释:
/* content */
(4)控制流
a. 逻辑判断
-
if
表达式- 根据
bool
类型的条件来执行不同的代码分支 - 与条件相关联的代码块称作分支(arm)
- 可选添加
else
fn main () { let x = 3; if x > 10 { println!("yes"); } else { println!("no"); } println!("{}", x); }
- 根据
-
使用
else if
处理多重条件- 当使用了多个
else if
时,最好使用match
重构代码
- 当使用了多个
-
在
let
语句中使用if
fn main () { let mut x = 3; let y = if x > 3 {x = 4} else {x = 5}; println!("{}", x); // output: 5 }
b. 循环
-
loop
循环- 默认无条件循环执行其中代码块
- 可以使用
break
终止循环
fn main () { let mut counter = 0; let result = loop { counter += 1; if counter == 10 { break counter; } }; println!("Result: {}", result); // output: Result: 10 }
-
while
循环- 对某个条件进行判断后决定是否执行循环
fn main () { let mut number = 10; while number != 0 { println!("{}", number); number -= 1; } println!("{}", number); // output: 0 }
-
for
循环- 主要用于安全、简洁遍历集合
fn main () { let arr = [1, 2, 3, 4, 5]; for element in arr.iter() { println!("{}", element); } }
-
Range
- 该关键字由标准库提供
- 在 Range 中指定开始数字
b
和结束数字e
,可生成[b, e)
之间的数字 - 使用
.rev()
方法可以翻转 Range
fn main () { for element in (1..4).rev() { println!("{}", element); } }
0x04 所有权
(1)概述
- Rust 的核心特性就是所有权
- 所有程序运行期间都必须管理其使用计算机内存的方式
- 所有权解决的问题
- 跟踪代码使用堆中数据情况
- 最小化堆中数据重复量
- 清理堆中数据以避免空间不足
a. 栈内存与堆内存
- 存储数据
- 栈内存按值接收的顺序来存储,按相反的顺序来读出(后进先出,LastInFirstOut)
- 栈内存中的数据存取操作名称:压入栈(存储数据)、弹出栈(读出数据)
- 所有在栈中存储的数据必须拥有已知的固定大小,否则存入堆中
- 操作系统会在堆中找到一块足够大的空间并返回一个指针,这个过程称为分配
- 访问数据
- 在堆中访问数据需要通过指针,比栈慢
- 函数调用
- 当调用函数时,函数本地的变量被压到栈上,当函数结束后,这些值就会从栈上弹出
b. 所有权规则
- 每个值都有一个变量,这个变量是该值的所有者
- 每个值有且仅有一个所有者
- 当所有者超过作用域(Scope)时将会被删除
- 变量作用域就是程序中一个项目的有效范围
为演示所有权相关规则,需要提取使用 String 类型数据
-
举例演示
-
创建 String 类型的值:
let s = String::from("Hello");
::
表示from
是 String 类型下的函数
-
字符串插入
fn main () { let mut s = String::from("Hello"); s.push_str(", world!"); println!("{}", s); }
-
c. 所有权与函数
-
在语义上,将值传递给函数和把值赋给变量是类似的
- 将值传递给函数将发生移动或复制
-
返回值与作用域
- 函数在返回值的过程中也发生所有权的转移
- 一个变量的所有权当把一个值赋给其他变量是就会发生移动,当一个包含堆数据的变量离开作用域时,除非数据的所有权被移动到另一个变量上,否则其值就会被 drop 函数清理
-
如何让函数使用值但不获取其所有权:
-
将参数传入后再传出以间接保障所有权
fn main() { let s1 = String::from("Hello"); let (s2, len) = function(s1); println!("The length of {} is {}.", s2, len,) // The length of Hello is 5. } fn function(s: String) -> (String, usize) { let length = s.len(); (s, length) }
-
由于上述方法过于麻烦,故而可使用引用来实现
-
(2)引用与借用
a. 引用
-
&
符号标识引用:允许引用其值而不获取其所有权fn main() { let s1 = String::from("Hello"); let len = function(&s1); println!("The length of {} is {}.", s1, len,) } fn function(s: &String) -> usize { s.len() }
-
默认引用不可变
b. 借用
- 把引用作为函数参数的行为叫做借用
- 借用的东西不可更改
c. 可变引用
-
可变引用:通过添加
mut
实现fn main() { let mut s1 = String::from("Hello"); let len = function(&mut s1); println!("The length of {} is {}.", s1, len,) } fn function(s: &mut String) -> usize { s.push_str(", world"); s.len() }
-
重要限制一:在特定作用域内,对某一块数据只能有一个可变引用
- 防止编译时产生数据竞争
数据竞争发生原因:
- 两个或多个指针同时访问同一个数据
- 至少有一个指针用于写入数据
- 没有使用任何机制来同步对数据的访问
-
可以创建新的作用域来允许非同时的创建多个可变引用
fn main() { let mut s = String::from("Hello"); { let s1 = &mut s; } let s2 = &mut s; }
-
重要限制二:禁止同时拥有一个可变引用和一个不变引用
- 可以拥有多个不变引用
d. 引用的作用域
- 从声明到最后一次使用
e. 悬空引用
- 悬空指针:一个指针引用了内存中的某个地址,而这块内存可能已经释放并被重新分配掉
- 在 Rust 中,编译器可保证引用永远都不是悬空引用。如果引用了某些数据,则编译器将保证在引用离开作用域之前数据不会离开作用域
(3)切片
-
切片:一种不持有所有权的数据类型
-
举例:编写一个函数
- 该函数接受字符串作为参数
- 返回在这个字符串里找到的第一个单词
- 当未找到任何空格则返回整个字符串
fn main() { let mut s = String::from("hello world"); let idx = function(&s); println!("{}", idx); } fn function(s: &String) -> usize { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return i; } } s.len() }
- 上述代码存在问题:当在输出前执行
s.clear()
时,idx
的意义就不存在了。为此提出字符串切片
-
字符串切片
-
是指向字符串中一部分的引用
-
形式:
[起始索引 .. 终止索引]
fn main() { let s = String::from("hello world"); let s1 = &s[0 .. 5]; let s2 = &s[6 .. 11]; println!("{}, {}", s1, s2); }
-
注意:
-
字符串切片的范围索引必须发生在有效的 UTF-8 字符边界内
-
当尝试从一个多字节的字符中创建字符串切片时,程序会报错退出
-
重写上述例子:
fn main() { let mut s = String::from("hello world"); let s1 = function(&s); println!("{}", s1); } fn function(s: &String) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[..i]; } } &s[..] }
-
-
-
字符串字面值是切片,其被直接存储在二进制程序中
-
将字符串切片作为参数传递:
fn function(s: &str) -> &str {}
- 此时可以同时接受切片或转换成切片的字符串两种类型的数据
- 定义函数时使用字符串切片来代替字符串引用会使 API 更加通用,且不会损失任何功能
fn main() { let s1 = String::from("hello world"); let s1_1 = function(&s[..]); let s2 = "hello world"; let s2_2 = function(s2); } fn function(s: &str) -> &str { &s[..] }
0x05 Struct
struct,结构体
- 自定义的数据类型
- 为相关联的值命名与打包
(1)定义并实例化 struct
a. 定义 struct
-
使用 struct 关键字,并为整个 struct 命名
-
在花括号内,为所有字段定义名称和类型
struct Student { name: String, age: u8, active: bool, }
b. 实例化 struct
-
需要创建 struct 实例
- 为每个字段指定具体值
- 无需按声明顺序进行指定
let mut s = Student { name: String::from("SRIGT"), age: 22, active: true, };
-
获取 struct 中的某个值
- 使用点标记法
s.name = String::from("SRIGT");
一旦 struct 的实例是可变的,那么实例中所有的字段都是可变的
-
struct 作为函数返回值
fn create_student(name: String) -> Student { Student { name: name, age: 0, active: true, } } fn main () { let mut s = create_student("SRIGT".to_string()); println!("{}", s.name); }
-
字段初始化简写
- 当字段名与字段值对应变量名相同时,就可以使用以下简写方法
fn create_student(name: String, age: u8) -> Student { Student { name, age, active: true, } } fn main () { let mut s = create_student("SRIGT".to_string(), 18); println!("{}", s.name); }
-
-
struct 更新语法
- 当需要基于已有的某个 struct 来创建另一个 struct 时使用以下更新方法
let mut s1 = Student { name: String::from("SRIGT"), age: 18, active: true, }; let mut s2 = Student { name: String::from("白月"), age: s1.age, active: false, };
-
简写
let mut s2 = Student { name: String::from("白月"), ..s1 };
-
Tuple struct
- 可以定义类似 tuple 的 struct,其整体有名称,里面的元素没有
struct Color(i32, i32, i32); let black = Color(0, 0, 0);
-
Unit-Like Struct
- 可以定义没有任何字段的 struct
- 适用于需要某个类型上实现某个 trait,但在里面又没有需要存储的数据时
-
struct 数据所有权
- 对于
struct Student { name: String };
,这里的字段使用了 String 而不是&str
- 该 struct 实例了其拥有的所有数据
- 只要 struct 实例是有效的,那么里面的字段也是有效的
- struct 里也可以存放引用,但需要使用生命周期
- 生命周期中保证只要 struct 实例时有效的,那么里面的引用也是有效的
- 如果 struct 里面存储引用,而不使用生命周期,就会报错
- 生命周期中保证只要 struct 实例时有效的,那么里面的引用也是有效的
#[derive(Debug)] struct Node { val: i32, } fn main () { let node = Node { val: 20}; println!("{:?}", node); }
- 对于
(2)struct 方法
-
方法与函数类似:
fn
关键字、名称、参数、返回值 -
方法与函数区别
- 方法在 struct(或 enum、trait 对象)的上下文中定义
- 第一个参数是
self
,表示方法被调用的 struct 实例
a. 定义方法
- 在
impl
块里定义方法 - 方法的第一个参数可以是
&self
,也可以是获得其所有权或可变借用(&mut self
) - 更良好的代码组织
struct node {
number1: u32,
number2: u32,
}
impl node {
fn sum(&self) -> u32 {
self.number1 + self.number2
}
}
fn main() {
let node = node {
number1: 1,
number2: 2,
};
println!("sum(1, 2) = {}", node.sum());
}
-
方法调用的运算符
- Rust 会自动引用或解引用(在调用方法时会发生这种行为)
- 在调用方法时,Rust 根据情况自动添加
&
、&mut
或*
,以便object
可以平铺方法的签名
-
每个 struct 允许拥有多个
impl
块
b. 关联函数
-
可以在
impl
块里定义不把self
作为第一个参数的函数,如:String::from()
-
关联函数通常用于构造器
::
符号一般用于关联函数与模块创建的命名空间
struct node { number1: u32, number2: u32, } impl node { fn equal(number: u32) -> node { node { number1: number, number2: number, } } } fn main() { let node = node::equal(1); }
0x06 枚举与模式匹配
(1)定义枚举
-
枚举允许列举所有可能的值来定义一个类型
enum Kind { A, B, } fn main() { let a = Kind::A; let b = Kind::B; function(a); function(b); println!(""); } fn function(kind: Kind) { match kind { Kind::A => println!("A"), Kind::B => println!("B"), } }
-
枚举的变体都位于标识符的命名空间下,使用
::
进行分隔-
enum 的变体若附带有值,那么就会在函数的命名空间下生成一个函数
-
将数据附加到枚举的变体中
- 不需要额外使用 struct
- 每个变体可以拥有不同的类型以及关联的数据量
enum Kind { A(String), B(u32) } fn main() { let a = Kind::A("abc".to_string()); let b = Kind::B(100); }
-
-
为枚举定义方法
- 使用
empl
关键字
- 使用
(2)Option 枚举
-
定义于标准库中
-
在 Prelude(预导入模块)中
-
描述了某个值可能存在或不存在的情况
-
Rust 中类似 Null 概念的枚举——
Option<T>
-
在标准库中定义为:
enum Option<T> { Some(T), None, }
-
避免将
Option<T>
直接当成 T,若想使用Option<T>
中的 T,则必须将它转换为 T
-
(3)控制流运算符 match
-
允许一个值与一系列模式进行匹配,并执行匹配的模式对应的代码
- 模式可以为字面值、变量名、通配符……
enum Kind { A, B } fn change(kind: Kind) -> String { match kind { Kind::A => String::from("a"), Kind::B => String::from("b") } } fn main() {}
-
绑定值的模式
- 匹配的分支可以绑定到任意被匹配对象的部分值
#[derive(Debug)] enum Kind2 { A, B } enum Kind { A, B(Kind2) } fn change(kind: Kind) -> u8 { match kind { Kind::A => 1, Kind::B(kind2) => { println!("{:?}", kind2); 2 } } } fn main() { let b = Kind::B(Kind2::A); println!("{}", change(b)); }
-
匹配
Option<T>
fn main() { let one = Some(1); let two = Some(one); let three = Some(None); } fn function(x: Option<i32>) -> Option<i32> { match x { Some(x) => Some(x + 1), None => None, } }
-
match
匹配必须穷举所有的可能- 或使用
_
通配符替代未列出的值
- 或使用
(4)if let
- 处理只关心一种匹配而忽略其他匹配情况
- 更少的代码与缩进
- 放弃了穷举的可能
- 可看作是
match
的语法糖
fn main() {
let x = Some(0u8);
if let Some(0) = x {
println!("0")
}
}
0x07 Package, Crate, Module
(1)Rust 的代码组织
- 可以暴露或私有的细节
- 作用域内有效的名称
(2)模块系统
-
Package(包):Cargo 的特性,用于构建、测试、共享 crate
- 包含一个 Cargo.toml,描述了构建 Crates 的方法
- 只能包含 0 或 1 个 library crate
- 可以包含人员数量的 binary crate
- 但必须至少包含一个 crate
-
Crate(单元包):一个模块树,可产生一个 library 或可执行文件
-
类型:binary 或 library
-
Crate Root:
- 是源代码文件,是 Rust 编译器的入口
Cargo 项目默认设置:
- src/main.rs:
- binary crate 的 crate root
- crate 名与 package 名相同
- src/lib.rs:
- package 包含一个 library crate
- library crate 的 crate root
- crate 名与 package 名相同
- Cargo 将 crate root 文件交给 rustc 来构建 library 或 binary
-
Crate 的作用
- 将相关功能组合到一个作用域内,便于在项目间进行共享
- 如 rand crate,访问其功能时需要通过其名字 rand
- 将相关功能组合到一个作用域内,便于在项目间进行共享
-
-
Module(模块):利于控制代码的组织、作用域、私有路径
-
在一个 crate 内将代码进行分组
-
增加可读性,易于复用
-
控制项目的私有性(私有边界 privacy boundary)
- 模块不仅可以组织代码,还可以定义私有边界
- 如果想把函数或 struct 设为私有,则可将其放入某个模块中
- Rust 中的所有条目默认私有
- 父级模块无法访问子模块的私有条目
- 子模块可以使用所有祖先模块的条目
- 使用
pub
关键字可以将某些条目标记为公共的
-
建立 module
- 关键字
mod
- 可嵌套
- 可包含其他项的定义
mod module_a { mod module_a_1 { fn function_1() {} fn function_2() {} } mod module_a_2 { fn function_2() {} fn function_3() {} } }
- 关键字
-
-
Path(路径):为 struct、function 或 module 等项命名的方式
-
为了在 Rust 的模块中找到某个条目,需要使用路径
-
路径的形式
- 绝对路径:从 crate root 开始,使用 crate 名或字面值 crate
- 相对路径:从当前模块开始,使用
self
、super
或当前模块的标识符
-
路径至少有一个标识符组成,标识符之间使用
::
mod module_outside { pub(crate) mod module_inside { pub(crate) fn function_1() {} pub(crate) fn function_2() {} } } pub fn function() { crate::module_outside::module_inside::function_1(); module_outside::module_inside::function_2(); }
super
关键字可以用来访问父级模块路径中的内容
-
(3)use
关键字
-
可以使用
use
关键字将路径导入作用域内mod module_outside { pub mod module_inside { pub fn function_1() {} } } use crate::module_outside::module_inside; pub fn function() { module_inside::function_1(); }
-
use
的习惯用法- 将函数的父级模块引入作用域
- 指定 struct、enum 等的完整路径
- 指定同名条目到父级
-
使用
pub use
重新导出名称- 使用
use
将路径导入到作用域内后,该名称在此作用域内是私有的 - 重导出
- 将条目引入作用域
- 该条目可以被外部代码引入到其作用域中
- 使用
-
as
关键字as
关键字可以为引入的路径指定本地的别名
0x08 常用的集合
(1)Vector
a. Vec<T>
称作 vector
- 由标准库提供
- 可存储多个值
- 智能存储相同类型的数据
- 值在内存中连续存放
b. 创建 Vector
-
Vec::new
函数fn main() { // let vec: Vec<i32> = Vec::new(); let mut v = Vec::new(); v.push(1); v.push(2); }
-
使用初始值创建
Vec<T>
,使用vec!
宏fn main() { let v = vec![1, 2, 3, 4]; }
c. 删除 Vector
-
与任何其他 struct 一样,当 Vector 离开作用域之后就会被清理
-
读取 Vector 元素
-
索引(越界:panic)
fn main() { let v = vec![1, 2, 3, 4]; let first: &i32 = &v[0]; println!("first: {:?}", first); } }
-
get
方法(越界:返回 None)fn main() { let v = vec![1, 2, 3, 4]; match v.get(1) { Some(x) => println!("v[1]: {}", x), None => println!("v[1]: None"), } }
-
d. 所有权和借用规则
- 不能再同一作用域内同时拥有可变和不可变引用
fn main() {
let mut v = vec![1, 2, 3, 4];
let first= &v[0];
v.push(5); // mutable borrow occurs here
println!("first: {}", first);
}
e. 遍历 Vector 的值
fn main() {
let mut v = vec![1, 2, 3, 4];
for i in &v {
println!("{}", i);
}
}
基于 vector 使用 enum 来存储多种数据类型
enum list { Int(i32), Float(f32), Str(String), } fn main() { let vec = vec![ list::Int(1), list::Float(2.0), list::Str("3".to_string()), ]; }
(2)String
a. 概述
- Rust 的核心语言层面,只有一个字符串类型:字符串切片 str(或
&str
) - 字符串切片:对存储再其他地方、UTF-8 编码的字符串的引用
- 字符串字面值:存储再二进制文件中,也是字符串切片
- String 类型
- 由标准库提供,而非核心语言
- 可增长、可修改、可拥有
- 采用了 UTF-8 编码
- 其他字符串类型:OsString、OsStr、CString、CStr
b. 创建字符串
-
String::new()
函数fn main() { let mut s = String::new(); }
-
使用初始值来创建 String
-
to_string()
方法,可用于实现了 Display trait 的类型,包括字符串字面值fn main() { let data = "hello"; let mut s = data.to_string(); }
-
String::from()
方法,从字面值创建 Stringfn main() { let data = "hello"; let mut s = String::from(data); }
-
c. 更新 String
-
push_str()
方法,把一个字符串切片附加到 Stringfn main() { let mut s = String::from("abc"); s.push_str("def"); println!("{}", s); }
-
push()
方法,把单个字符附加到 Stringfn main() { let mut s = String::from("abc"); s.push('d'); println!("{}", s); }
-
使用
+
直接连接字符串fn main() { let mut s1 = String::from("abc"); let mut s2 = String::from("def"); println!("{}", s1+&s2); }
-
使用
format!
宏连接多个字符串fn main() { let mut s1 = String::from("abc"); let mut s2 = String::from("def"); let s = format!("{}-{}", s1, s2); println!("{}", s); }
- 和
println!()
类似,但返回字符串 - 不会获得参数的所有权
- 和
d. 访问 String
-
Rust 的字符串不支持索引语法访问
- 索引操作应消耗一个常量时间(\(O(1)\)),而 String 无法保证需要遍历的内容是否存在非法字符
-
String 是对
Vec<u8>
的包装-
len()
方法fn main() { let len = String::from("abc").len(); println!("{}", len); }
-
Rust 中有三种看待字符串的方式
- 字节 Bytes
- 标量值 Scalar Values
- 字形簇 Grapheme Clusters
-
-
遍历 String
-
对于标量值使用
chars()
方法fn main() { let s = String::from("abc"); for i in s.chars() { println!("{}", i); } }
-
对于字节使用
bytes()
方法fn main() { let s = String::from("abc"); for i in s.bytes() { println!("{}", i); } }
-
e. 切割 String
-
可以使用
[]
和一个范围来创建字符串的切片(遵循 “ 左闭右开 ” 的范围内取值的规则)fn main() { let s = String::from("123456"); let s1 = &s[0..2]; println!("{}", s1); }
- 如果切割时跨越了字符边界,程序就会 panic
(3)HashMap
a. 概述
HashMap<K, V>
- 键值对的形式存储数据,一个键(Key)对应一个值(Value)
- Hash 函数决定如何在内存中存放 K 和 V
- 适用于通过任何类型的 K 来寻找对应的 V 数据,而非索引
- HashMap 使用频率较低,因此不在 Prelude 中
- 标准库对其支持较少,没有内置的宏来创建 HashMap
- HashMap 的数据存储在堆中
- HashMap 是同构的
- 即在一个 HashMap 中,所有的 K 类型相同、所有的 V 类型相同
b. 创建 HashMap
- 创建空 HashMap:
new()
函数 - 添加数据:
insert()
方法
use std::collections::HashMap;
fn main() {
let mut stus = HashMap::new();
stus.insert(String::from("Amy"), 18);
stus.insert(String::from("Bob"), 19);
}
-
可以使用
collect
方法创建 HashMap- 在元素类型为 Tuple 的 Vector 上使用
collect
方法- 要求 Tuple 有两个值,分别作为 K 和 V
collect
方法可以把数据整合成很多种集合类型
use std::collections::HashMap; fn main() { let name = vec![String::from("Amy")m String::from("Bob")]; let age = vec![18, 19]; let stus: HashMap<_, _> = name.iter().zip(age.iter()).collect(); }
- 在元素类型为 Tuple 的 Vector 上使用
c. HashMap 和所有权
- 对于实现了 Copy trait 类型(如
i32
),值会被复制到 HashMap - 对于拥有所有权的值(如 String),值会被移动,所有权会被转移给 HashMap
d. 访问 HashMap 中的值
-
get()
方法- 参数 K
- 返回值:
Option<&V>
use std::collections::HashMap; fn main() { let mut stus = HashMap::new(); stus.insert(String::from("Amy"), 18); stus.insert(String::from("Bob"), 19); let age = stus.get("Amy"); match age { Some(age) => println!("age is {}", age), None => println!("age is not found"), } }
e. 遍历 HashMap
use std::collections::HashMap;
fn main() {
let mut stus = HashMap::new();
stus.insert(String::from("Amy"), 18);
stus.insert(String::from("Bob"), 19);
for (k, v) in &stus {
println!("{}: {}", k, v);
}
}
f. 更新 HashMap
-
HashMap 的大小可变
-
每个 K 同时只能对应一个 V
-
更新 HashMap 中的数据
-
K 已经存在,对应了一个 V
-
替换现有的 V
-
强制替换
use std::collections::HashMap; fn main() { let mut stus = HashMap::new(); stus.insert(String::from("Amy"), 18); stus.insert(String::from("Amy"), 19); println!("{:?}", stus) }
-
判空后替换
- 使用
entry()
方法,检查指定的 K 是否对应一个 V- 参数为 K
- 返回值为
enum Entry
代表值是否存在
- Entry 的
or_insert()
方法:如果 K 不存在则插入新值,否则返回 V 的可变引用
use std::collections::HashMap; fn main() { let mut stus = HashMap::new(); stus.insert(String::from("Amy"), 18); stus.entry(String::from("Amy")).or_insert(19); stus.entry(String::from("Bob")).or_insert(20); println!("{:?}", stus) }
- 使用
-
-
保留现有的 V
-
合并新旧的 V
use std::collections::HashMap; fn main() { let mut map = HashMap::new(); let text = "The man is iron man"; for word in text.split_whitespace() { let cnt = map.entry(word).or_insert(0); *cnt += 1; } println!("{:?}", map) }
-
-
K 不存在
- 添加一对
<K, V>
- 添加一对
-
g. Hash 函数
- 默认情况下,HashMap 使用加密功能强大的 Hash 函数,可以抵抗 DoS 攻击
- 不是最快,但是安全性高
- 可以指定不同的 hasher 来切换至另一个 Hash 函数
- hasher 是实现 BuildHasher trait 的类型
-End-