安装
- curl https://sh.rustup.rs -sSf | sh
- source $HOME/.cargo/env
- 更新 rustup update
- 卸载 rustup self uninstall
- 文档 rustdoc
- hello world
# rust.rs
fn main() {
println!("Hello, world!");
}
rustc main.rs
./main
intro
- cargo 项目管理工具
- 创建新项目
cargo new hello_cargo
- 编译代码 生成debug
cargo build
- release模式生成代码 以更长的编译时间为代价来优化代码,从而使代码拥有更好的运行时性能
cargo build --release
cargo run
- 检查代码,不编译,速度快
cargo check
- 格式化代码
cargo-fmt --all
- 文档查看
cargo doc --open
第二章 猜谜游戏
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1, 101);
// println!("The secret number is:{}", secret_number);
loop {
println!("Please input your guess.");
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(k) => {
println!("err: {}", k);
continue;
}
};
println!("You guessed: {}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("Your win!");
break;
}
}
}
}
第三章 通用编程概念
- 变量,默认是不可变的。当一个变量是不可变的是,这个值就不能再改变了。使用mut使变量可变。
- 常量 const ABC :u32 = 100;
- 隐藏(shadow):新声明变量可以覆盖旧的同名变量。可以使用let关键字并配以相同的名称来不断地隐藏变量。
- 通过使用let,可以进行一系列的变换操作,并允许这个操作在完成后保持自己的不变性
- 由于重复使用let关键字会创建出新的变量,所以我们可以复用变量名称的同时改变它的类型。
- 数据类型:
- 标量类型 scalar:单个值类型的统称
- 整数 i8 i16 i32 i64 isize u8 u16 u32 u64 usize。isize usize长度取决于程序运行的平台。
67u8 一个u8类型的整数67
98_222 十进制
0xff 十六进制
0o77 八进制
0b1111_0000 二进制
b'A' 字符
- 浮点数 f64 f32
- 布尔值 true false
- 字符 char:Unicode变量值,占4个字节 ‘😄’
- 复合类型 compound
- 元组tuple:将其他不同类型的多个值组合进一个复合类型中
let tup:(i32,f64,u8,char) = (500,6.4,1,'宝');
# 索引访问
println!("0:{} 1:{} 2:{} 3:{}",tup.0,tup.1,tup.2,tup.3);
# 解构destructuring
let (a,b,c,d) = tup;
println!("a:{} b:{} c:{} d:{}",a,b,c,d);
- 数组类型array:存储多个相同类型的值。rust会自动进行越界检查。
let a = [1,2,3,4];
let a : [u8;4]= [1, 2, 3, 4];
println!("0:{} 1:{} 2:{} 3:{}", a[0], a[1], a[2], a[3]);
let a = [3;5]; // 等价于 let a = [3,3,3,3,3];
- 函数:rust使用蛇形命名法(snake case)来作为规范函数和变量名称的风格。蛇形命名法只使用小写的字母进行命名,并以下划线分割单词。
-
语句(statement)指那些执行操作但不返回值得指令,而表达式(expression)则是指会进行计算并产生一个值作为结果的指令
·`。
let x = 5;
let y = {
let x = 3;
x + 1 // 不能有分号;
};
println!("x:{} y:{}", x, y); // x:5 y:4
- 返回值
fn another_function(x: i32) -> i32 {
x + 1
}
- 控制流
- if表达式; if condition{}else if condition{} else{}
- 在let中使用if; let number = if condition{5}else{6};
分支产生的类型必须一致,否则编译错误
:if
andelse
have incompatible types - 使用循环重复执行代码:loop while for
- loop循环:break可以返回数据,分号结尾。
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter==10{
break counter*2;
}
};
println!("The result is {}",result);
}
- while条件循环
fn main() {
let mut number = 3;
while number!=0{
println!("{}!",number);
number -= 1;
}
println!("LIFEOFF!");
}
- 使用for来循环遍历集合
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a.iter() {
println!("the value is: {}", element);
}
for number in (1..4).rev() {
println!("{}!", number);
}
println!("LIFEOFF!");
}
第四章 认识所有权
- Rust内存管理:使用包含特定规则的所有权系统来管理内存,这套规则允许编译器在编译过程中执行检查工作,而不会产生任何运行时开销。
- 堆和栈:所有存储在栈中的数据都必须拥有一个固定的大小。对于那些在编译期无法确定大小的数据,你就只能将它们存储在堆中。
- 向栈上推入数据要比在堆上分配数据更有效率一些,因为操作系统省去了搜索新数据存储位置的工作;这个位置永远在栈的顶端。除此之外,堆上分配空间还必须首先找到足够放下对应数据的空间。
- 由于多了指针跳转的缓解,所以访问堆上的数据要慢于栈上的数据。
- 处理器在操作排布紧密的数据(比如在栈上)时要比操作排布稀疏的数据(比如在堆上)有效率的多。
所有权规则
:
-
Rust中的每一个值都有一个对应的变量作为它的所有者
。 -
在同一时间内,值有且仅有一个所有者
。 -
当所有者离开自己的作用域,它持有的值就会被释放掉
。
- Rust会在作用域结束的地方(即}处)自动调用drop函数。
- 移动(move):下面的代码s2使s1变量无效,称这种行为为移动;即s1被移动到了s2上。只复制栈上的数据,堆上的数据不复制。
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("s1:{} s2:{}", s1,s2); // error[E0382]: borrow of moved value: `s1`
}
- rust永远不会自动创建数据的深度拷贝。因此,在Rust上,任何自动的赋值操作都可以被视为高效的。
- 克隆(clone):当确实需要深度拷贝String堆上的数据,而不仅仅是栈数据时,就可以使用clone的方法。
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1:{} s2:{}", s1,s2); // s:hello s2:hello
}
- 栈上数据的复制:s1在赋值给s2之后依然有效。
fn main() {
let s1 = "hello";
let s2 = s1;
println!("s1:{} s2:{}", s1,s2); // s:hello s2:hello
}
- Rust提供一个名为Copy的trait,它可以用于整数这类完全存储在栈上的数据类型。一旦某种数据类型拥有了Copy这种trait,那么它的变量就可以赋值给其他变量之后保持可用性。如果一种类型本身或这种类型的任意成员实现了Drop这种trait,那么Rust就不允许其实现Copy这种trait。尝试给某个需要在离开作用域时执行特殊指令的类型实现Copy这种trait会导致编译时错误。
- 任何简单标量的组合类型都可以是Copy的
- 整数类型、bool、char、浮点类型
- tuple(元组),如果其所有的字段都是Copy的。(i32,i32)是Copy的,(i32,string)不是Copy的
- 任何需要分配内存或某种资源的都不是Copy的
- 所有权与函数:**将值传递给函数语义上类似于对变量进行赋值。将变量传递给函数将会触发移动或复制。*2
fn main() {
let s = String::from("hello"); // 变量s进入作用域
takes_ownership(s); // s的值被移动进了函数
// 所以它从这里开始不再有效
let x = 5; // 变量x进入作用域
makes_copy(x); // 变量x被传递进入函数
println!("{}",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(); // 函数将它的返回值移动至s1中
let (s2, len) = calculate_length(s1);
println!("The length of '{}' is {}!", s2, len);
}
fn gives_ownership() -> String {
// 返回值移动至调用它的函数内
let some_string = String::from("Hello");
some_string //作为返回值移动至调用函数
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len();
(s, length) // 返回元组
}
- 变量所有权的转移总是遵循相同的模式:当一个值赋值给另一个变量时就会转移所有权。当一个持有堆数据的变量离开作用域时,它的数据就会被drop清理回收,除非这些数据的所有权移动到了另一个变量上。
- 引用与借用:
- &引用,允许在不获取所有权的前提下使用值.由于引用不持有值的所有权,所以当引用离开当前作用域时,它指向的值也不会被丢弃。
- 引用默认是不可变的,Rust不允许我们去修改引用指向的值。
- 通过引用传递参数给函数的方法也被称为借用(borrowing)。
- 可变引用
fn main() {
let mut s1 = String::from("hello");
let l = change(&mut s1);
println!("str:{} len:{}", s1, l);
}
fn change(s: &mut String) -> usize {
s.push_str(", world");
s.len()
}// s离开作用域,但是由于它不持有自己所指向值的所有权,所以它不会销毁其指向的数据
// 对于特定作用域中的特定数据来说,一次只能声明一个可变引用作为参数
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // 失败
// 我们不能在拥有不可变引用的同时创建可变引用。
let mut s = String::from("hello");
let r1 = & s; // 成功
let r2 = & s; // 成功
let r3 = &mut s; // 失败
- 引用规则
- 在任何一段时间里,你要么只能拥有一个可变引用,要么只能拥有任意数量的不可变引用。
它帮助我们在编译时避免数据竞争。
- 引用总是有效的。
- 悬垂引用:Rust编译器保证不会进入这种悬垂状态(引用指向已经释放的内存)
fn main(){
let ref_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello"); // s被绑定到新的String上
&s // 将指向s的引用返回给调用者
} // 变量s在这里离开作用域并随之被销毁,它指向的内存也不再有效
- 切片slice:除了引用,还有一种没有所有权的类型。切片允许我们引用集合中某一段连续的元素序列,而不是整个集合。
- 字符串切片类型写作
&str
。不可变引用
let s = String::from("hello world ccc");
println!("{} {} {} {}",&s[..5],&s[6..11],&s[3..],&s[..]);
let s = "Hello, world!";// 变量s的类型其实就是&str:它是一个指向二进制程序特定位置的切片。
- 数组切片类型 &[i32]
fn ff(sli :&[i32])->&[i32]{
&sli[3..]
}
第五章 使用结构体来组织相关联的数据
- 结构体
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
fn main() {
let mut user1 = User { // 一旦实例可变,那么实例中的所有字段都将是可变
email: String::from("someone123@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
user1.email = String::from("another123@example.com"); // 可修改
let user2 = User {
email: String::from("someone456@example.com"),
username: String::from("someusername456"),
..user1 // 其他字段和user1相等可以采用此方式初始化
};
let user3 = build_user(
String::from("someone789@example.com"),
String::from("someusername789"),
);
}
fn build_user(email: String, username: String) -> User {
User {
email, // 同名可以省略
username,
active: true,
sign_in_count: 1,
}
}
- 元组结构体:不需要对字段命名,只保留类型,访问和元组类似
struct Color(i32,i32,i32);
struct Point(i32,i32,i32);
let black = Color(0,0,0);
let origin = Point(0,0,0);
println!("Color {} {} {}",black.0,black.1,black.2);
println!("Point {} {} {}",origin.0,origin.1,origin.2);
- 空结构体:没有任何字段。当想要在某些类型上实现一个trait,却不需要在该类型中存储任何数据时,空结构体就可以发挥相应作用。
- 调试打印结构体。派生trait。Rust提供了许多可以通过derive注解来派生的trait。
#[derive(Debug)] //
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rectl = Rectangle {width: 30,height: 50,};
println!("rectl is {:#?}",rectl); // {:?} 或 {:#?}
}
- 方法与关联函数
- 方法:为结构体实例指定行为
- 关联函数:可以将不需要实例的特定功能放置到结构体的命名空间中.String::from就是关联函数
- 每个结构体可以拥有多个impl块
#[derive(Debug)]
struct Rectangle {width: u32,height: u32,}
fn main() {
let rectl1 = Rectangle {width: 30,height: 50,};
let rectl2 = Rectangle {width: 30,height: 50,};
println!("rectl area is {:#?}", rectl1.area());
println!("can rectl1 hold rectl2? {} ", rectl1.can_hold(&rectl2));
println!("rectl is {:#?}", Rectangle::square(100));
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
// 方法:为结构体实例指定行为
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
// 关联函数:可以将不需要实例的特定功能放置到结构体的命名空间中
fn square(size: u32) -> Rectangle {
Rectangle {width: size,height: size,}
}
}
第六章 枚举与模式匹配
- 枚举:列举所有可能的值来定义一个类型。
enum Message {
Quit,
Move { x: i32, y: i32 }, // 包含一个匿名结构体
Write(String), // 包含了一个String
ChangeColor(i32, i32, i32), // 包含了三个i32值
}
- Option:它常常被用来描述某些可能不存在的值。在编写代码的过程中,不必再去考虑一个值是否为空可以极大地增强我们对自己代码的信心。为了持有一个可能为空的值,我们总是需要将它显示地放入对应类型的Option< T>值中。当我们随后使用这个值得时候,也必须显示地处理它可能为空的情况。无论在什么地方,只要一个值得类型不是Option< T>的,我们就可以完全地假设这个值不是非空的。这是Rust为了限制空值泛滥以增加Rust代码安全性而做出的一个有意为之的设计决策。
enum Option<T>{
Some(T),
None,
}
let some_number = Some(5);
let some_string = Some("a string");
let absent_number: Option<i32> = None;
- match它允许将一个值与一系列的模式相比较,并根据匹配的模式执行相应的代码。
enum Coin {
Penny,
Nickel,
Dime,
Quarter(String),
}
fn value_in_cents(coin: &Coin) -> u32 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
},
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => { // 绑定值的模式
println!("state:{}",state);
25
},
}
}
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
fn main() {
let a = Coin::Penny;
println!("{}", value_in_cents(&a));
let a = Coin::Quarter(String::from("Csq"));
println!("{}", value_in_cents(&a));
}
- 匹配必须穷举所有可能
- if let else
match coin {
Coin::Quarter(state) => println!("state:{}", state),
_ => count += 1,
}
// 等价
if let Coin::Quarter(state) = coin{
println!("state:{}", state)
}else{
count+=1;
}
第七章 使用包、单元包及模块来管理日渐复杂的项目
- 模块系统
- 包package:一个用于构建、测试并分享单元包的cargo功能
- 单元包crate:一个用于生成库或可执行文件的树形模块结构
- 模块module及use关键字:它们被用于控制文件结构、作用域及路径的私有性。
- 路径path:一种用于命名条目的方法,这些条目包括结构体、函数和模块等
- 包
- 一个包中只能拥有最多一个库单元包。src/lib.rs
- 包可以拥有任意多个二进制单元包。src/bin目录下添加二进制单元包
- 包必须存在至少一个单元包
- 通过定义模块来控制作用域及私有性。
- Rust中的所有条目(函数、方法、结构体、枚举、模块及常量)默认都是私有的。处于父级模块中的条目无法使用子模块中的私有条目,但子模块中的条目可以使用它所有祖先模块中的条目。
- 使用pub暴露路径
- 使用super关键字构造相对路径
- use关键字将路径导入作用域
- 使用as关键字来提供新的名称
- 使用pub use重导出名称
pub use crate::front_of_house::hosting; // 当其他包引用此包时,可直接使用hosting::xxx
- 使用外部包 use std::io;
- 使用嵌套的路径来清理众多use语句 use std::{cmp::Ordering, io};
第八章 通用集合类型
- 集合将自己持有的数据存储在堆上。这意味着数据的大小不需要在编译时确定,并且可以随着程序的运行按需扩大或缩小数据占用空间。
- 动态数组(vector):连续存储任意多个值
fn main() {
let v: Vec<i32> = Vec::new(); // 创建空动态数组
let v = vec![1, 2, 3]; // 创建包含了值的新动态数组
let mut v = Vec::new();
v.push(5); // 更新动态数组
v.push(6);
v.push(7);
v.push(8);
let mut v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2]; // 索引读取
// 我们持有了一个指向动态数组中元素的不可变引用,但却尝试向这个动态数组的结尾处添加元素,该尝试是不会成功的
// v.push(6); // 编译失败
println!("The third element is {}", third);
match v.get(2) { // v.get()返回Option<T>
Some(third) => println!("The third element is {}", third),
None => println!("There is no third element."),
}
let does_not_exist = v.get(100); // 索引越界返回None
// let does_not_exist = &v[100]; // 越界触发panic
let mut v = vec![1, 2, 3, 4, 5];
for i in &mut v{
*i += 50;
}
v.pop();
} // v离开作用域并随之销毁,销毁动态数组时也会销毁其中的元素
- vector使用枚举来存储不同类型的值,可以显式地列出所有被放入动态数组的值类型。
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,它通常以借用的形式(&str)出现。
fn main() {
let mut s = String::new();
// 基于字符串字面量创建String的两种方式
let data = "initial contents";
let mut s = data.to_string();
let mut s = String::from("initial contents");
// 更新字符串
let s2 = " junmo ";
s.push_str(s2);
s.push('k');
println!("s:{} \ns2:{}", s, s2); // 拼接之后字符串s2还能使用
// 字符串拼接
let s1 = String::from("111");
let s2 = String::from("222");
let s3 = String::from("333");
let s4 = format!("{}-{}-{}", s1, s2, s3);
println!("{} {} {} {}", s1, s2, s3, s4);
let s5 = s1 + &s2+ &s3; // s1不再能使用,所有权转移
println!(" {} {} {}", s2, s3, s5);
// 字符串索引访问时不允许的
let s = "张君宝";
let a = &s[0..3]; // 字符串切片
// 遍历字符串
for i in s.chars(){
println!("{} ",i);// 张君宝
}
for i in s.bytes(){
println!("{} ",i); // 229 188 160 229 144 155 229 174 157
}
println!("{} {}",s,a);
}
- 哈希映射(hash map)可以让你将值关联到一个特定的键上,它是另外一种数据结构–映射(map)的特殊实现。
- HashMap< K, V >;所有的键必须是同种类型,所有的值也必须拥有相同的类型。
fn main() {
use std::collections::HashMap;
let mut s = HashMap::new();
let s1 = String::from("Blue");
let s2 = String::from("Yellow");
s.insert(s1, 10); // s1从这一刻开始失效
s.insert(s2, 50); // s2从这一刻开始失效
// collect构造hashmap
let terms = vec![String::from("Blue"), String::from("Yellow")];
let initial_scores = vec![10, 50];
// HashMap<_, _>不能省略,
let scores: HashMap<_, _> = terms.iter().zip(initial_scores.iter()).collect();
// 获取hashmap值
let key = String::from("Blue");
let score = s.get(&key);
match score{
Some(sss)=>println!("score is {}",sss),
None=>println!("score is None"),
}
for (k,v) in &scores{
println!("{} {}",k,v);
}
// 在键没有对应值时插入数据
let r = s.entry(String::from("Blue")).or_insert(100);
println!("{}",r);
println!("{:?}",s); // {"Blue": 10, "Yellow": 50}
let r = s.entry(String::from("Black")).or_insert(100);
println!("{}",r);
println!("{:?}",s); // {"Black": 100, "Blue": 10, "Yellow": 50}
// 基于旧值更新值
let r = s.entry(String::from("red")).or_insert(0);
*r += 10
}
第九章 错误处理
- 可恢复错误类型Result(T,E)。不可恢复错误时中止运行的panic!宏。
- 回溯信息 RUST_BACKTRACE=1 cargo run
- 失败时触发panic的快捷方式:
- unwrap:当Result的返回值是Ok变体时,unwrap会返回Ok内部的值。而当Result的返回值是Err变体时,unwrap会替我们调用panic!宏。
- expect:在upwrap的基础上允许指定panic!宏所附带的错误提示信息。
第十章 泛型
- 泛型:提高代码复用能力
- 处理重复代码问题
- 泛型是具体类型或其它属性的抽象代替
- 编写的代码不是最终的代码,而是一种模板,里面有一些“占位符”
- 编译器在编译时将“占位符”替换为具体的类型
- 例如: fn largest(list: &[T]) -> T {…}
- 类型参数:
- 短:一个字母
- 驼峰式命名
struct Point<T,U>{
x:T,
y:U,
}
impl <T,U> Point<T,U> {
fn x(&self) -> &T
}
// Enum定义的泛型
enum Result<T,E>{
Ok(T),
Err(e),
}
- Rust泛型没有运行时额外的消耗。在编译时执行泛型代码的单态化。
- 单态化是一个在编译期将泛型代码转化为特定代码的过程,它会将所有使用过的具体类型填入泛型参数从而得到有具体类型的代码
- Trait:定义共享行为
- 被用来向Rust编译器描述某些特定类型拥有的且能够被其他类型共享的功能,它使我们可以以一种抽象的方式来定义共享行为。
- 可以使用trait约束来将泛型参数指定为实现了某些特定行为的类型。
- trait实现限制,孤儿原则:只有当trait或类型定义于我们的库中时,我们才能为该类型实现对应的trait。
- 默认实现:为trait中的某些或所有方法提供默认行为非常有用,它使我们无需为每一个类型的实现唞提供自定义行为。当我们为某个特定类型实现trait时,可以选择保留或重载每个方法的默认行为。
- 使用trait作为参数 pub fn notify(item: impl Summary) {}
- trait bound: pub fn notify<T: Summary>(item: T) {}。impl Summary只是trait bound的一种语法糖
- 通过+语法来指定多个trait约束pub fn notify(item: impl Summary + Display) {}
- 使用where从句来简化trait约束。
fn some_func<T: Display + Clone,U: Clone + Debug>(t: t,u: U) -> i32 {...}
fn some_func<T,U>(t: t,u: U) ->i32
where T: Display + Clone,
U: Clone + Debug
{...}
- 使用trait约束有条件地实现方法
- Rust的每个引用都有自己的生命周期(lifetime),它对应着引用保持有效性的作用域。
- 生命周期最主要的目标在于避免悬垂引用,进而避免程序引用到非预期的数据。
- 生命周期标注并不会改变任何引用的生命周期长度。使用泛型生命周期的函数可以接收带有任何生命周期的引用。在不影响生命周期的前提下,标注本身会被用于描述多个引用生命周期之间的关系。
- fn longest<'a>(x: &'a str,y: &'a str) -> &'a str{…}
- 生命周期省略规则
- 每一个引用参数都会拥有自己的生命周期参数
- 当只存在一个输入生命周期参数时,这个生命周期会被赋予给所有输出生命周期参数
- 当拥有多个输入生命周期参数,而其中一个是&self或&mut self时,self的生命周期会被赋予给所有的输出生命周期参数。
- 静态生命周期:'static 表示整个程序的执行期。
第十一章 编写自动化测试
- 测试函数的函数体组成:
- 准备所需的数据或状态
- 调用需要测试的代码
- 断言运行结果与我们所期望的一致。
- Rust中的测试就是一个标注有test属性的函数。属性attribute是一种用于修饰Rust代码的元数据。
- 当使用Cargo新建一个库项目时,它会自动生成一个带有测试函数的测试模块。
- cargo new adder --lib
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
let result = 2 + 2;
assert_eq!(result, 4);
}
}
- assert!宏:当为true时,无事发生,当为false时,assert!就会调用panic!
- assert_eq!和assert_ne!
- 使用should_panic检查panic:判断代码能否按照预期处理错误状况。
pub struct Guess {
value: u32,
}
impl Guess {
pub fn new(value: u32) -> Guess {
if value < 1 || value > 100 {
panic!("guess value must be greater than or equal to 1,got {}.", value);
}else if value >100{
panic!("guess value must be less than or equal to 100,got {}.", value);
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected="guess value must be less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
}
- 使用Result<T, E>编写测试
// 测试通过时返回Ok(()),在失败时返回一个带有String的Err值。
#[test]
fn it_works() -> Result<(),String>{
if 2+2 == 4{
Ok(())
}else{
Err(String::from("two plus two does not equal four"))
}
}
- cargo test生成的二进制文件默认会并行执行所有的测试,并截获测试运行过程中产生的输出来让测试结果相关的内容更加易读。
- 串行执行测试:cargo test – --test-threads=1。将线程数量限制为1,意味着程序不会使用任何并行操作。
- 默认不打印成功函数的函数打印,可以使用 cargo test – --nocapture 来显示打印输出
- 只运行指定名称的测试:cargo test
this_test_will_p
- 通过显示指定来忽略某些测试:使用 #[ignore] 属性来标记耗时的测试,使用如下命令来指定运行忽略测试 cargo test – --ignored
- 测试的组织结构:
- 单元测试unit test:小而专注,每次只单独测试一个模块或私有接口
- 在tests模块上标注#[cfg(test)]可以让Rust只在执行cargo test命令时编译和运行该部分测试代码,而在执行cargo build时剔除它们。我们不需要对集成测试标注#[cfg(test)],因为集成测试本身就放置在独立的目录中。但是,由于单元测试是和业务代码放置在同一文件中,所以我们必须使用#[cfg(test)]进行标注才能将单元测试的代码排除在编译产出物之外。
Rust允许测试私有函数
- 集成测试integration test:完全位于代码库之外,和正常从外部调用代码库一样使用外部代码,只能访问公共接口,并且再一次测试中可能会联用多个模块。集成测试的目的在于验证库的不同部分能否协同起来正常工作。
- 创建tests目录
- 在执行cargo test时使用–test并指定文件名,可以单独运行某个特定集成测试文件下的所有测试函数。cargo test --test integration_test
- 将每个集成测试的文件编译成独立的包有助于隔离作用域,并使集成测试环境更加贴近于用户的使用场景。可以把公共函数放到tests/commom/xx.rs中,使用mod common即可使用
use adder;
mod common;
#[test]
fn it_adds_two(){
common::setup();
assert_eq!(4,adder::it_works())
}
第十二章 I/O项目:编写一个人命令行程序
- 二进制项目的关注点分离:main.rs只负责运行程序,而lib.rs则负责处理所有真正的业务逻辑。
- 将程序拆分为main.rs和lib.rs,并将实际的业务逻辑放入lib.rs
- 当命令行解析逻辑相对简单时,将它留在main.rs中也无妨。
- 当命令行解析逻辑开始变得复杂时,同样需要将它从main.rs提取至lib.rs中。
第十三章 函数式语言特性:迭代器与闭包
- 闭包:能够捕获环境的匿名函数。
- Rust中的闭包是一种可以存入变量或作为参数传递给其他函数的匿名函数。
- 闭包中的每一个参数及返回值都会被推导为对应的具体类型。
let expensive_result = |num|{
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
- 试图使用两种不同的类型调用同一个需要类型推导的闭包,
会触发类型不匹配的错误。
let csq = |x| x;
let s = csq(String::from("Hello"));
let n = csq(2);
- 标准库中提供了一系列Fn trait,而所有的闭包都至少实现了Fn、FnMut及FnOnce中的一个trait。
- 当闭包从环境中捕获值时,它会使用额外的空间来存储这些值以便在闭包体内使用。
- FnOnce意味着闭包可以从它的封闭作用域中,也就是闭包所处的环境中,消耗捕获的变量。为了实现这一功能,闭包必须在定义时取得这些变量的所有权并将它们移动至闭包中。
- FnMut可以从环境中可变地借用值并对它们进行修改。
- Fn可以从环境中不可变地借用值。
- 当创建闭包时,Rust会基于闭包从环境中使用值的方式来自动推导出它需要使用的trait。
- 所有闭包都自动实现了FnOnce,因为它们至少可被调用一次。那些不需要移动被捕获变量的闭包还会实现FnMut,而那些不需要对被捕获变量进行可变访问的闭包则同时实现了Fn。
- 在参数列表前面添加move关键字,可以强制闭包获取环境中值的所有权。
- 在Rust中,迭代器是惰性的layzy。这也意味着创建迭代器后,除非你主动调用方法来消耗并使用迭代器,否则它们不会产生任何的实际效果。
- 所有的迭代器都实现了定义于标准库中的Iterator trait
- 消耗迭代器,next方法
- 生成其他迭代器:Iterator trait定义了迭代器适配器,这些方法可以使你将已有的迭代器转换成其他不同类型的迭代器。但是因为所有的迭代器都是惰性的,所以必须调用一个消耗适配器的方法才能从迭代器适配器中获得结果。
let v1:Vec<i32> = vec![1,2,3];
// collect方法会消耗迭代器并将结果值收集到某种集合数据类型中。
v1.iter().map(|x| x+1).collect();
- 迭代器的filter方法会接收一个闭包作为参数,这个闭包会在遍历迭代器中的元素时返回一个布尔值,而每次遍历的元素只有在闭包返回true时才会被包含在filter生成的新迭代器中。
- 迭代器是Rust语言中的一种零开销抽象。
第十四章 认识cargo及crates.io
- 通过为任意的配置添加
[profile.*]
区域,可以覆盖默认设置的任意子集。 - 文档注释:使用三斜线来编写文档注释,并且可以在文档注释中使用Markdown语法来格式化内容。cargo doc --open
- 常用的文档注释区域:
- Examples
- Panics,指出函数可能引发panic的场景。
- Errors,当函数返回Result作为结果时,这个区域会指出可能出现的错误,以及造成这些错误的具体原因,它可以帮助
- Safety,当函数使用了unsafe关键字时,这个区域会指出当前函数不安全的原因,以及调用者应当确保的使用前提。
- 将文档注释最为测试
- //! 为包裹当前注释的外层条目添加文档
- 当存在较多嵌套模块时,使用pub use将类型重新导出到顶层模块可以显著地改善用户体验。
- 在工作空间中,整个工作空间只在根目录下有一个Cargo.lock文件,这个规则确保了所有的内部包都会使用完全相同的依赖版本。
第十五章 智能指针
- 智能指针(smart pointer)大多是数据结构,它的行为类似于指针但拥有额外的元数据和附加功能。
- 引用只借用数据的指针;大多数智能指针本身就拥有它们指向的数据。
-
String
与Vec<T>
可以被算作智能指针,因为它们拥有一片内存区域并允许用户对其进行操作。它们还拥有元数据(例如容量等),并提供额外的功能和保障。 - 我们通常会使用结构体来实现智能指针,但区别于一般结构体的地方在于它们会实现Defer和Drop trait。Defer trait使得智能指针结构体的实例拥有与引用一致的行为,它使你可以编写出能够同时用于引用和智能指针的代码。 Drop trait则使你可以自定义智能指针离开作用域时运行的代码。
- 使用
Box<T>
在堆上分配数据
#[derive(Debug)]
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let a = Box::new(Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil)))))));
println!("Box<List>: {:?}", a);
}
- 解引用转换(deref coercion)是Rust为函数和方法的参数提供的一种便捷属性。当某个类型T实现了Deref trait时,它能够将T的引用转化为经过Deref操作后生成的引用。
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
- 解引用与可变性:Rust会在类型与trait满足下面3中情形时执行解引用转换:
- 当
T:Deref<Target=U>
时,允许&T转换为&U。 - 当
T:DerefMut<Target=U>
时,允许&mut T转换为&mut U; - 当
T:Deref<Target=U>
时,允许&mut T转换为&U
- Drop trait在清理时运行代码,无法手动调用Drop trait的drop方法。可以使用std::mem::drop提前丢弃值。
Rust提供了一个名为Rc<T>的类型来支持多重所有权,它名称中的Rc是Reference counting(引用计数)的缩写
。Rc<T>
类型的实例会在内部维护一个用于记录值引用计数的计数器,从而确认这个值是否仍在使用。如果对一个值的引用计数为零,那么就意味着这个值可以被安全地清理掉,而不会触发引用失效的问题。Rc<T>
只能被用于单线程场景中。
- 克隆
Rc<T>
会增加引用计数。使用Rc<T>
可以使单个值拥有多个所有者,而引用计数机制则保证了这个值会在其拥有的所有者存活时一直有效,并在所有者全部离开作用域时被自动清理。
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;
fn main() {
let a = Rc::new(Cons(1, Rc::new(Cons(2, Rc::new(Cons(3, Rc::new(Nil)))))));
println!("a counting:{}", Rc::strong_count(&a));
let b = Cons(11, Rc::clone(&a));
println!("a counting:{}", Rc::strong_count(&a));
{
let c = Cons(12, Rc::clone(&a));
println!("a counting:{}", Rc::strong_count(&a));
}
println!("a counting:{}", Rc::strong_count(&a));
}
// a counting:1
// a counting:2
// a counting:3
// a counting:2
RefCell<T>
类型代表了其持有数据的唯一所有权。对于使用RefCell的代码,Rust则只会在运行时检查这些规则,并在出现违反借用规则的情况下触发panic来提前中止程序。RefCell<T>
只能被用于单线程场景中。
-
Rc<T>
允许一份数据由多个所有者,而Box<T>
和RefCell<T>
都只有一个所有者。 -
Box<T>
允许在编译时检查的可变或不可变借用,Rc<T>
仅允许编译时检查的不可变借用,RefCell<T>
允许允许时检查的可变或不可变借用。 - 由于
RefCell<T>
允许我们在运行时检查可变借用,所以即便RefCell<T>
本身是不可变的,我们仍然能够更改其中存储的值。 - borrow和borrow_mut作为
RefCell<T>
的安全接口,分别返回Ref<T>
和RefMut<T>
这两种智能指针,这两种智能指针都实现了Deref,可以把它们作为一般的引用来对待。RefCell<T>
会记录当前存在多少个活跃的Ref<T>
和RefMut<T>
智能指针。每次调用borrow方法时,RefCell<T>
会将活跃的不可变借用计数加1,并且在任何一个Ref<T>
的值离开作用域被释放时,不可变借用计数将减1。RefCell基于上面的技术来维护和编译器同样的借用检查规则:在任何一个给定的时间里,它只允许你拥有多个不可变借用或一个可变借用。
- 将Rc和RefCell结合使用来实现一个拥有多重所有权的可变数据。Rc允许多个所有者持有同一数据,但只能提供针对数据的不可变访问。如果在Rc内存储了RefCell,那么就可以定义出拥有多个所有者且能够进行修改的值了。
use std::{rc::Rc, cell::RefCell};
use crate::List::{Cons,Nil};
#[derive(Debug)]
enum List{
Cons(Rc<RefCell<i32>>,Rc<List>),
Nil,
}
fn main() {
let value = Rc::new(RefCell::new(5));
let a = Rc::new(Cons(Rc::clone(&value),Rc::new(Nil)));
let b = Cons(Rc::new(RefCell::new(6)),Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(10)),Rc::clone(&a));
*value.borrow_mut() += 10;
println!("a after = {:?}",a);
println!("a after = {:?}",b);
println!("a after = {:?}",c);
}
- 使用Sync trait和Send trait,在模块std::marker
- 允许线程间转移所有权的Send trait
- 允许多线程同时访问的Sync trait
第十七章 Rsut的面向对象编程特性
- 面向对象语言特性:命名对象、继承和封装。
- 对象包含函数和行为
- 封装实现细节。封装:调用对象的外部代码无法直接访问对象内部的实现细节,而唯一可以以对象进行交互的方法便是通过它公开的接口。
- 作为类型系统和代码共享机制的继承。继承机制使得对象可以沿用另一个对象的数据和行为,而无需重复定义代码。
- trait对象安全
- 方法的返回类型不是Self。
- 方法中不包含任何泛型参数。
第十八章 模式匹配
- match表达式必须穷尽匹配值的所有可能性
- if let 、else if、else if let
- while let 反复执行同一模式匹配直到出现失败的情形
- 模式可以分为不可失败irrefutable和可失败refutable两种类型。不可失败的模式能够匹配任何传入的值。
- 函数参数、let语句及for循环只接收不可失败模式,因为在这些场合下,我们的程序无法在值不匹配时执行任何有意义的行为。
- if let和while let表达式则只接收可失败模式,因为它们在设计之时就将失败考虑在内了:条件表达式的功能就是根据条件的成功与否执行不同的操作。
- 模式语法
- _模式匹配剩余, …忽略剩余部分
- 分支匹配使用 | 表示或
- 使用 … 来匹配区间值
let x = 1;
match x {
1|3 => println!("one or three"),
2 => println!("two"),
5...10 => println!("5-10")
_ => println!("other"),
}
let num = (1,2,3,4,5);
match num{
(first,...,last)=>println("some number:{}, {}",first,last);
}
- 使用结构分解值
- let Point{x,y} = Point(x: 0,y: 7)
- 匹配守卫(match guard)是附加在match分支模式后的if条件语句,分支中的模式只有在该条件被同时满足时才能匹配成功。
let num = Some(4);
match num{
Some(x) if x<5 => println!("less than five: {}",x);
Some(x) => println!("{}",x),
None => (),
}
- @绑定:@运算符允许我们测试一个值是否匹配模式的同时创建存储该值的变量。
第十九章 高级特性
- 不安全Rust:舍弃Rust的某些安全保障并负责手动维护相关规则。
- 使用关键字unsafe来切换到不安全模式,并在被标记后的代码块中使用不安全代码。不安全超能力包括
- 解引用裸指针(raw pointer)。裸指针与引用、智能指针的区别在于
- 允许忽略借用规则,可以同时拥有指向同一内存地址的可变和不可变指针,或者拥有指向同一个地址的多个可变指针。
- 不能保证自己总是指向了有效的内存地址。
- 允许为空
- 没有实现任何自动清理机制
let mut num = 5;
// 可以在安全代码内创建裸指针,但不能在不安全代码块外解引用裸指针。
let r1 = &num as *const i32; // 不可变裸指针
let r2 = &mut num as *mut i32; // 可变裸指针
unsafe {
println!("r1 is : {}", *r1);
println!("r2 is : {}", *r2);
}
- 调用不安全的函数或方法。
- 创建不安全代码的安全抽象:将不安全代码封装在安全函数中是一种十分常见的抽象。
- 使用extern函数调用外部代码。有时Rust可能需要和另一种语言编写的代码进行交互。Rust为此提供了extern关键字来简化创建和使用外部函数接口(FFI)的过程。
unsafe{
// 必须在unsafe块中调用unsafe函数
dangerous();
}
unsafe fn dangerous(){} // 定义unsafe函数
// 引用C标准库代码
// "C"指明了外部函数使用的应用二进制接口ABI:它被用来定义函数在汇编层面的调用方式。
extern "C"{
fn abs(input:i32)->i32;
}
unsafe{
println!("-3 abs :{}",abs(-3));
}
- 访问或修改可变的静态变量。
- 静态变量和常量的区别:静态变量的值在内存中拥有固定的地址,使用它的值总是会访问到同样的数据。与之相反,常量则允许在任何被使用到的时候复制其数据。静态变量是可变的,访问和修改可变的静态变量是不安全的。
- 实现不安全trait。
- 当某个trait中存在至少一个方法拥有编译器无法校验的不安全因素时,称这个trait是不安全的。可以在trait前加上unsafe关键字来声明一个不安全trait,同时该trait也只能在unsafe代码块中实现。
- unsafe关键字并不会关闭借用检查器或禁用任何其他Rust安全检查:如果你在不安全代码中使用引用,那么该引用依然会被检查
- 高级trait:关联类型、默认类型参数、完全限定语法、超trait(supertrait),以及与trait相关的newtype模式
- 关联类型(associated type)是trait中的类型占位符,它可以被用于trait的方法签名中
pub trait Iterator{
type Item; // 关联类型
fn next(&mut self) -> Option<Self::Item>
}
- 默认泛型参数和运算符重载
trait Add<RP = Self> {
type Output;
fn add(self, param: RP) -> Self::Output;
}
struct Point {
x: i32,
y: i32,
}
// 使用范型默认类型
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
struct Millmeters(u32);
struct Meters(u32);
impl Add<Meters> for Millmeters {
type Output = Millmeters;
fn add(self, other: Meters) -> Millmeters {
Millmeters(self.0 + (other.0 * 1000))
}
}
- 完全限定语法:
<Type as Trait>::function(receiver_if_method, next_arg, ...)
- supertrait: trait OutlinePrint: fmt::Display{}
- 使用newtype模式在外部类型上实现外部trait。
use std::fmt;
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper{
fn fmt(&self,f: &mut fmt::Formatter)->fmt::Result{
write!(f,"[{}]",self.0.join(", "))
}
}
- 高级类型:
- 类型别名:type Kilometers = i32;
- 永不返回的Never类型(!):fn bar() -> ! {}
- 动态大小类型DST和Sized trait
- Rust 使用动态大小类型的通用方式:它们会附带一些额外的元数据来存储动态信息的大小
- 为了处理动态大小类型,Rust还提供了一个特殊的Sized trait来确定一个类型的大小在编译时是否可知。编译时可计算出大小的类型会自动实现这一trait。
- 高级函数和闭包:函数指针与返回闭包。
- 函数指针fn(function pointer):函数在传递过程中被强制转换成fn类型。
- 使用trait对象返回闭包。
fn return_closure() -> Box<dyn Fn(i32)>{
Box::new(|x| x+1)
}
- 宏:在编译期生成更多代码的方法
- 宏的种类
- 声明宏:marco_rules!
- 用于结构体或枚举的自定义#[derive]宏,它可以指定随derive属性自动添加的代码。
- 用于任意条目添加自定义属性的属性宏
- 看起来类似于函数的函数宏,它可以接收并处理一段标记(token)序列。
- 宏与函数的区别
- 宏是一种用于编写其他代码的代码编写方式,也就是所谓的元编程范式。
- 元编程可以极大程度地减少需要编写和维护的代码数量
- 函数在定义签名时必须声明自己参数的个数和类型,而宏则能够处理可变数量的参数。
- 宏的定义要比函数定义复杂得多,因为你需要编写的是用于生成Rust代码的Rust代码。
- 当在某个文件中调用宏时,必须提前定义宏或将宏引入当前作用域中,而函数则可以在任意位置定义并在任意位置使用。