Rust 基础入门(一)(已完结)
本文为阅读《Rust圣经》的第一篇笔记,内容包括《Rust基础入门》前4章节,后续章节请移步到本网站文章《Rust基础入门(二)》等,如果想查看原书请搜索“Rust圣经”或点击此链接:[Rust语言圣经(Rust Course)](https://course.rs/about-book.html)
变量绑定与解构
变量命名
Rust语言变量命名不能和关键字重复
变量绑定
将一个值绑定给一个变量,类似赋值但不完全是赋值,要注意“所有权”这个概念。
变量可变性
Rust变量默认情况下不可变:
不可变变量:
1
2let x = 5;
//此时不能再对x赋值,若有代码“x = 6;”则会报错可变变量:
1
2let mut x = 5;
//可以对x进行再赋值
未使用的变量
如果未使用的变量开头不是下划线,则会引发warning
避免方法:
变量开头使用下划线
使用 #![allow(unused)] 属性,例如:
1
2
3
4
fn main() {
let x = 1;
}
变量解构
1 | let (a, mut b): (bool,bool) = (true, false); |
常量
1 | const MAX_POINTS: u32 = 100_000; |
变量遮蔽
1 | fn main() { |
注意:使用变量遮蔽(let)可以改变变量的数据类型,但是不用let对可变变量进行不同类型的赋值是不允许的
基本类型
数值类型
- 数值类型:有符号整数 (i8, i16, i32, i64, isize)、 无符号整数 (u8, u16, u32, u64, usize) 、浮点数 (f32, f64)、以及有理数、复数
- 字符串:字符串字面量和字符串切片 &str
- 布尔类型:true 和 false
- 字符类型:表示单个 Unicode 字符,存储为 4 个字节
- 单元类型:即 () ,其唯一的值也是 ()
语句
语句完成了一个具体的操作,但是并没有返回值。语句一定以“;”结尾。
表达式
表达式会进行求值,然后返回一个值。表达式结尾不会有“;”。
函数
Rust 的函数体是由一系列语句组成,最后由一个表达式来返回值
1 | fn add_with_extra(x: i32, y: i32) -> i32 { |

当用 ! 作函数返回类型的时候,表示该函数永不返回( diverging functions ),特别的,这种语法往往用做会导致程序崩溃的函数:
1 | fn dead_end() -> ! { |
与函数有所区别的是宏调用,例如 println! 是宏调用,看起来像是函数但是它返回的是宏定义的代码块。常见宏调用如下:
1 | println!("a + b = {}", c); // 打印 |
所有权和借用
所有权
栈
栈按照顺序存储值并以相反顺序取出值,这也被称作后进先出。增加数据叫做进栈,移出数据则叫做出栈。
因为上述的实现方式,栈中的所有数据都必须占用已知且固定大小的内存空间,假设数据大小是未知的,那么在取出数据时,你将无法取到你想要的数据。
堆
与栈不同,对于大小未知或者可能变化的数据,我们需要将它存储在堆上。当向堆上放入数据时,需要请求一定大小的内存空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的指针,该过程被称为在堆上分配内存,有时简称为 “分配”(allocating)。
接着,该指针会被推入栈中,因为指针的大小是已知且固定的,在后续使用过程中,你将通过栈中的指针,来获取数据在堆上的实际内存位置,进而访问该数据。
所有权原则
所有权的规则:
- Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
- 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
- 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)
Rust语言的变量作用域和C等其他语言没有区别,例如:
1 | { // s 在这里无效,它尚未声明 |
初识String
Rust 为我们提供动态字符串类型: String,该类型被分配到堆上,因此可以动态伸缩,也就能存储在编译时大小未知的文本。
String 的用法如下:
1 | let mut s = String::from("hello"); |
注:String 不是基本类型,String 类型包含了堆指针、字符串长度、字符串容量等多个复杂内容。
变量绑定后的数据交互
转移所有权:将一个值的所有权进行转移,原变量名会失去这个值的所有权。
1
2let s1 = String::from("hello");
let s2 = s1;此例中,s1将会失去”hello”的所有权,运行
1
println!("{}, world!", s1);
会报错。
深拷贝:clone,性能比较低,是在堆的基础上的拷贝。
1
2let s1 = String::from("hello");
let s2 = s1.clone();此例中s1,s2都会拥有”hello”的所有权(但是是两个不同的”hello”)。
Rust 永远也不会自动创建数据的 “深拷贝”。因此,任何自动的复制都不是深拷贝。
浅拷贝:浅拷贝只发生在栈上,因此性能很高。
1
2let x = 5;
let y = x;此例中x和y都会赋值为5.这种可以进行直接浅拷贝的变量类型所拥有的特征叫做Copy特征。
任何基本类型的组合可以 Copy ,不需要分配内存或某种形式资源的类型是可以 Copy 的。
函数的传值与返回
1 | fn main() { |
上述例子中,在 takes_ownership(s); 之后运行 println!(“{}”,s); 将会报错,因为s的”hello”所有权已经转移给some_string。但是i32型的变量是Copy的,所以在 makes_copy(x); 后运行 println!(“{}”,x); 是不会报错的。
1 | fn main() { |
同理,在 main() 函数最后, s1,s3 可以被打印, s2 不可被打印。
引用和借用
引用与解引用
引用(名词):常规引用是一个指针类型,指向了对象存储的内存地址。
解引用(名词):解出引用所指向的值,即引用对象存储的值
借用(动词):获取变量的引用,称之为借用
引用的作用域:从引用创建开始到最后一次使用的位置(旧版 Rust 的引用的作用域等于变量作用域)。对于这种编译器优化行为,Rust 专门起了一个名字 —— Non-Lexical Lifetimes(NLL),专门用于找到某个引用在作用域(})结束前就不再被使用的代码位置。
例子:
1
2
3
4
5
6fn main() {
let x = 5;
let y = &x; // y 为引用
assert_eq!(5, x); // x 对 5 的所有权并没有消失
assert_eq!(5, *y); // *y 为解引用
}
不可变引用与可变引用
不可变引用:变量默认不可变,所以引用指向的值也默认不可变。示例:
1
2
3
4
5
6
7
8
9
10
11fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}这里的 & 就是引用,这样传入引用的函数可以维持 s1 的 “hello” 所有权,不需要复杂地在函数中传入传出所有权。但是不能通过这个引用修改 s1 的值,原因: s1 是不可变变量,引用也不可变。
下为引用示意图:

可变引用:对可变变量的引用。
1
2
3
4
5
6
7
8fn main() {
let mut s = String::from("hello"); // 可变
change(&mut s); // 可变
}
fn change(some_string: &mut String) {
some_string.push_str(", world"); // push_str 是在目标字符串后添加字符串的函数
}这样运行就不会报错了。
可变引用有两个很大限制:
- 同一作用域,特定数据只能有一个可变引用
- 可变引用与不可变引用不能同时存在
悬垂引用
悬垂引用:悬垂引用也叫做悬垂指针,意思为指针指向某个值后,这个值被释放掉了,而指针仍然存在,其指向的内存可能不存在任何值或已被其它变量重新使用。Rust语言不允许悬垂指针的出现。
1 | fn main() { |
复合类型(一):字符串和数组
字符串与切片
切片
对于字符串而言,切片就是对String类型中某一部分的引用,语法如下:
1
2
3
4let s = String::from("hello world");
let hello = &s[0..5]; // 左闭右开
let world = &s[6..11]; // 左闭右开示意图如下:

Rust 语言的
..range序列与 Python 的类似,可以在索引 0 处省略 0 ,索引 len 处省略 len:1
2
3
4
5
6let slice = &s[0..2];
let slice = &s[..2];
let slice = &s[4..len];
let slice = &s[4..];
let slice = &s[0..len];
let slice = &s[..];区间符号:
..:左闭右开。如:切片&s[0..2]代表 “he”..=:左闭右闭,闭区间。如:切片&s[0..=2]代表 “hel”
切片索引必须在字符边界位置,这一点在UTF-8等编码的字符处理时要注意。以UTF-8为例,如果要切片纯UTF-8的中文字符串,必须满足切片的两个索引都为3的倍数(中文在 UTF-8 中占用三个字节)。
字符串切片的类型标识是 &str
因为切片是对集合的部分引用,因此不仅仅字符串有切片,其它集合类型也有,例如数组。
字符串的字面量是切片:
1
2
3let s = "Hello, world!"; // s 的类型是 &str
/* 下面这个语句和上面等效 */
let s: &str = "Hello, world!";s 这个切片指向了程序可执行文件中的某个点,这也是为什么字符串字面量是不可变的,因为 &str 是一个不可变引用。
简单总结下切片的特点:(ps:如果这里没有看懂可以先跳过,等学完“数组——数组切片”后或许能更好地理解)
- 切片的长度在运行时确定,并非固定不变,而是取决于创建时指定的起始和结束位置,提供了灵活的数据访问视图
- 切片本身不拥有底层数据,它只是对原始数据(如数组、String或 &str)的一个“视图”或引用。因此创建切片的代价很小,避免了不必要的数据复制
- 切片类型 [T] 和 str 本身是动态大小类型(DST),其大小在编译时无法确定。而更常用的切片引用类型 &[T]和 &str是胖指针,包含指向数据的指针和长度信息,具有固定的大小(在64位系统上通常为16字节),这使其能够满足Rust对固定大小数据类型的要求,因此更为实用
- Rust的借用检查器会确保切片的生命周期不会超过其引用的底层数据,从而在提供灵活性的同时保证了内存安全
字符串
Rust 中的字符是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是在字符串中不一样,字符串是 UTF-8 编码,也就是字符串中的字符所占的字节数是变化的(1 - 4)
Rust 在语言级别,只有一种字符串类型: str,它通常是以引用类型出现 &str,也就是上文提到的字符串切片。虽然语言级别只有上述的 str 类型,但是在标准库里,还有多种不同用途的字符串类型,其中使用最广的即是 String类型。
str 类型是硬编码进可执行文件,也无法被修改,但是 String 则是一个可增长、可改变且具有所有权的 UTF-8 编码字符串,当 Rust 用户提到字符串时,往往指的就是 String 类型和 &str 字符串切片类型,这两个类型都是 UTF-8 编码。
String 与 &str 的转换:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/* &str -> String */
let s = String::from("hello,world")
let s = "hello,world".to_string() // to_string 将值转换为其字符串表示形式
/* String -> &str */
fn main() {
let s = String::from("hello,world!");
say_hello(&s);
say_hello(&s[..]);
say_hello(s.as_str()); // as_str 获取该字符串的一个不可变切片引用(&str)
}
fn say_hello(s: &str) {
println!("{}",s);
}Rust 不允许使用索引的方式访问字符串的某个字符或者子串。通过索引区间来访问字符串时,需要格外的小心。
字符串操作
由于 String 是可变字符串,下面介绍 Rust 字符串的修改,添加,删除等常用方法:
追加(Push)
1
2
3
4
5
6
7
8
9fn main() {
let mut s = String::from("Hello ");
s.push_str("rust"); //追加字符串字面量
println!("追加字符串 push_str() -> {}", s);
s.push('!'); //追加字符 char
println!("追加字符 push() -> {}", s);
}代码运行结果:
1
2追加字符串 push_str() -> Hello rust
追加字符 push() -> Hello rust!插入(Insert)
1
2
3
4
5
6
7fn main() {
let mut s = String::from("Hello rust!");
s.insert(5, ','); // 插入单个字符 char
println!("插入字符 insert() -> {}", s);
s.insert_str(6, " I like"); // 插入字符串字面量
println!("插入字符串 insert_str() -> {}", s);
} // insert 的第一个参数实质上是字节偏移量,而不是字符序号。代码运行结果:
1
2插入字符 insert() -> Hello, rust!
插入字符串 insert_str() -> Hello, I like rust!替换(Replace)
replace,可适用于 String 和 &str 类型
1
2
3
4
5fn main() {
let string_replace = String::from("I like rust. Learning rust is my favorite!");
let new_string_replace = string_replace.replace("rust", "RUST"); // replace 会返回一个新字符串,而不是直接在原字符串上进行修改。
dbg!(new_string_replace);
}代码运行结果:
1
new_string_replace = "I like RUST. Learning RUST is my favorite!"
replacen,可适用于 String 和 &str 类型
1
2
3
4
5fn main() {
let string_replace = "I like rust. Learning rust is my favorite!";
let new_string_replacen = string_replace.replacen("rust", "RUST", 1); // 第三个参数指的是替换的个数,从头开始数。
dbg!(new_string_replacen);
}代码运行结果:
1
new_string_replacen = "I like RUST. Learning rust is my favorite!"
replace_range,仅适用于 String 类型
1
2
3
4
5fn main() {
let mut string_replace_range = String::from("I like rust!");
string_replace_range.replace_range(7..8, "R"); // 直接操作原来的字符串,不会返回新字符串。第二个参数要是字符串。
dbg!(string_replace_range);
}代码运行结果:
1
string_replace_range = "I like Rust!"
删除(Delete)
下列相关方法仅适用于 String ,因为都是直接操作原来的字符串。
pop —— 删除并返回字符串的最后一个字符
其返回值是一个 Option 类型,为删除掉的字符( Option 类型的特点:如果字符串为空,则返回 None)。
1
2
3
4
5
6
7
8fn main() {
let mut string_pop = String::from("rust pop 中文!");
let p1 = string_pop.pop();
let p2 = string_pop.pop();
dbg!(p1);
dbg!(p2);
dbg!(string_pop);
}代码运行结果:
1
2
3
4
5
6
7
8p1 = Some(
'!',
)
p2 = Some(
'文',
)
string_pop = "rust pop 中"remove —— 删除并返回字符串中指定位置的字符
其返回值为删除位置的字符串。remove() 方法是按照字节来处理字符串的,如果参数所给的位置不是合法的字符边界,则会发生错误。
1
2
3
4
5
6
7
8
9
10
11
12
13
14fn main() {
let mut string_remove = String::from("测试remove方法");
println!(
"string_remove 占 {} 个字节",
std::mem::size_of_val(string_remove.as_str())
);
// 删除第一个汉字
string_remove.remove(0);
// 下面代码会发生错误
// string_remove.remove(1);
// 直接删除第二个汉字
// string_remove.remove(3);
dbg!(string_remove);
}代码运行结果:
1
2string_remove 占 18 个字节
string_remove = "试remove方法"truncate —— 删除字符串中从指定位置开始到结尾的全部字符
无返回值,它同样是按照字节来处理字符串的,如果参数所给的位置不是合法的字符边界,则会发生错误。
1
2
3
4
5fn main() {
let mut string_truncate = String::from("测试truncate");
string_truncate.truncate(3);
dbg!(string_truncate);
}代码运行结果:
1
string_truncate = "测"
clear —— 清空字符串
相当于 truncate() 方法参数为 0 的时候
1
2
3
4
5fn main() {
let mut string_clear = String::from("string clear");
string_clear.clear();
dbg!(string_clear);
}代码运行结果:
1
string_clear = ""
连接(Concatenate)
使用
+或者+=连接字符串使用 + 或者 += 连接字符串,要求右边的参数必须为字符串的切片引用(Slice)类型。其实当调用 + 的操作符时,相当于调用了 std::string 标准库中的 add() 方法,这里 add() 方法的第二个参数是一个引用的类型。因此我们在使用 + 时, 必须传递切片引用类型。不能直接传递 String 类型。+ 是返回一个新的字符串,所以变量声明可以不需要 mut 关键字修饰。
1
2
3
4
5
6
7
8
9
10
11
12fn main() {
let string_append = String::from("hello ");
let string_rust = String::from("rust");
// &string_rust会自动解引用为&str
let result = string_append + &string_rust;
let mut result = result + "!"; // `result + "!"` 中的 `result` 是不可变的
result += "!!!";
println!("连接字符串 + -> {}", result);
// 以下代码去掉注释后会报错,因为 + 实际上是调用了 add() 方法,所有权被转移到 add() 方法里面, add() 方法调用后就被释放了,同时 string_append 也被释放了。再使用 string_append 就会发生错误。
// println!("原字符串: {}", string_append);
}代码运行结果:
1
连接字符串 + -> hello rust!!!!
format! 宏
format! 这种方式适用于 String 和 &str 。format! 的用法与 print! 的用法类似,它会将格式化文本输出到
String字符串。1
2
3
4
5
6fn main() {
let s1 = "hello";
let s2 = String::from("rust");
let s = format!("{} {}!", s1, s2);
println!("{}", s);
}代码运行结果
1
hello rust!
字符串转义
我们可以通过转义的方式 \ 输出 ASCII 和 Unicode 字符。
1 | fn main() { |
当然,在某些情况下,可能你会希望保持字符串的原样,不要转义:
1 | fn main() { |
操作 UTF-8 字符串
如果想要以 Unicode 字符的方式遍历字符串,最好的办法是使用 chars 方法,例如:
1 | for c in "中国人".chars() { |
输出如下:
1 | 中 |
如果想要获取底层字节数组表现形式,例如:
1 | for b in "中国人".bytes() { |
输出如下:
1 | 228 |
如果想获取子串:考虑尝试下这个库:utf8_slice。
数组
数组的定义和分类
数组的三要素:
- 长度固定
- 元素必须有相同的类型
- 依次线性排列
数组的分类:
- 数组:array,速度快但是长度固定,数组属于基本类型(类比&str)
- 动态数组:Vector,可动态增长,但是有性能损耗(类比String)
从上述分类可以看出,array 和 &str 属于基本类型,而 Vector 和 String 属于非基本类型,实际上,Vector 和 String 属于“集合类型”,较为复杂。
本章节重点放在 array 上。
数组的创建
一般的创建:
1 | fn main() { |
声明类型的数组:
1 | fn main() { |
这里的 i32 是元素类型,5 是数组长度。
重复出现某值的数组:
1 | fn main() { |
a 数组包含 5 个元素,这些元素的初始化值为 3(这种语法跟数组类型的声明语法其实是保持一致的)
数组元素的访问
1 | fn main() { |
索引下标是从 0 开始的
Rust语言不允许越界访问数组元素,一旦发生越界访问将会直接出现 panic。
非基础类型的数组元素
如果数组元素是非基本类型的,下列代码不被允许:
1 | let array = [String::from("rust is good!"); 8]; |
原因:重复出现某值的数组的创建实质上是不断 Copy 实现的,而非基础类型不支持 Copy。
这里的 “{:#?}” 是一个功能强大的格式化占位符,它主要用于调试输出,并且会以更美观、易读的格式来展示数据。
正确的写法:
1 | let array = [String::from("rust is good!"),String::from("rust is good!"),String::from("rust is good!")]; |
输出结果为(看看”{:#?}”的功能):
1 | [ |
from_fn 是标准库提供的一个函数,用于通过一个闭包(匿名函数)来动态生成数组的每个元素。它的核心价值在于,它为数组中的每个索引都调用一次这个闭包,从而创建全新的实例。这对于像 String这样没有实现 Copytrait 的类型至关重要,因为它避免了所有权问题。
闭包逻辑:|_i| String::from(“rust is good!”)
- 这是传递给 from_fn 的闭包
- |_i|:闭包接收一个参数 i(类型为 usize),代表当前元素的索引(从 0 到 7)。前面的下划线 _ 是一个约定,告诉 Rust 编译器我们有意不使用这个参数,以避免未使用变量的警告(见前文“变量的绑定与解构——未使用的变量”)
- String::from(“rust is good!”):闭包的主体。它每次被调用时都会执行一次,在堆上创建一个全新的、内容为 “rust is good!”的 String。由于每次调用都会创建一个新的 String,我们最终得到了 8 个独立的、所有权明确的字符串对象。
数组切片
1 | let a: [i32; 5] = [1, 2, 3, 4, 5]; |
上面的数组切片 slice 的类型是&[i32],与之对比,数组的类型是[i32;5]
现在可以再看看切片的特点:(ps:可以回到“复合类型——字符串与切片——切片”看看能否更好地理解切片的特点)
- 切片的长度可以与数组不同,并不是固定的,而是取决于你使用时指定的起始和结束位置
- 创建切片的代价非常小,因为切片只是针对底层数组的一个引用
- 切片类型 [T] 拥有不固定的大小,而切片引用类型 &[T] 则具有固定的大小,因为 Rust 很多时候都需要固定大小数据类型,因此 &[T] 更有用,
&str字符串切片也同理
二维数组
1 | fn main() { |
数组的注意事项
数组虽然很简单,但是其实还是存在几个要注意的点:
- 数组类型容易跟数组切片混淆,[T;n] 描述了一个数组的类型,而 [T] 描述了切片的类型, 因为切片是运行期的数据结构,它的长度无法在编译期得知,因此不能用 [T;n] 的形式去描述
- [u8; 3]和[u8; 4]是不同的类型,数组的长度也是类型的一部分
- 在实际开发中,使用最多的是数组切片[T],我们往往通过引用的方式去使用&[T],因为后者有固定的类型大小
复合类型(二):元组、结构体和枚举
元组
元组
元组是多种类型组合一起形成的复合类型,长度和顺序都固定。
1 | fn main() { |
1 | fn main() { |
这些都是合法的元组创建和绑定。
元组访问与使用
访问某个元组的特定元素,使用.的访问方式:
1 | fn main() { |
元组使用示例:
1 | fn main() { |
结构体
结构体创建
一个结构体由几部分组成:
- 通过关键字 struct 定义
- 一个清晰明确的结构体:名称
- 几个有名字的结构体:字段
例如:
1 | struct User { |
注意:
- 初始化实例时,每个字段都需要进行初始化
- 初始化时的字段顺序不需要和结构体定义时的顺序一致
结构体访问
通过 . 操作符即可访问结构体实例内部的字段值,也可以修改它们
1 | fn main() { |
简化结构体创建
下面的函数类似一个构建函数,返回了 User 结构体的实例,两个函数等效:
1 | /* 1 */ |
结构体更新语法
下面的函数根据已有的结构体实例,创建新的结构体实例,例如根据已有的 user1 实例来构建 user2,则两个语段等效:
1 | /* 1 */ |
结构体更新语法跟赋值语句 = 非常相像,因此在上面代码中,user1 的部分字段所有权被转移到 user2 中:username 字段发生了所有权转移,作为结果,user1 无法再被使用。
(注意,只有username是所有权转移,剩下两个从 user1 传过去的字段是 Copy 过去的)
结构体的内存排列
1 |
|
其中的 #[derive(Debug)] 是让编译器为你定义的类型(例如结构体或枚举)自动实现 Debug 的属性(Attribute)。此时如果想输出结构体,可以使用 {:?}(不换行) 或 {:#?}(会换行)的形式打印出来。当然用宏 dbg! 也可以。
上面定义的 File 结构体在内存中的排列如下图所示:

从图中可以清晰地看出 File 结构体两个字段 name 和 data 分别拥有底层两个 [u8] 数组的所有权(String 类型的底层也是 [u8] 数组),通过 ptr 指针指向底层数组的内存地址,这里你可以把 ptr 指针理解为 Rust 中的引用类型。
该图片也侧面印证了:把结构体中具有所有权的字段转移出去后,将无法再访问该字段,但是可以正常访问其它的字段。
元组结构体
1 | struct Color(i32, i32, i32); |
元组结构体在你希望有一个整体名称,但是又不关心里面字段的名称时将非常有用。
单元结构体
如果你定义一个类型,但是不关心该类型的内容,只关心它的行为时,就可以使用单元结构体:
1 | struct AlwaysEqual; |
枚举
枚举的语法
以扑克牌花色为例:
定义一个枚举类型:
1
2
3
4
5
6enum PokerSuit {
Clubs,
Spades,
Diamonds,
Hearts,
}枚举类型是一个类型(PokerSuit),它会包含所有可能的枚举成员(Clubs、Spades、Diamonds、Hearts),而枚举值是该类型中的具体某个成员的实例。
枚举值:用
::操作符来创建 PokerSuit 枚举类型的两个成员实例,1
2let heart = PokerSuit::Hearts;
let diamond = PokerSuit::Diamonds;定义一个函数使用它们:
1
2
3
4
5
6
7
8
9
10fn main() {
let heart = PokerSuit::Hearts;
let diamond = PokerSuit::Diamonds;
print_suit(heart);
print_suit(diamond);
}
fn print_suit(card: PokerSuit) {
// 需要在定义 enum PokerSuit 的上面添加上 #[derive(Debug)],否则会报 card 没有实现 Debug
println!("{:?}",card);
}print_suit 函数的参数类型是 PokerSuit,因此我们可以把 heart 和 diamond 传给它,虽然 heart 是基于 PokerSuit 下的 Hearts 成员实例化的,但是它是货真价实的 PokerSuit 枚举类型。
其他枚举的用法:
以下两段代码都可以实现扑克牌花色和点数的联动:
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
33/* 1 */
enum PokerSuit {
Clubs,
Spades,
Diamonds,
Hearts,
}
struct PokerCard {
suit: PokerSuit,
value: u8
}
fn main() {
let c1 = PokerCard {
suit: PokerSuit::Clubs,
value: 1,
};
let c2 = PokerCard {
suit: PokerSuit::Diamonds,
value: 12,
};
}
/* 2 */
enum PokerCard { // 直接将数据信息关联到枚举成员上
Clubs(u8),
Spades(u8),
Diamonds(u8),
Hearts(u8),
}
fn main() {
let c1 = PokerCard::Spades(5);
let c2 = PokerCard::Diamonds(13);
}同一枚举类型下的不同成员可以持有不同数据类型:
1
2
3
4
5
6
7
8
9
10enum PokerCard {
Clubs(u8),
Spades(u8),
Diamonds(char),
Hearts(char),
}
fn main() {
let c1 = PokerCard::Spades(5);
let c2 = PokerCard::Diamonds('A');
}甚至可以是字符串、数值、结构体甚至另一个枚举:
1
2
3
4
5
6
7
8
9
10
11enum Message {
Quit, // 没有任何关联数据
Move { x: i32, y: i32 }, // 包含一个匿名结构体
Write(String), // 包含一个 String 字符串
ChangeColor(i32, i32, i32), // 包含三个 i32
}
fn main() {
let m1 = Message::Quit;
let m2 = Message::Move{x:1,y:1};
let m3 = Message::ChangeColor(255,255,0);
}
由于每个结构体都有自己的类型,因此我们无法在需要同一类型的地方进行使用,例如某个函数它的功能是接受消息并进行发送,那么用枚举的方式,就可以接收不同的消息,但是用结构体,该函数无法接受 4 个不同的结构体作为参数。而且从代码规范角度来看,枚举的实现更简洁,代码内聚性更强,不像结构体的实现,分散在各个地方。
同一化类型
我们有一个这样的问题:
- 有两种不同的网络流:
TcpStream和TlsStream<TcpStream> - 我们希望用同一个函数处理这两种流
- 但它们是不同的类型,Rust的强类型系统不允许直接互换
那么我们可以定义一个枚举类型:
1 | enum WebSocket { |
类型名称:WebSocket(实际上是WebSocket枚举)
两个变体:
- Tcp:包装普通的TCP WebSocket连接
- Tls:包装TLS加密的WebSocket连接
利用以下代码处理这两个连接:
1 | fn new (stream: TcpStream) { |
Option 枚举用于处理空值
Rust 抛弃了 null,而改为使用 Option 枚举变量来表述当前时刻变量的值是缺失的这种结果。
Option 枚举包含两个成员,一个成员表示含有值:Some(T), 另一个表示没有值:None,定义如下:
1 | enum Option<T> { |
其中 T 是泛型参数,Some(T)表示该枚举成员的数据类型是 T,换句话说,Some 可以包含任何类型的数据。
Option 枚举以及 Some 和 None 都被包含在了 prelude(prelude 属于 Rust 标准库,Rust 会将最常用的类型、函数等提前引入其中,省得我们再手动引入)之中,不需要将其显式引入作用域,甚至不需要 Option:: 前缀就可直接使用 Some 和 None 。
1 | let some_number = Some(5); |
如果使用 None 而不是 Some,需要告诉 Rust Option
Option的优势:
1 | let x: i8 = 5; // 一定有值,编译器保证不为空 |
编译器强制你显式处理 None 情况:
1 | // match 是模式匹配知识,可以先理解为按顺序检查每个分支的模式,并执行第一个匹配成功的分支对应的代码。 |
为了拥有一个可能为空的值,你必须要显式的将其放入对应类型的 Option<T> 中。接着,当使用这个值时,必须明确的处理值为空的情况。只要一个值不是 Option<T> 类型,你就 可以 安全的认定它的值不为空。这是 Rust 的一个经过深思熟虑的设计决策,来限制空值的泛滥以增加 Rust 代码的安全性。
本文章已完结,后续可能会有部分内容的修改和更新,以保证文章的正确性和逻辑严密性。如需要继续学习Rust基础知识请移步《Rust基础入门(二)》





