Rust学习笔记
Cargo
Cargo 是 Rust 的构建系统和包管理器。因为它可以为你处理很多任务,比如构建代码、下载依赖库并编译这些库
-
查看版本号
cargo --version rustc --version # 查看rust的版本
-
新建项目
cargo new hello_cargo
创建时会初始化一个git仓库, 如果在现有的git仓库中运行cargo new, 则不会生成git文件
-
编译项目
cargo build
这个命令会创建一个可执行文件 target/debug/hello_cargo (在 Windows 上是 target\debug\hello_cargo.exe)
-
运行项目
cargo run
若未编译则该命令也会先编译
-
检查编译
cargo check
该命令快速检查代码确保其可以编译,但并不产生可执行文件, 通常
cargo check
要比cargo build
快得多,因为它省略了生成可执行文件的步骤 -
发布release
cargo build --release
当项目最终准备好发布时,可以使用
cargo build --release
来优化编译项目。这会在 target/release 而不是 target/debug 下生成可执行文件。这些优化可以让 Rust 代码运行的更快,不过启用这些优化也需要消耗更长的编译时间。
变量和可变性
变量默认是不可改变的(immutable), Rust 编译器保证,如果声明一个值不会变,它就真的不会变。
你可以在变量名之前加 mut
来使其可变
声明常量使用 const
关键字而不是 let
,并且 必须 注明值的类型
数据类型
rust有两类数据类型子集:标量(scalar)和复合(compound)
标量
标量(scalar)类型代表一个单独的值。Rust 有四种基本的标量类型:整型、浮点型、布尔类型和字符类型
整型
长度 | 有符号 | 无符号 |
---|---|---|
8-bit | i8 |
u8 |
16-bit | i16 |
u16 |
32-bit | i32 |
u32 |
64-bit | i64 |
u64 |
128-bit | i128 |
u128 |
arch | isize |
usize |
整型溢出
比方说有一个 u8
,它可以存放从零到 255
的值。那么当你将其修改为 256
时就会发生 “整型溢出”(“integer overflow” ),关于这一行为 Rust 有一些有趣的规则
当在 debug 模式编译时,Rust 检查这类问题并使程序 panic
在 release 构建中,Rust 不检测溢出,相反会进行一种被称为二进制补码包装(two’s complement wrapping)的操作。简而言之,值 256
变成 0
,值 257
变成 1
,依此类推。
浮点型
Rust 的浮点数类型是 f32
和 f64
,分别占 32 位和 64 位。默认类型是 f64
,因为在现代 CPU 中,它与 f32
速度几乎一样,不过精度更高。
let x = 2.0; // f64
let y: f32 = 3.0; // f32
布尔型
Rust 中的布尔类型有两个可能的值:true
和 false
。Rust 中的布尔类型使用 bool
表示。
let t = true;
let f: bool = false; // 显式指定类型注解
字符类型
Rust 的 char
类型是语言中最原生的字母类型, 注意 char
由单引号指定,不同于字符串使用双引号。
let c = 'z';
let z = 'ℤ';
let heart_eyed_cat = '😻';
Rust 的 char
类型的大小为四个字节(four bytes),并代表了一个 Unicode 标量值
复合类型
元组类型
元组是一个将多个其他类型的值组合进一个复合类型的主要方式。元组长度固定:一旦声明,其长度不会增大或缩小。
let tup: (i32, f64, u8) = (500, 6.4, 1);
使用模式匹配(pattern matching)来解构(destructure)元组值
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {}", y);
使用点号(.
)后跟值的索引访问元素
let x: (i32, f64, u8) = (500, 6.4, 1);
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
数组类型
元组不同,数组中的每个元素的类型必须相同. Rust 中的数组与一些其他语言中的数组不同,Rust 中的数组是固定长度的:一旦声明,它们的长度不能增长或缩小。
let a = [1, 2, 3, 4, 5];
数组长度固定, 存放在栈(stack)上而不是在堆(heap)上
使用[index]访问元素
let a = [1, 2, 3, 4, 5];
let first = a[0];
let second = a[1];
可以像这样编写数组的类型:在方括号中包含每个元素的类型,后跟分号,再后跟数组元素的数量。
let a: [i32; 5] = [1, 2, 3, 4, 5];
这里,i32
是每个元素的类型。分号之后,数字 5
表明该数组包含五个元素。
你还可以通过在放括号中指定初始值加分号再加元素个数的方式来创建一个每个元素都为相同值的数组:
let a = [3; 5];
变量名为 a
的数组将包含 5
个元素,这些元素的值最初都将被设置为 3
。这种写法与 let a = [3, 3, 3, 3, 3];
效果相同,但更简洁。
函数
Rust 代码中的函数和变量名使用 snake case 规范风格。在 snake case 中,所有字母都是小写并使用下划线分隔单词。
fn main() {
println!("Hello, world!");
let x = another_function(5, 'h');
}
fn another_function(value: i32, unit_label: char) -> i32 {
println!("The measurement is: {}{}", value, unit_label);
2 + 3
}
形参(parameters)/ 实参(arguments)
语句(Statements)是执行一些操作但不返回值的指令, 后面必须跟上分号(😉
表达式(Expressions)计算并产生一个值, 函数会返回最后一个表达式的值
控制流
if else
fn main() {
let number = 6;
if number % 4 == 0 {
println!("number is divisible by 4");
} else if number % 3 == 0 {
println!("number is divisible by 3");
} else if number % 2 == 0 {
println!("number is divisible by 2");
} else {
println!("number is not divisible by 4, 3, or 2");
}
}
在let语句中使用if
因为 if
是一个表达式,我们可以在 let
语句的右侧使用它, 但注意 if
的每个分支的返回值都必须是相同类型
fn main() {
let condition = true;
let number = if condition {
5
} else {
6
};
println!("The value of number is: {}", number);
}
循环
Rust 有三种循环:loop
、while
和 for
。
loop: 一直循环
loop
关键字告诉 Rust 一遍又一遍地执行一段代码直到你明确要求停止。
fn main() {
loop {
println!("again!");
}
}
fn main() {
let mut counter = 0;
let result = loop { // rust接收loop的返回值
counter += 1;
if counter == 10 {
break counter * 2; // counter * 2为loop的返回值
}
};
println!("The result is {}", result); //最后打印出 result 的值,也就是 20
}
while: 条件循环
fn main() {
let mut number = 3;
while number != 0 {
println!("{}!", number);
number = number - 1;
}
println!("LIFTOFF!!!");
}
for: 遍历集合
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a.iter() {
println!("the value is: {}", element);
}
}
fn main() {
for number in (1..4).rev() {
println!("{}!", number);
}
println!("LIFTOFF!!!");
}
所有权
所有运行的程序都必须管理其使用计算机内存的方式。一些语言中具有垃圾回收机制,在程序运行时不断地寻找不再使用的内存;在另一些语言中,程序员必须亲自分配和释放内存。Rust 则选择了第三种方式:通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。在运行时,所有权系统的任何功能都不会减慢程序。
栈(Stack)与堆(Heap)
栈中的所有数据都必须占用已知且固定的大小
堆上存的是编译时大小未知或大小可能变化的数据
堆是缺乏组织的:当向堆放入数据时,你要请求一定大小的空间。内存分配器(memory allocator)在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的 指针(pointer)。这个过程称作 在堆上分配内存(allocating on the heap),有时简称为 “分配”(allocating)
入栈比在堆上分配内存要快,因为(入栈时)分配器无需为存储新数据去搜索内存空间;其位置总是在栈顶。相比之下,在堆上分配内存则需要更多的工作,这是因为分配器必须首先找到一块足够存放数据的内存空间,并接着做一些记录为下一次分配做准备。
访问堆上的数据比访问栈上的数据慢,因为必须通过指针来访问。处理器在处理的数据彼此较近的时候(比如在栈上)比较远的时候(比如可能在堆上)能更好的工作。在堆上分配大量的空间也可能消耗时间。
当你的代码调用一个函数时,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。
跟踪哪部分代码正在使用堆上的哪些数据,最大限度的减少堆上的重复数据的数量,以及清理堆上不再使用的数据确保不会耗尽空间,这些问题正是所有权系统要处理的。
所有权规则
- Rust 中的每一个值都有一个被称为其 所有者(owner)的变量。
- 值在任一时刻有且只有一个所有者。
- 当所有者(变量)离开作用域,这个值将被丢弃。
变量与数据交互方式一: 移动
分配在栈上的内存:
let x = 4;
let y = x;
println!("{}, {}", x, y); // 该行可以正常打印
分配在堆上的内存:
let s1 = String::from("hello");
let s2 = s1;
println!("{}, {}", s1, s2); // 该行会报错, 因为s1已经失效了
若s1没有失效的话, 那么s1和s2都指向了同一块内存地址, 当s1和s2都离开作用域时, 他们都会尝试释放相同的内存。这是一个叫做 二次释放(double free)的错误,也是之前提到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。
这种操作叫做移动而不是浅拷贝
变量与数据交付方式二: 克隆
当确实需要深度复制堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 clone
的通用函数。
let s3 = String::from("hello");
let s4 = s3.clone(); // 拷贝
println!("{}, {}", s3, s4); // 该行可以正常打印
只在栈上的数据: 拷贝
前面整型的例子let y = x;
中没有使用clone()
方法, 但是x
依然有效且没有被移动到 y
中。
原因是类似整型/布尔类型/浮点数类型/字符类型这样的存储在栈上的类型, 都默认实现了Copy
这个trait, 类比在python中, 可以理解为都实现了__copy__
这个魔法方法. 赋值时会自动复制一份, 而不是移动
如果一个类型实现了 Copy
trait,那么一个旧的变量在将其赋值给其他变量后仍然可用。
但Rust 不允许自身或其任何部分实现了 Drop
trait 的类型使用 Copy
trait。
所有权与函数
将值传递给函数在语义上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。
fn main() {
let s = String::from("hello"); // s 进入作用域
takes_ownership(s); // s 的值移动到函数里 ...
// ... 所以到这里不再有效
let x = 5; // x 进入作用域
makes_copy(x); // x 应该移动函数里,
// 但 i32 是 Copy 的,所以在后面可继续使用 x
} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
// 所以不会有特殊操作
fn takes_ownership(some_string: String) { // some_string 进入作用域
println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放
fn makes_copy(some_integer: i32) { // some_integer 进入作用域
println!("{}", some_integer);
} // 这里,some_integer 移出作用域。不会有特殊操作
返回值与作用域
返回值也可以转移所有权。
fn main() {
let s1 = gives_ownership(); // gives_ownership 将返回值
// 移给 s1
let s2 = String::from("hello"); // s2 进入作用域
let s3 = takes_and_gives_back(s2); // s2 被移动到
// takes_and_gives_back 中,
// 它也将返回值移给 s3
} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
// 所以什么也不会发生。s1 移出作用域并被丢弃
fn gives_ownership() -> String { // gives_ownership 将返回值移动给
// 调用它的函数
let some_string = String::from("hello"); // some_string 进入作用域.
some_string // 返回 some_string 并移出给调用的函数
}
// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
a_string // 返回 a_string 并移出给调用的函数
}
变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 drop
被清理掉,除非数据被移动为另一个变量所有。
引用和借用
引用(references)可以理解为获取对象的指针的动作, 允许你使用值但不获取其所有权.
借用(borrowing)可以理解为获取引用作为函数参数的动作.
引用也可以是可变或不可变的. 但需要遵循下面的规则:
1. 同一数据在同一作用域内不能同时存在两个及以上的可变引用, 防止数据竞争问题
2. 同一数据在同一作用域内不能同时存在可变引用和不可变引用, 防止不可变应用使用时数据发生变化
3. 同一数据在同一作用域内可以同时存在多个不可变引用
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // 该行代码将报错, 因为已经存在一个可变应用r1了, 在r1为失效前, 不可再创建另一个可变引用, 防止数据竞争
let r3 = &s; // 该行代码将报错, 因为已经存在一个可变引用r1了, 不能同时存在另一个不可变引用
println!("{}", r1);
悬垂引用 (dangling reference), 可以理解空指针, 即一个指针什么都没有指向. 这种情况在rust中不会存在
let x = dangling(); // dangling方法编译会报错, 因为返回了一个空指针, 这是不会编译通过的
fn dangling() -> &String {
let s = String::from("hello"); // 定义一个新字符串
&s // 返回该字符串的引用, 注意:s是该方法新生成的, 当该方法执行完毕后, s被销毁, 那么s的引用(&s)将是无效的
}
Slice类型
slice是一个没有所有权的数据类型, slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。
字符串slice
let s = String::from("hello world");
let hello = &s[0..5]; // 左闭右开
let world = &s[6..11];
let slice1 = &s[0..2];
let slice1 = &s[..2]; // 如果想要从索引 0 开始,可以不写两个点号之前的值
let len = s.len(); // 获取s的长度
let slice2 = &s[3..len]; // 截取3至后面所有字符
let slice2 = &s[3..]; // 如果 slice 包含 String 的最后一个字节,也可以舍弃尾部的数字。
let slice3 = &s[..]; // 也可以同时舍弃这两个值来获取整个字符串的 slice
“字符串 slice” 的类型声明写作 &str
字符串字面值就是 slice
let s = "Hello, world!";
这里 s
的类型是 &str
:它是一个指向二进制程序特定位置的 slice。这也就是为什么字符串字面值是不可变的;&str
是一个不可变引用。
字符串 slice 作为参数
在定义函数时, 可以使用&String
类型作为参数
fn first_word(s: &String) -> &str {
而更有经验的 Rustacean 会使用&Str
作为参数
fn first_word(s: &str) -> &str {
定义一个获取字符串slice
而不是 String
引用的函数使得我们的 API 更加通用并且不会丢失任何功能: 如果有一个字符串 slice,可以直接传递它。如果有一个 String
,则可以传递整个 String
的 slice 或对 String
的引用。
数组类型slice
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]); // 断言成功
slice转为字符串
以下四种方法都可以转为字符串
String::from("hello")
"hello".into()
"hello".to_string()
"hello".to_owned()
结构体(struct)
和元组一样,结构体的每一部分可以是不同类型。但不同于元组,结构体需要命名各部分数据以便能清楚的表明其值的意义。
由于有了这些名字,结构体比元组更灵活:不需要依赖顺序来指定或访问实例中的值。
// 定义结构体
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
// 实例化结构体
// 一个可变的结构体实例
let mut user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
// 改变实例的email属性值
user1.email = String::from("anotheremail@example.com");
注意整个实例必须是可变的;Rust 并不允许只将某个字段标记为可变。
变量与字段同名时的字段初始化简写语法
fn build_user(email: String, username: String) -> User {
// email/username, 结构体中的名字和当前方法中的变量名一致, 那么就可以简写
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
使用结构体更新语法从其他实例创建实例
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};
如上面例子, 在创建user2时, active/username/sign_in_count这三个值和user1的值一样, 那么可以简写为:
let user2 = User{
email: String::from("another@example.com"),
..user1
}
即: 使用结构体更新语法为一个 User
实例设置一个新的 email
值,不过其余值来自 user1
变量中实例的字段
注意: 结构更新语法就像带有 =
的赋值,因为它移动了数据
在这个例子中,我们在创建 user2
后不能再使用 user1
,因为 user1
的 username
字段中的 String
被移到 user2
中。如果我们给 user2
的 email
和 username
都赋予新的 String
值,从而只使用 user1
的 active
和 sign_in_count
值,那么 user1
在创建 user2
后仍然有效。active
和 sign_in_count
的类型是实现 Copy
trait 的类型.
元祖结构体
元组结构体有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型。
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
注意 black
和 origin
值的类型不同,因为它们是不同的元组结构体的实例。你定义的每一个结构体有其自己的类型,即使结构体中的字段有着相同的类型。
结构体方法
// 结构体方法
// 创建长方形结构体
struct Rectangle {
width: u32,
height: u32,
}
// 定义Rectangle的方法
// 第一个参数的self的方法理解为实例方法
// 不带self的方法理解为静态方法, 一般用作构造函数
impl Rectangle {
// 求面积(实例方法)
fn area(&self) -> u32 {
self.width * self.height
} // 后面不需要分号
// 求周长(实例方法)
fn perimeter(&self) -> u32 {
(self.width + self.height) * 2
} // 后面不需要分号
// 构造函数
fn square(size: u32) -> Rectangle {
Rectangle{width: size, height: size}
}
}
// 实例化
let rect1 = Rectangle {width: 3, height: 4};
// 调用方法
println!("{}, {}", rect1.area(), rect1.perimeter());
// 使用构造函数, 通过::调用
let rect2 = Rectangle::square(5);
println!("{}, {}", rect2.area(), rect2.perimeter());
在一个 impl
块中,Self
类型是 impl
块的类型的别名。方法的第一个参数必须有一个名为 self
的Self
类型的参数,所以 Rust 让你在第一个参数位置上只用 self
这个名字来缩写。
注意,我们仍然需要在 self
前面使用 &
来表示这个方法借用了 Self
实例。方法可以选择获得 self
的所有权,或者像我们这里一样不可变地借用 self
,或者可变地借用 self
,就跟其他参数一样。
关联函数
所有在 impl
块中定义的函数被称为 关联函数(associated functions),因为它们与 impl
后面命名的类型相关。我们可以定义不以 self
为第一参数的关联函数(因此不是方法),因为它们并不作用于一个结构体的实例。我们已经使用了一个这样的函数,String::from
函数,它是在 String
类型上定义的。
不是方法的关联函数经常被用作返回一个结构体新实例的构造函数
枚举
通过在代码中定义一个 IpAddrKind
枚举来表现这个概念并列出可能的 IP 地址类型,V4
和 V6
。
enum IpAddrKind {
V4,
V6,
}
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
定义枚举值的类型
enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
每个成员可以处理不同类型和数量的数据
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
也可以使用impl
在枚举上定义方法
// 枚举中实现方法
impl IpAddr {
fn call(&self){
}
}
let four = IpAddr::V4(127, 0, 0, 1);
let six = IpAddr::V6(String::from("::1"));
four.call();
six.call();
Option枚举
空值的问题在于当你尝试像一个非空值那样使用一个空值,会出现某种形式的错误。因为空和非空的属性无处不在,非常容易出现这类错误。
但空值尝试表达的概念仍然是有意义的:空值是一个因为某种原因目前无效或缺失的值。
问题不在于概念而在于具体的实现。为此,Rust 并没有空值,不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是 Option
,而且它定义于标准库中,如下:
enum Option<T> {
Some(T),
None,
}
<T>
意味着 Option
枚举的 Some
成员可以包含任意类型的数据
match 控制流运算符
当 match
表达式执行时,它将结果值按顺序与每一个分支的模式相比较。如果模式匹配了这个值,这个模式相关联的代码将被执行。
每个分支相关联的代码是一个表达式,而表达式的结果值将作为整个 match
表达式的返回值。
如果分支代码较短的话通常不使用大括号
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
},
Coin::Nickel => 5, // 如果分支代码较短的话通常不使用大括号
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
匹配 Option<T>
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1), // i 绑定了 Some 中包含的值
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
match分支需要是穷尽的, 可以使用通配符_
来代替分支
let some_u8_value = 0u8;
match some_u8_value {
1 => println!("one"),
3 => println!("three"),
5 => println!("five"),
7 => println!("seven"),
_ => (),
}
_
模式会匹配所有的值。通过将其放置于其他分支之后,_
将会匹配所有之前没有指定的可能的值。()
就是 unit 值,所以 _
的情况什么也不会发生。因此,可以说我们想要对 _
通配符之前没有列出的所有可能的值不做任何处理。
常见集合
不同于内建的数组和元组类型,这些集合指向的数据是储存在堆上的,这意味着数据的数量不必在编译时就已知,并且还可以随着程序的运行增长或缩小。
vector
它在内存中彼此相邻地排列所有的值。vector 只能储存相同类型的值。
// 创建
let v1: Vec<i32> = Vec::new(); // 空的vector, 可以指定存储值的类型
let v2 = vec![1, 2, 3]; // 包含初值的 vector
// 创建可变的vector
let mut v = Vec::new();
// 添加元素, 末尾追加
v.push(5);
v.push(6);
v.push(7);
// 移除元素, 末尾移除
v.pop();
// 获取元素 [index]
let n: &i32 = &v[2];
// 获取元素 get
match v.get(2) {
Some(m) => println!("The third element is {}", m),
None => println!("There is no third element."),
}
// 遍历
for i in &v{
println!("{}", i);
}
使用枚举存储多种类型
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
字符串String
Rust 的核心语言中只有一种字符串类型:str
,字符串 slice,它通常以被借用的形式出现,&str
。
称作 String
的类型是由标准库提供的,而没有写进核心语言部分,它是可增长的、可变的、有所有权的、UTF-8 编码的字符串类型。
当 Rustacean 们谈到 Rust 的 “字符串”时,它们通常指的是 String
和字符串 slice &str
类型,而不仅仅是其中之一。
// 创建一个空的 String
let mut s = String::new();
// 使用 to_string 方法从字符串字面值创建 String
let mut s1 = "hello".to_string();
// String::from和to_string效果相同
let mut s1 = String::from("hello");
let s2 = "world";
// push_str 追加字符串 slice, 传入的是引用, 不会获取s2所有权
s1.push_str(s2);
println!("{}", s2); // 仍然可以打印s2
// push 追加单个字符
s1.push('a');
+号拼接两个字符串String
let s1 = String::from("hello");
let s2 = String::from(" world");
let s3 = s1 + &s2;
+
运算符使用了 add
函数,这个函数签名看起来像这样:
fn add(self, s: &str) -> String {
首先,s2
使用了 &
,意味着我们使用第二个字符串的 引用 与第一个字符串相加。这是因为 add
函数的 s
参数:只能将 &str
和 String
相加,不能将两个 String
值相加。不过等一下 —— 正如 add
的第二个参数所指定的,&s2
的类型是 &String
而不是 &str
。那么为什么还能通过编译呢?
之所以能够在 add
调用中使用 &s2
是因为 &String
可以被 强转(coerced)成 &str
。当add
函数被调用时,Rust 使用了一个被称为 Deref 强制转换(deref coercion)的技术,你可以将其理解为它把 &s2
变成了 &s2[..]
。
其次,可以发现签名中 add
获取了 self
的所有权,因为 self
没有 使用 &
。这意味着 s1
的所有权将被移动到 add
调用中,之后就不再有效。所以虽然 let s3 = s1 + &s2;
看起来就像它会复制两个字符串并创建一个新的字符串,而实际上这个语句会获取 s1
的所有权,附加上从 s2
中拷贝的内容,并返回结果的所有权。
多个字符串拼接format!
let s3 = String::from("hello");
let s4 = String::from("world");
let s6 = String::from("!");
let s7 = format!("{}-{}-{}", s3, s4, s6);
println!("{}", s7);
字符串无法通过索引下标获取单个字符
let s1 = String::from("hello");
let h = s1[0];
尝试使用索引语法访问 String
的一部分,会出现一个错误. Rust 的字符串不支持索引。
原因是在String的UTF-8编码中, 不同语言的字符所占用的字节不一样
let len1 = String::from("Hola").len(); // 4个字符, len1为4个字节, 1个字符占1个字节
let len2 = String::from("Здравствуйте").len(); // 12个字符, len2为24个字节, 1个字符占2个字节
let len3 = String::from("你好").len(); // 2个字符, len3为6个字节, 1个字符占3个字节
通过索引[0]访问时, 访问的是第一个字节的值, 而对于上面例子中的后两个语言而言, 无法返回第一个字节的值, 因此Rust拒绝通过索引访问String
字节、标量值和字形簇
从 Rust 的角度来讲,事实上有三种相关方式可以理解字符串:字节、标量值和字形簇(最接近人们眼中 字母 的概念)。
比如这个用梵文书写的印度语单词 “नमस्ते”,最终它储存在 vector 中的 u8
值看起来像这样:
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164, 224, 165, 135]
这里有 18 个字节,也就是计算机最终会储存的数据。如果从 Unicode 标量值的角度理解它们,也就像 Rust 的 char
类型那样,这些字节看起来像这样:
['न', 'म', 'स', '्', 'त', 'े']
这里有六个 char
,不过第四个和第六个都不是字母,它们是发音符号本身并没有任何意义。最后,如果以字形簇的角度理解,就会得到人们所说的构成这个单词的四个字母:
["न", "म", "स्", "ते"]
Rust 提供了多种不同的方式来解释计算机储存的原始字符串数据,这样程序就可以选择它需要的表现方式,而无所谓是何种人类语言。
最后一个 Rust 不允许使用索引获取 String
字符的原因是,索引操作预期总是需要常数时间 (O(1))。但是对于 String
不可能保证这样的性能,因为 Rust 必须从开头到索引位置遍历来确定有多少有效的字符。
遍历字符串
chars()返回Unicode 标量值
for c in "नमस्ते".chars() {
println!("{}", c);
}
这些代码会打印出如下内容:
न
म
स
्
त
े
bytes()返回每个原始字节
for b in "नमस्ते".bytes() {
println!("{}", b);
}
这些代码会打印出组成 String
的 18 个字节:
224
164
// --snip--
165
135
不过请记住有效的 Unicode 标量值可能会由不止一个字节组成。
从字符串中获取字形簇是很复杂的,所以标准库并没有提供这个功能。crates.io 上有些提供这样功能的 crate。
哈希 map 储存键值对
创建哈希map
// 引入
use std::collections::HashMap;
// 创建
let mut scores = HashMap::new();
// 添加
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
类似于 vector,哈希 map 是同质的:所有的键必须是相同类型,值也必须都是相同类型。
另一个构建哈希 map 的方法是使用一个元组的 vector 的 collect
方法, 其中每个元组包含一个键值对。collect
方法可以将数据收集进一系列的集合类型,包括 HashMap
。
用队伍列表和分数列表创建哈希 map:
let teams = vec![String::from("Blue"), String::from("Yellow")];
let init_scores = vec![10, 50];
let scores: HashMap<_, _> = teams.iter().zip(init_scores.iter()).collect();
这里 HashMap<_, _>
类型注解是必要的,因为可能 collect
为很多不同的数据结构,而除非显式指定否则 Rust 无从得知你需要的类型。但是对于键和值的类型参数来说,可以使用下划线占位,而 Rust 能够根据 vector 中数据的类型推断出 HashMap
所包含的类型。
访问哈希 map 中的值
可以通过 get
方法并提供对应的键来从哈希 map 中获取值
use std::collections::HashMap;
let mut scores = HashMap::new();
let key1 = String::from("Favorite color");
let key2 = String::from("Blue");
scores.insert(key1, 10);
scores.insert(key2, 50);
// 上面key1和key2不再有效
let team_name = String::from("Blue");
// get获取
let score = scores.get(&team_name);
这里,score
是与蓝队分数相关的值,应为 Some(10)
。因为 get
返回 Option
,所以结果被装进 Some
;如果某个键在哈希 map 中没有对应的值,get
会返回 None
。
循环哈希 map
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
for (key, value) in &scores {
println!("{}: {}", key, value);
}
这会以任意顺序打印出每一个键值对:
Yellow: 50
Blue: 10
更新哈希 map
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
// 更新-直接覆盖value
scores.insert(String::from("Blue"), 20);
// 更新-只在键没有对应值时插入
scores.entry(String::from("Blue")).or_insert(30); // 不会更新Blue对应的value
scores.entry(String::from("Red")).or_insert(30); // 不存在Red建, 则插入键值对
// 根据旧值更新一个值
let count = scores.entry(String::from("Red")).or_insert(100); // 存在Red键, count值为30
// or_insert 方法事实上会返回这个键的值的一个可变引用(&mut V)。
// 这里我们将这个可变引用储存在 count 变量中,所以为了赋值必须首先使用星号(*)解引用 count。
*count += 1;
// 此时Red对应的value为31
for (key, value) in scores.iter() {
println!("{}-{}", key, value);
}
错误处理
Rust 将错误组合成两个主要类别:可恢复错误(recoverable)和 不可恢复错误(unrecoverable)。
可恢复错误通常代表向用户报告错误和重试操作是合理的情况,比如未找到文件。
不可恢复错误通常是 bug 的同义词,比如尝试访问超过数组结尾的位置。
panic! 与不可恢复的错误
fn main() {
panic!("crash and burn");
}
运行程序将会出现类似这样的输出:
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished dev [unoptimized + debuginfo] target(s) in 0.25s
Running `target/debug/panic`
thread 'main' panicked at 'crash and burn', src/main.rs:2:5
note: Run with `RUST_BACKTRACE=1` for a backtrace.
可以设置 RUST_BACKTRACE
环境变量来得到一个 backtrace。backtrace 是一个执行到目前位置所有被调用的函数的列表。
RUST_BACKTRACE=1 cargo run
Result 与可恢复的错误
match匹配Result
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => {
panic!("Problem opening the file: {:?}", error)
},
};
}
当 File::open
成功的情况下,变量 f
的值将会是一个包含文件句柄的 Ok
实例。
这里我们告诉 Rust 当结果是 Ok
时,返回 Ok
成员中的 file
值(句柄),然后将这个文件句柄赋值给变量 f
。
match
的另一个分支处理从 File::open
得到 Err
值的情况。在这种情况下,我们选择调用 panic!
宏。
匹配不同的错误
上面例子中不管 File::open
是因为什么原因失败都会 panic!
。
我们真正希望的是对不同的错误原因采取不同的行为:如果 File::open
因为文件不存在而失败,我们希望创建这个文件并返回新文件的句柄。如果 File::open
因为任何其他原因失败,例如没有打开文件的权限,那么就panic!
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),
},
};
}
File::open
返回的 Err
成员中的值类型 io::Error
,它是一个标准库中提供的结构体。这个结构体有一个返回 io::ErrorKind
值的 kind
方法可供调用
io::ErrorKind
是一个标准库提供的枚举,它的成员对应 io
操作可能导致的不同错误类型。我们感兴趣的成员是 ErrorKind::NotFound
,它代表尝试打开的文件并不存在。这样,match
就匹配完 f
了,不过对于 error.kind()
还有一个内层 match
。
失败时 panic 的简写:unwrap 和 expect
如果 Result
值是成员 Ok
,unwrap
会返回 Ok
中的值。如果 Result
是成员 Err
,unwrap
会为我们调用 panic!
。
use std::fs::File;
fn main() {
let f = File::open("hello.txt").unwrap();
}
但unwrap
只会panic!
, 而不能自定义错误信息, 使用expect
可以定义错误信息
use std::fs::File;
fn main() {
let f = File::open("hello.txt").expect("Failed to open hello.txt");
}
传播(抛出)错误
将错误return
即可传播错误
use std::io;
use std::io::Read;
use std::fs::File;
fn read_username_from_file() -> Result<String, io::Error> {
let f = File::open("hello.txt");
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e), // 若出现错误, 则直接return出去
};
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
传播错误的简写:? 运算符
use std::io;
use std::io::Read;
use std::fs::File;
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?; // Ok则继续, 否则return Err
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
Result
值之后的 ?
被定义为与上面例子中定义的处理 Result
值的 match
表达式有着完全相同的工作方式。如果 Result
的值是 Ok
,这个表达式将会返回 Ok
中的值而程序将继续执行。如果值是 Err
,Err
中的值将作为整个函数的返回值,就好像使用了 return
关键字一样,这样错误值就被传播给了调用者。
可以在 ?
之后直接使用链式方法调用来进一步缩短代码:
use std::io;
use std::io::Read;
use std::fs::File;
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)
}
? 运算符只能被用于返回 Result 的函数
直接在main
函数(无返回值)中使用?
时, 会报错:
use std::fs::File;
fn main() {
let f = File::open("hello.txt")?;
}
会得到如下错误信息:
error[E0277]: the `?` operator can only be used in a function that returns
`Result` or `Option` (or another type that implements `std::ops::Try`)
--> src/main.rs:4:13
|
4 | let f = File::open("hello.txt")?;
| ^^^^^^^^^^^^^^^^^^^^^^^^ cannot use the `?` operator in a
function that returns `()`
|
= help: the trait `std::ops::Try` is not implemented for `()`
= note: required by `std::ops::Try::from_error`
泛型
在函数中使用泛型
fn largest<T>(list: &[T]) -> T { // 泛型类型参数声明位于函数名称与参数列表中间的尖括号 <> 中
let mut largest = list[0];
for &item in list.iter() {
if item > largest { // 目前编译会报错 binary operation `>` cannot be applied to type `T`
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {}", result);
}
泛型类型参数声明位于函数名称与参数列表中间的尖括号 <> 中, 可以这样理解这个定义:函数 largest
有泛型类型 T
。它有个参数 list
,其类型是元素为 T
的 slice。largest
函数的返回值类型也是 T
。
结构体中使用泛型
struct Point<T> {
x: T,
y: T,
} // 创建结构体时x和y必须相同类型
struct Point2<T> {
x: T,
y: U,
} // 创建结构体时x和y可以是相同类型也可以是不同类型
fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
let wont_work = Point { x: 5, y: 4.0 }; // 该行会报错, 因为传入的x和y类型不一致
//
let integer = Point2 { x: 5, y: 10 };
let float = Point2 { x: 1.0, y: 4.0 };
let wont_work = Point2 { x: 5, y: 4.0 }; // 该行不会报错, 因为定义中的x和y类型不一致
}
结构体方法中使用泛型
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());
}
注意必须在 impl
后面声明 T
,这样就可以在 Point
上实现的方法中使用它了。在 impl
之后声明泛型 T
,这样 Rust 就知道 Point
的尖括号中的类型是泛型而不是具体类型。
结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型。
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);
}
枚举中定义泛型
// 标准库提供的枚举
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
泛型的性能
Rust 通过在编译时进行泛型代码的 单态化(monomorphization)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。使得使用泛型类型参数的代码相比使用具体类型并没有任何速度上的损失。
Trait(定义共享的行为)
trait 类似于其他语言中的常被称为 接口(interfaces)的功能,虽然有一些不同。
定义Trait
定义trait
pub trait Summary {
fn summarize(&self) -> String; // 只定义方法
fn summarize2(&self) -> String {
// 定义默认实现
format!("this is default")
}
fn summarize3(&self) -> String {
// 在默认实现中也可以调用其他的方法, 哪怕这些方法没有默认实现。
self.summarize()
}
}
使用trait
// 定义struct
struct Tweet {
title: String,
username: String,
}
// struct实现trait
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}:{}", self.title, self.username)
}
}
// 创建struct实例
let tweet1 = Tweet {
title: String::from("haha"),
username: String::from("lihua"),
};
println!("{}", tweet1.summarize());
println!("{}", tweet1.summarize2()); // Tweet没有实现Summary的summarize2方法, 但是也可以直接调用
println!("{}", tweet1.summarize3()); // Tweet没有实现Summary的summarize2方法, 但是也可以直接调用
trait作为参数
fn notify(item: impl Summary) {
println!("{}", item.summarize());
}
notify(tweet1);
对于 item
参数,我们指定了 impl
关键字和 trait 名称,而不是具体的类型。该参数支持任何实现了指定 trait 的类型。在 notify
函数体中,可以调用任何来自 Summary
trait 的方法,比如 summarize
。我们可以传递任何 NewsArticle
或 Tweet
的实例来调用 notify
。任何用其它如 String
或 i32
的类型调用该函数的代码都不能编译,因为它们没有实现 Summary
。
trait bound
fn notify<T: Summary>(item: T) {
println!("{}", item.summarize());
}
notify(tweet1);
trait bound 与泛型参数声明在一起,位于尖括号中的冒号后面。
impl Trait
很方便,适用于短小的例子。trait bound 则适用于更复杂的场景。
例如,可以获取两个实现了 Summary
的参数。使用 impl Trait
的语法看起来像这样:
pub fn notify(item1: impl Summary, item2: impl Summary) {
使用Trait Bound则可以写成这样:
pub fn notify<T: Summary>nitify(item1: T, item2: T){
泛型 T
被指定为 item1
和 item2
的参数限制,如此传递给参数 item1
和 item2
值的具体类型必须一致
通过+号指定多个trait bound
如果 notify
需要显示 item
的格式化形式,同时也要使用 summarize
方法,那么 item
就需要同时实现两个不同的 trait:Display
和 Summary
。这可以通过 +
语法实现:
pub fn notify(item: impl Summary + Display) {
+
语法也适用于泛型的 trait bound:
pub fn notify<T: Summary + Display>(item: T) {
通过 where 简化 trait bound
如果每个泛型都有自己的Trait bound, 如泛型T
需要满足Display + Clone
, 泛型U
需要满足Clone + Debug
, 那么写起来就会像这样, 使得泛型的定义<>
中写的很长, 影响阅读
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {
可以通过where
语句优化定义的写法
fn some_function<T, U>(t: T, u: U) -> i32
where T: Display + Clone,
U: Clone + Debug
{
这个函数签名就显得不那么杂乱,函数名、参数列表和返回值类型都离得很近,看起来跟没有那么多 trait bounds 的函数很像。
返回实现了 trait 的类型
也可以在返回值中使用 impl Trait
语法,来返回实现了某个 trait 的类型:
fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
}
}
不过这只适用于返回单一类型的情况。例如,这段代码的返回值类型指定为返回 impl Summary
,但是返回了 NewsArticle
或 Tweet
就行不通, 尽管NewsArticle
和Tweet
都实现了Summary
这个trait, 但是他们还是两种不同的类型
fn returns_summarizable(switch: bool) -> impl Summary {
if switch {
NewsArticle {
headline: String::from("Penguins win the Stanley Cup Championship!"),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from("The Pittsburgh Penguins once again are the best
hockey team in the NHL."),
}
} else {
Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
}
}
}
生命周期
函数中的泛型生命周期
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
我们编写了一个判断两个字符串 slice 中较长者的函数longest
, 但是longest
函数它并不能通过编译, 报错为:
error[E0106]: missing lifetime specifier
--> src/main.rs:1:33
|
1 | fn longest(x: &str, y: &str) -> &str {
| ^ expected lifetime parameter
|
= help: this function's return type contains a borrowed value, but the
signature does not say whether it is borrowed from `x` or `y`
因为 Rust 并不知道将要返回的引用是指向 x
或 y
, 那么也就不能检查出改引用是否是有效的。
为了修复这个错误,我们将增加泛型生命周期参数来定义引用间的关系以便借用检查器可以进行分析。
生命周期注解语法
生命周期注解并不改变任何引用的生命周期的长短。只是类似于泛型注解能够接收任何类型的参数一样, 使用了生命周期注解, 就可以接收任何生命周期的引用.
生命周期注解有着一个不太常见的语法:生命周期参数名称必须以撇号('
)开头,其名称通常全是小写,类似于泛型其名称非常短。'a
是大多数人默认使用的名称。生命周期参数注解位于引用的 &
之后,并有一个空格来将引用类型与生命周期注解分隔开。
&i32 // 引用
&'a i32 // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用
单个的生命周期注解本身没有多少意义,因为生命周期注解告诉 Rust 多个引用的泛型生命周期参数如何相互联系的。
函数签名中的生命周期注解
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
现在函数签名表明对于某些生命周期 'a
,函数会获取两个参数,他们都是与生命周期 'a
存在的一样长的字符串 slice。函数会返回一个同样也与生命周期 'a
存在的一样长的字符串 slice。它的实际含义是 longest
函数返回的引用的生命周期与传入该函数的引用的生命周期的较小者一致。这就是我们告诉 Rust 需要其保证的约束条件。记住通过在函数签名中指定生命周期参数时,我们并没有改变任何传入值或返回值的生命周期,而是指出任何不满足这个约束条件的值都将被借用检查器拒绝。注意 longest
函数并不需要知道 x
和 y
具体会存在多久,而只需要知道有某个可以被 'a
替代的作用域将会满足这个签名。
让我们看看如何通过传递拥有不同具体生命周期的引用来限制 longest
函数的使用。
fn main() {
let string1 = String::from("long string is long");
{
let string2 = String::from("xyz");
let result = longest(string1.as_str(), string2.as_str());
println!("The longest string is {}", result);
}
}
在上面这个例子中,string1
直到外部作用域结束都是有效的,string2
则在内部作用域中是有效的,而 result
则引用了一些直到内部作用域结束都是有效的值。借用检查器认可这些代码;它能够编译和运行,并打印出 The longest string is long string is long
。
接下来,让我们尝试另外一个例子,该例子揭示了 result
的引用的生命周期必须是两个参数中较短的那个。以下代码将 result
变量的声明移动出内部作用域,但是将 result
和 string2
变量的赋值语句一同留在内部作用域中。接着,使用了变量 result
的 println!
也被移动到内部作用域之外。
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);
}
如果尝试编译会出现如下错误:
error[E0597]: `string2` does not live long enough
--> src/main.rs:6:44
|
6 | result = longest(string1.as_str(), string2.as_str());
| ^^^^^^^ borrowed value does not live long enough
7 | }
| - `string2` dropped here while still borrowed
8 | println!("The longest string is {}", result);
| ------ borrow later used here
错误表明为了保证 println!
中的 result
是有效的,string2
需要直到外部作用域结束都是有效的。Rust 知道这些是因为(longest
)函数的参数和返回值都使用了相同的生命周期参数 'a
。
如果从人的角度读上述代码,我们可能会觉得这个代码是正确的。 string1
更长,因此 result
会包含指向 string1
的引用。因为 string1
尚未离开作用域,对于 println!
来说 string1
的引用仍然是有效的。然而,我们通过生命周期参数告诉 Rust 的是: longest
函数返回的引用的生命周期应该与传入参数的生命周期中较短那个保持一致。因此,借用检查器不允许上面的代码,因为它可能会存在无效的引用。
深入理解生命周期
指定生命周期参数的正确方式依赖函数实现的具体功能。例如,如果将 longest
函数的实现修改为总是返回第一个参数而不是最长的字符串 slice,就不需要为参数 y
指定一个生命周期。如下代码将能够编译:
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}
在这个例子中,我们为参数 x
和返回值指定了生命周期参数 'a
,不过没有为参数 y
指定,因为 y
的生命周期与参数 x
和返回值的生命周期没有任何关系。
测试
创建一个新的库项目 adder
:
$ cargo new adder --lib
Created library `adder` project
$ cd adder
文件名: src/lib.rs
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
assert!(bool)
: 断言, 参数为bool
类型
assert_eq!(a, b)
: 断言, 判断传入的两个参数值是否相同
assert_ne!(a, b)
: 断言, 判断传入的两个参数值是否不相同
运行测试的命令
cargo test
: 运行项目中所有的测试
cargo test -- --test-threads=3
: 将测试线程设置为 3
cargo test -- --nocapture
: 默认在测试结果中不显示通过的测试中的print信息, 设置此参数后则会显示
cargo test one
: 指定运行one
这个测试方法, 或者方法名中包含了one
闭包(closures)
Rust的闭包可以理解为一个匿名函数, 且该函数可以作为其他函数的参数或者赋值给某个变量
let expensive_closure = |num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
expensive_closure(3)
定义了一个匿名函数, 接收一个参数num
, 将该函数赋值给expensive_closure
这个变量, 可通过expensive_closure(3)
调用该函数
上述案例中, 并没有像定义普通函数一样指定参数num
的类型, Rust能够推断出闭包参数的类型, 当然也可以手动指定其类型或函数的返回类型
let expensive_closure = |num: u32| -> u32 {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
虽然没有指定参数的类型, 但是rust只会在第一次调用闭包的时候推断其类型, 并且一直保存该参数的类型, 也就是如果两次调用闭包函数传入的参数类型不一致, 也会发生报错, 如:
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);
迭代器Iterator
生成迭代器
vector转为迭代器
let my_fav_fruits = vec!["banana", "custard apple", "avocado", "peach", "raspberry"];
// 使用iter()或into_iter()方法转为迭代器
let mut my_iterable_fav_fruits = my_fav_fruits.iter(); // 或my_fav_fruits.into_iter()
next()从迭代器取值
next()
的返回值是Option
类型的, 并且Some
中的值是迭代器中元素的引用
迭代器中没有元素时返回的是None
assert_eq!(my_iterable_fav_fruits.next(), Some(&"banana"));
assert_eq!(my_iterable_fav_fruits.next(), Some(&"custard apple"));
assert_eq!(my_iterable_fav_fruits.next(), Some(&"avocado"));
assert_eq!(my_iterable_fav_fruits.next(), Some(&"peach"));
assert_eq!(my_iterable_fav_fruits.next(), Some(&"raspberry"));
assert_eq!(my_iterable_fav_fruits.next(), None);
注意: 使用next()
显式地消费迭代器, 那么迭代器变量my_iterable_fav_fruits
需要是可变的mut
变量
for循环遍历迭代器
for i in my_iterable_fav_fruits {
println!("{}", i)
}
消费适配器(consuming adaptors)
标准库中有一系列调用了next()的方法, 被称为消费适配器, 因为调用他们会消耗迭代器
let v1 = vec![1, 2, 3, 4, 5];
let v1_iter = v1.iter();
sum()
获取迭代器所有项的总和
let total: i32 = v1_iter.sum();
collect()
将迭代器转为Vec
// 转为Vec的基本用法, 前面需指定变量类型为Vec
let v2: Vec<i32> = v1_iter.collect(); // 该用法前面必须指定变量类型
let v3: Vec<_> = v1_iter.collect(); // 同上, 也可以不指定Vec中元素的具体类型
// 使用turbofish语法, 前面不需要指定变量类型
let v4 = v1_iter.collect()::<Vec<i32>>(); // 该用法在collect()后面指定转换的类型为Vec
let v5 = v1_iter.collect()::<Vec<_>>(); // 同上, 也可以不指定Vec中元素的具体类型
将迭代器转为Result
let results = [Ok(1), Err("nope"), Ok(3), Err("bad")];
let result: Result<Vec<_>, &str> = results.iter().cloned().collect();
// gives us the first error
assert_eq!(Err("nope"), result);
let results = [Ok(1), Ok(3)];
let result: Result<Vec<_>, &str> = results.iter().cloned().collect();
// gives us the list of answers
assert_eq!(Ok(vec![1, 3]), result);
字符串字面量使用迭代器
案例为: 字符串字面量首字符大写, 返回结果为字符串类型String
fn capitalize_first() -> String {
let input = "hello";
let mut c = input.chars(); // 转为迭代器
match c.next() { // 使用next取出第一个值 "h"
None => String::new(),
Some(first) => first.to_uppercase().to_string() + c.as_str(),
// to_uppercase()将h转为大写的H
// to_string()转为字符串String类型
// c.as_str()将剩下的元素转为字面量"ello"
// + 将"H"(String)和"ello"(&str)拼接为"Hello"(字符串String)
}
}
迭代器适配器(iterator adaptors)
他们允许我们将当前迭代器变为不同类型的迭代器。可以链式调用多个迭代器适配器。不过因为所有的迭代器都是惰性的,必须调用一个消费适配器方法以便获取迭代器适配器调用的结果。
let v1: Vec<i32> = vec![1, 2, 3, 4, 5];
// map
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
for i in v2 {
println!("{}", i); // 6 2 3 4 5
}
// filter
let v3: Vec<_> = v1.iter().filter(|&x| x % 2 == 0).collect();
for i in v2 {
println!("{}", i); // 6 2 4
}
// fold 将每次计算结果作为下一次计算的参数acc, 可设置acc的初始值为0或其他值, 返回值类型为初始值的类型
let v2 = v1.iter().fold(1, |acc, x| acc * x); // 计算所有元素的乘积
println!("{}", v2); // 120
// flatten 拉平嵌套类型
// 拉平Vec中的Vec
let data = vec![vec![1, 2, 3, 4], vec![5, 6]];
let flattened = data.into_iter().flatten().collect::<Vec<u8>>();
assert_eq!(flattened, &[1, 2, 3, 4, 5, 6]);
// 一次只能拉平一个层级
let d3 = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]];
let d2 = d3.iter().flatten().collect::<Vec<_>>(); // 拉平一次
assert_eq!(d2, [&[1, 2], &[3, 4], &[5, 6], &[7, 8]]);
let d1 = d3.iter().flatten().flatten().collect::<Vec<_>>(); //拉平两次
assert_eq!(d1, [&1, &2, &3, &4, &5, &6, &7, &8]);
// 拉平Vec中的HashMap
use std::collections::HashMap;
let mut h1 = HashMap::new();
h1.insert("a", 1);
h1.insert("b", 2);
h1.insert("c", 3);
let mut h2 = HashMap::new();
h2.insert("aa", 10);
h2.insert("bb", 20);
let z = vec![h1, h2];
// 获取z中value为偶数的元素个数
let r = z.iter().flatten().filter(|x| x.1 % 2 == 0).count();
println!("{}", r);
//flatten() 将vec中的两个hashmap的键值对拉平到一起, 且每个键值对是元祖的形式(key, value)存放, 结果类似于: [("a", 1), ("b", 2), ("c", 3), ("aa", 10), ("bb", 20)]
// 再通过filter遍历每个元祖, 获取元祖的第二个值(即HashMap的value值), 判断是否为偶数
// count()返回偶数的个数
Cargo和Crates.io
自定义发布配置
cargo编译
# dev开发环境, 未优化+debug信息
$ cargo build
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
# release正式环境, 已优化
$ cargo build --release
Finished release [optimized] target(s) in 0.0 secs
定制cargo.toml配置
[profile.dev]
opt-level = 0
[profile.release]
opt-level = 3
opt-level
设置控制 Rust 会对代码进行何种程度的优化。这个配置的值从 0 到 3。越高的优化级别需要更多的时间编译, dev
的 opt-level
默认为 0
, release
配置的 opt-level
默认为 3
将 crate 发布到 Crates.io
编写有用的文档注释
文档注释使用三斜杠 ///
而不是两斜杆以支持 Markdown 注解来格式化文本。文档注释就位于需要文档的项的之前。
/// Adds one to the number given.
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}
可以运行 cargo doc
来生成这个文档注释的 HTML 文档
也可以运行 cargo doc --open
构建当前 crate 文档(同时还有所有 crate 依赖的文档)的 HTML 并在浏览器中打开。
文档注释作为测试
在文档注释中增加示例代码块是一个清楚的表明如何使用库的方法, 没有什么比有例子的文档更好的了,但最糟糕的莫过于写完文档后改动了代码,而导致例子不能正常工作。而Rust在运行cargo test
时, 也会运行文档注释中的示例代码!
使用 pub use 导出合适的公有 API
在开发时候文件结构可能是一个包含多个层级的分层结构,这对于用户来说并不方便。这是因为想要使用被定义在很深层级中的类型的人可能很难发现这些类型的存在。他们也可能会厌烦要使用 use my_crate::some_module::another_module::UsefulType;
而不是 use my_crate::UsefulType;
来使用类型。
可以选择使用 pub use
重导出(re-export)项来使公有结构不同于私有结构。重导出获取位于一个位置的公有项并将其公开到另一个位置,好像它就定义在这个新位置一样。
智能指针
智能指针通常使用结构体实现。智能指针区别于常规结构体的显著特性在于其实现了 Deref
和 Drop
trait。Deref
trait 允许智能指针结构体实例表现的像引用一样,这样就可以编写既用于引用、又用于智能指针的代码。Drop
trait 允许我们自定义当智能指针离开作用域时运行的代码。
Box <T>
Box特性: box 允许你将一个值放在堆上而不是栈上, 且没有性能损失
适用场景:
- 当有一个未知大小的类型,而编译时又需要明确知道它的大小(否则无法通过编译)的时候, 如使用递归类型时, 可以使用box
- 当有大量数据, 并想在不拷贝数据(copy())的情况下转移所有权的时候, 可以使用box
- 当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候
使用 Box<T>在堆上储存数据
fn main() {
let a = 5;
let b = Box::new(5);
println!("a = {}, b = {}", a, b);
}
第一行定义了变量a
, 其指向存在栈上的5
这个值, 默认情况下简单类型都存放在栈上
第二行定义了变量 b
,其值是一个box
, 这个box
本身存在栈上, 这个box
是一个指针, 指向存在堆上的5
这个值
即通过box将原本会存在栈上值存在了堆上面, 当然将一个单独的值存放在堆上并不是很有意义, 下面看看一个不使用 box 则无法定义的类型的例子。
Box 允许创建递归类型
Rust 需要在编译时知道类型占用多少空间。一种无法在编译时知道大小的类型是 递归类型(recursive type),其值的一部分可以是相同类型的另一个值。这种值的嵌套理论上可以无限的进行下去,所以 Rust 不知道递归类型需要多少空间。不过 box 有一个已知的大小,所以通过在循环类型定义中插入 box,就可以创建递归类型了。
不使用Box定义递归类型的枚举
enum List {
Cons(i32, List),
Nil,
}
let list = Cons(1, Cons(2, Cons(3, Nil)));
使用Box定义递归类型的枚举
enum List {
Cons(i32, Box<List>),
Nil,
}
let list = Cons(1,
Box::new(Cons(2,
Box::new(Cons(3,
Box::new(Nil))))));
Cons
成员将会需要一个 i32
的大小加上储存 box 指针数据的空间。Nil
成员不储存值,所以它比 Cons
成员需要更少的空间。
Deref Trait
实现 Deref
trait 允许我们重载 解引用运算符(dereference operator)*
通过解引用运算符追踪指针的值
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
像引用一样使用 Box<T>
因为Box<T>已经实现了Deref Trait
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
自定义智能指针
// 定义结构体
struct MyBox<T>(T);
// 实现构造函数
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
// 实现Deref Trait
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0 // 返回元祖的第一个元素
}
}
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
函数和方法的隐式 Deref 强制转换
Deref 强制转换(deref coercions)是 Rust 在函数或方法传参上的一种便利。当这种特定类型的引用作为实参传递给和形参类型不同的函数或方法时,Deref 强制转换将自动发生。这时会有一系列的 deref
方法被调用,把我们提供的类型转换成了参数所需的类型。
fn hello(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
上述的hello
函数接收的参数类型为字符串切片slice
, 而在main
函数中, MyBox
实现了Deref Trait
, 因此调用hello
时, 传入MyBox
的应用时, 也能成功运行. 因为其中发生了一系列的自动Deref转换, 即&MyBox
->&Sting
-> &str
Drop Trait
对于智能指针模式来说第二个重要的 trait 是 Drop
,其允许我们在值要离开作用域时执行一些代码。可以为任何类型提供 Drop
trait 的实现,同时所指定的代码被用于释放类似于文件或网络连接的资源。我们在智能指针上下文中讨论 Drop
是因为其功能几乎总是用于实现智能指针。例如,Box
自定义了 Drop
用来释放 box 所指向的堆空间。
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer { data: String::from("my stuff") };
let d = CustomSmartPointer { data: String::from("other stuff") };
println!("CustomSmartPointers created.");
}
Drop
trait 包含在 prelude 中,所以无需导入它。我们在 CustomSmartPointer
上实现了 Drop
trait,并提供了一个调用 println!
的 drop
方法实现。当运行这个程序,会出现如下输出:
CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!
通过 std::mem::drop 提早丢弃值
我们不能直接手动调用drop
方法, 如下面直接调用则无法编译
fn main() {
let c = CustomSmartPointer { data: String::from("some data") };
println!("CustomSmartPointer created.");
c.drop(); // 不能直接调用
println!("CustomSmartPointer dropped before the end of main.");
}
如果我们需要强制提早清理值,可以使用 std::mem::drop
函数。
fn main() {
let c = CustomSmartPointer { data: String::from("some data") };
println!("CustomSmartPointer created.");
drop(c);
println!("CustomSmartPointer dropped before the end of main.");
}
运行这段代码会打印出如下:
CustomSmartPointer created.
Dropping CustomSmartPointer with data `some data`!
CustomSmartPointer dropped before the end of main.
无畏并发
多线程
fn main() {
use std::thread;
use std::time::Duration;
let handle = thread::spawn(|| {
for i in 1..10 {
println!("spawned thread: {}", i);
thread::sleep(Duration::from_millis(100));
}
});
for i in 1..5 {
println!("main thread: {}", i);
thread::sleep(Duration::from_millis(100));
}
handle.join().unwrap(); // 使用 join 会阻塞当前线程, 等待handle线程运行结束, 否则不使用join的话主线程结束后, 子线程也会结束
}
为了创建一个新线程,需要调用 thread::spawn
函数并传递一个闭包,并在其中包含希望在新线程运行的代码。
使用 join
会阻塞当前线程, 等待handle
的线程运行结束后, 才会继续往下执行
否则不使用join的话, 主线程结束后, 子线程也会结束, 不管其是否执行完毕
main thread: 1
spawned thread: 1
main thread: 2
spawned thread: 2
main thread: 3
spawned thread: 3
main thread: 4
spawned thread: 4
spawned thread: 5
spawned thread: 6
spawned thread: 7
spawned thread: 8
spawned thread: 9
线程与 move 闭包
在参数列表前使用 move
关键字强制闭包获取其使用的环境值的所有权。这个技巧在创建新线程将值的所有权从一个线程移动到另一个线程时最为实用。
不使用move时, 下面代码会报错: closure may outlive the current function, but it borrows v, which is owned by the current function
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();
}
因为闭包并没有v
的所有权, 通过move
强制让闭包获取其使用的值得所有权
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();
}
Channel
Rust 中一个实现消息传递并发的主要工具是 通道(channel), 与Golang语言中的通道作用类似
use std::sync::mpsc;
fn main(){
// 创建通道, 并将两端返回
let (tx, rx) = mpsc::channel(); // mpsc 是 多个生产者,单个消费者(multiple producer, single consumer)的缩写
// 创建子线程, 通过move将生产者传入线程中使用
thread::spawn(move ||{
let val = String::from("hello");
// send向通道发送值, 发送时会转移所有权, 一旦发送成功, 作用域中后面的代码将无法使用该值
println!("开始发送数据");
thread::sleep(Duration::from_secs(5));
tx.send(val).unwrap();
});
// 主线程中从通道接收值
println!("等待接收数据");
let received = rx.recv().unwrap(); // recv会阻塞当前线程, 直到从通道中接收一个值, try_recv不会阻塞,相反它立刻返回一个 Result<T, E>
println!("received: {}", received);
}
这里使用 mpsc::channel
函数创建一个新的通道;mpsc
是 多个生产者,单个消费者(multiple producer, single consumer)的缩写。简而言之,Rust 标准库实现通道的方式意味着一个通道可以有多个产生值的 发送(sending)端,但只能有一个消费这些值的 接收(receiving)端。
通道的发送端有一个 send
方法用来获取需要放入通道的值。send
方法返回一个 Result
类型,所以如果接收端已经被丢弃了,将没有发送值的目标,所以发送操作会返回错误。
通道的接收端有两个有用的方法:recv
和 try_recv
。这里,我们使用了 recv
,它是 receive 的缩写。这个方法会阻塞主线程执行直到从通道中接收一个值。一旦发送了一个值,recv
会在一个 Result
中返回它。当通道发送端关闭,recv
会返回一个错误表明不会再有新的值到来了。
try_recv
不会阻塞,相反它立刻返回一个 Result
:Ok
值包含可用的信息,而 Err
值代表此时没有任何消息。如果线程在等待消息过程中还有其他工作时使用 try_recv
很有用:可以编写一个循环来频繁调用 try_recv
,在有可用消息时进行处理,其余时候则处理一会其他工作直到再次检查。
多个生产者一个消费者
use std::sync::mpsc;
fn main() {
// 创建通道, 并将两端返回
let (tx, rx) = mpsc::channel();
// 创建多个子线
for i in 0..3 {
// 循环创建三个线程, 每个线程克隆一个生产者
let tx_i = tx.clone();
thread::spawn(move || {
for j in 1..10 {
let msg = format!("第{}个线程, msg:{}", i, j);
tx_i.send(msg).unwrap();
thread::sleep(Duration::from_secs(1))
}
});
}
// 循环从通道接收值, 一直阻塞
for msg in rx {
println!("{}", msg);
}
}
使用互斥锁防止多线程同时修改同一对象
use std::sync::Mutex;
fn main() {
let m = Mutex::new(5);
{
let mut num = m.lock().unwrap();
*num = 6;
}
println!("m = {:?}", m);
}
Mutex
是一个智能指针。更准确的说,lock
调用 返回 一个叫做 MutexGuard
的智能指针。这个智能指针实现了 Deref
来指向其内部数据;其也提供了一个 Drop
实现当 MutexGuard
离开作用域时自动释放锁,这正发生于示例内部作用域的结尾。为此,我们不会冒忘记释放锁并阻塞互斥器为其它线程所用的风险,因为锁的释放是自动发生的。