您现在的位置是:首页 >技术交流 >rust初级概念及部分操作网站首页技术交流
rust初级概念及部分操作
简介rust初级概念及部分操作
文章目录
下载rust: curl https://sh.rustup.rs -sSf | sh
更新rust: rustup update
删除rust: rustup self uninstall
查看本地文档: rustup doc
编译文件: rustc 源文件名,只适合简单的rust程序
使用cargo创建项目: cargo new 项目名
构建并运行: cargo run 构建: cargo build 为发布构建: cargo build --release
检查代码,确保能够通过编译,不会产生任何可执行文件: cargo check
参考《Rust权威指南》
1、变量与可变性
- 变量声明使用
let
关键字- 变量不可以声明在全局作用域
- 变量默认是不可变的
- 声明变量时添加
mut
关键字可以使变量可变
- 常量
- 绑定值之后是不可变的
- 不可以使用
mut
关键字,常量永远都是不可变的 - 声明常量时类型必须显式声明,使用
const
关键字 - 常量可以在任何作用域内声明,包括全局作用域
- 常量只能绑定到常量表达式上,无法绑定到函数的调用结果或只能在运行时才能计算出的值
- 不可以使用
- 在程序运行期间,常量在其声明的作用域中一直有效
- 常量命名应遵循
UPPER_SNAKE_CASE
命名规则
- 绑定值之后是不可变的
- shadowing
- 可以使用相同的名字声明新的变量,新的变量就会
shadow
之前声明的同名变量 - 使用
let
声明的同名新变量,类型可以与之前的变量不同 - 使用
let
声明的新变量也是不可变的 - 若不使用
let
关键字,那么重新给非mut的变量赋值会导致编译时的错误
- 可以使用相同的名字声明新的变量,新的变量就会
2、数据类型
2.1、标量类型
- 整数
- 有符号整数范围
-(2^n-1)~2^(n-1)-1
- 无符号整数范围
0~(2^n)-1
isize/usize
类型的位数由程序运行的计算机架构所决定-
长度 有符号 无符号 8bit i8 u8 32bit i32(默认) u32 16bit i16 u16 64bit i64 u64 128bit i128 u128 arch isize usize - 除byte类型外,所有的数值字面值都允许使用类型后缀,如
21u8
,代表u8
类型的21
-
字面值 例子 十进制 100_000 十六进制 0xcc 八进制 0o56 二进制 0b1010_0101 byte(u8) b’A’ - 整数溢出:如
u8
的范围是0~255
,倘若将其设为256
- 在调试模式下,rust会检查整数溢出,若溢出则
panic
- 在发布模式下,rust不会检查整数溢出,若发生溢出,rust则会执行
环绕操作
,即256
变为0
- 在调试模式下,rust会检查整数溢出,若溢出则
- 有符号整数范围
- 浮点
- f32,单精度
- f64,双精度(默认)
- 字符(char)
- 是unicode标量值
- 字面值使用单引号
- 占用4个字节
- bool,占用一个字节
2.2、复合类型
- 元组(tuple)
- 将多个类型的多个值放在一个类型中
- 长度固定,一旦声明就不能更改
let t:(i32,&str,char) = (500,"zhangsan",'A');
- 获取值
- 使用模式匹配来解构一个tuple获取元素值
let (x,y,z) = t;
let a = t.0;
- 使用模式匹配来解构一个tuple获取元素值
- 数组
- 将同一类型的多个值放在一个类型里
- 长度固定
let arr:[i32;3] = [1,2,4];
let arr = [5;3];
相当于let arr = [5,5,5];
println!("{}",arr[0]);
- 想要将数据放在stack上而不是heap上以及保证有固定数量的元素时,可以使用数组
3、函数
- 声明函数使用
fn
关键字 - 命名遵循
snake case
命名规范,所有字母都是小写,字母之间用下划线隔开 - 函数的签名中,必须声明每个参数的类型
-
fn product(id:i32,name:&str) {}
- 函数体中的语句和表达式
- 函数体由一系列的语句组成,可选一个表达式结束
- 语句不返回值,所以不可以使用let将一个语句赋值给另外一个变量
- 语句是执行一些动作的指令
- 不带分号的是表达式
- 函数的返回值
- 在
->
后面声明函数的返回值类型,返回值不可命名 - 返回值就是函数体内最后一个表达式的值
- 若想提前返回需要使用
return
关键字,并指定一个值,大多数函数都是默认使用最后一个表达式作为返回值
- 在
4、控制流
4.1、if else
if
表达式允许根据条件(必须是bool类型)执行不同的代码分支- 与条件相关联的代码块就叫做分支
- 若使用超过一个的
else if
,应用match
重构代码 - 因为
if
是一个表达式,所以可以将其放在let语句中等号左边的位置let num = if true {4} else {5};
4.2、循环
- loop,死循环
fn main() {
let mut num = 0;
let res = loop {
num +=2;
if num == 10 {
break num * num;
}
};
println!("{}",res);
}
- while
- 每次执行循环体之前都要判断一次条件
fn main() {
let mut num = 5;
while num != 0 {
println!("{}", num);
num -= 1;
}
println!("end")
}
- for
fn main() {
let arr = [1, 2, 4, 56, 7];
for n in arr.iter() {
println!("{}", n);
}
for num in (1..4).rev() {
println!("{}",num);
}
}
5、所有权
5.1、stack与heap
- 所有程序在运行时都必须管理它们使用计算机内存的方式
- 内存是通过一个所有权系统来管理的,其中包含一组编译器在编译时检查的规则
- 当程序运行时,所有权特性不会减慢程序的运行速度
- 一个值是在stack或者heap上对语言的行为和要做的决定是有很大影响的
- 栈和堆
- stack
- 按照值的接收顺序来存储,按照相反的顺序将它们取出,即
先进后出
- 添加数据叫做压栈
- 移除数据叫做弹出栈
- 所有存储在stack上的数据必须拥有已知固定的大小
- 编译时未知大小的数据和运行时大小可能发生变化的数据必须放在heap上
- 按照值的接收顺序来存储,按照相反的顺序将它们取出,即
- heap
- 当把数据放在heap中时,会申请一定数量的内存空间
- 操作系统在heap中找到一块足够大的空间,将其标记为在用,并返回一个指针,也就是这个空间的地址,也叫做
分配
- 指针是已知固定大小数据,可以将其存储到stack上,但想要得到具体的数据就必须使用指针来定位
- stack
- 函数调用
- 当代码调用函数时,值被传入到函数(包括指向heap的指针),函数本地的变量被压到stack上。当函数结束后,这些值会从stack上弹出
5.2、所有权规则
- 每个值都有一个变量,这个变量是该值的所有者
- 每个值同时只能有一个所有者
- 当所有者超出作用域时,该值将被删除
5.3、内存与分配
- 对于某个值来说,当拥有他的变量走出作用范围时,内存会立即自动的交还给操作系统
- 变量和数据的交互方式
- 移动(move)
-
let s1 = String::from("zhangsan"); // 失效 let s2 = s1;
- 当s1的值赋值给s2时,String的数据被复制了一份
- 在stack上复制了一份指针、长度、容量,并没有复制指针指向的heap上的数据
- 当变量离开作用域时,rust会自动调用
drop
函数,并将变量使用的heap内存释放 - 当s1,s2离开作用域时,它们都会尝试释放相同的内存,就会出现
二次释放
这个bug- 为了保证安全,rust没有尝试复制被分配的内存
- rust会让s1失效,当s1离开作用域的时候,rust不必释放任何东西
- rust不会自动创建数据的深拷贝
-
- 克隆(clone)
- 若想对heap上的String数据进行深度拷贝,而不是仅拷贝stack上的数据,就需要使用clone方法
-
let s1 = String::from("zhangsan"); let s2 = s1.clone(); println!("{},{}",s1,s2);
- 复制
- copy trait:可以用于像整数这样完全存放在stack上的类型
- 若一个类型实现了copy的trait,那么旧的变量在赋值后仍然可用
- 若一个类型或者类型的一部分实现了drop trait,那么就不允许让其去实现copy trait了
- 拥有copy trait的类型
- 简单的标量类型以及其组合类型都是可以copy的
- 整数、bool、浮点、char、元组(其所有字段都是可以copy的)
- 任何需要分配内存或者某种资源的类型都不可以copy
- 简单的标量类型以及其组合类型都是可以copy的
- 移动(move)
5.4、所有权与函数
- 将值传递给函数和将值赋值给变量是类似的
- 将值传递给函数将发生移动或复制
- 函数在返回的过程中同样也会发生所有权的转移
- 将一个值赋值给其他变量时就会发生移动
- 当一个包含heap数据的变量离开作用域时,他的值就会被drop函数清理,除非数据的所有权移动到另外一个变量上了
5.5、引用和借用
- &表示引用,允许引用某些值而不取得其所有权
- 将引用作为函数参数的行为叫做借用
- 不可以修改借用的变量,因为引用默认也是不可变的
- 可变引用的限制:在特定的作用域内,对某一块数据,只能有一个可变的引用,目的是为了在编译时防止数据竞争
-
fn main() { let mut s = String::from("rust"); let size = get_size(&mut s); println!("{}",size); } fn get_size(s:&mut String) -> usize { s.push_str(" gogogog!"); s.len() }
-
- 可以通过创建新的作用域,来允许非同时的创建多个可变引用
-
let mut s = String::from("rust"); { let s3 = &mut s; println!("{}", s3); } let s1 = &mut s; // let s2 = &mut s; // error println!("{}", s1);
-
- 不可以同时拥有一个可变引用和不可变引用
- 悬存引用(dangling references)
- 一个指针引用了内存中某个地址,而这块内存可能已经释放并分配给其它变量使用了
5.6、切片
- 不持有所有权
- 字符串切片(
&str
)是指向字符串中一部分内容的引用- 格式:
[开始索引..结束索引]
,前包含后不包含 -
fn main() { let s = String::from("hello rust"); let b = &s[..5]; // hello let e = &s[6..]; // rust let a = &s[..]; // hello rust println!("{},{},{}", b, e, a); }
- 格式:
- 字符串切片的范围索引必须发生在有效的UTF8字符边界内
- 若尝试从一个多字节的字符中创建字符串切片,程序会报错并退出
- 字符串字面值就是切片,被直接存储在二进制程序中
- 定义函数时使用字符串切片代替字符串引用作为参数会使api更加通用,且不会损失任何功能
6、struct
- 定义
- 使用
struct
关键字,并为其命名 - 在
{}
内为所有字段定义类型和名称 -
fn main() { struct User { id:i32, name:String, age:u8, } }
- 使用
- 实例化
- 为每个字段指定具体的值
- 无需按照声明的顺序进行指定
- 当字段名与字段值对应变量名相同时,就可以使用字段初始化的简写方式
-
fn main() { let mut u = User { id:1, name:String::from("zhangsan"), age:18, }; } fn get_info(id:i32,name:String,age:u8) -> User { User { id, name, age, } }
- 取值
println!("{}",u.name);
- 一旦struct实例是可变的,那么实例中的所有字段都是可变的
- 更新
-
let u2 = User { id: 2, name: String::from("lisi"), ..u // 相同的值可以用此表达式代替 };
-
- tuple struct
- 定义类似tuple的struct,tuple struct整体有名字,但是里面的元素没有名字
strcut User(i8,i32,i64);
- 空结构体
- 没有任何字段的struct,叫做unit-like struct
- 适用于在某个类型上实现某个trait,但是又没有想要存储的数据
- 方法
- 方法和函数类似,都具有fn关键字、名称、参数、返回值
- 不同
- 方法是在struct(enum/trait对象)的上下文中定义的
- 第一个参数是self,表示方法被调用的struct实例
- 第一个参数可以是
&self
也可以获得其所有权或可变借用
- 第一个参数可以是
-
#[derive(Debug)] // 调试模式 struct Oblong { width: u32, length: u32, } impl Oblong { // 定义方法 fn area(&self) -> u32 { self.length * self.width } } fn main() { let o = Oblong { width: 34, length: 56, }; println!("{}", o.area()); println!("{:#?}", o); // 打印结构体 }
- 关联函数(如:
String::from
)-
impl Oblong { fn square(size: u32) -> Oblong { Oblong { width: size, length: size, } } } fn main() { let oo = Oblong::square(14); println!("{:#?},{}", oo,oo.area()) }
-
7、枚举与匹配
7.1、枚举
- 允许列举所有可能的值来定义一个类型
// 创建枚举值
enum IpAddrKind {
V4,
V6
}
let ip_addr = IpAddrKind::V6; // 获取枚举值
- 将数据附加到枚举的变体中
- 不需要使用额外的struct
- 每个变体可以拥有不同的类型及关联的数据量
- 可以在枚举的变体中嵌入任何类型的数据
-
enum Country { China(String), USA(String), India(String) }
- 为枚举定义方法
impl Country {
fn province(&self) {}
}
fn main() {
let c = Country::China(String::from(""));
c.province();
}
- option枚举(类似nil概念的枚举)
- 定义于标准库中,且处于prelude(预导入模块)中
-
enum Option<T> { Some(T), None, } let res = Some(10); let res = Some("rust"); let res:Option<u8> = None;
7.2、match
- 必须穷举所有的可能
- 允许一个值于一系列模式进行匹配,并执行匹配到模式的对应代码逻辑
- 模式可以是字面值、通配符、变量名等
enum RMB {
fen,
jiao,
yuan(String),
}
fn price(r: RMB) -> u8 {
match r {
RMB::fen => 1,
RMB::jiao => 10,
RMB::yuan(goods) => {
println!("{}", goods); // 取值
100
},
}
}
fn price_ignore(r: RMB) -> u8 {
match r {
RMB::fen => 1,
_ => 0,
}
}
fn price_if_let(r: RMB) -> u8 {
if let RMB::fen = r {
println!("fen");
1
} else {
println!("nil");
0
}
}
fn main() {
println!("{}", price(RMB::yuan(String::from("apple"))));
}
- 绑定值的模式
- 匹配的分支可以绑定到被匹配对象的部分值
- if let
- 处理只关心一种匹配而忽略其他匹配的情况
8、package、crate、module
- 模块系统
package
:cargo的特性,可以构建、测试以及共享crate- 包含
Cargo.toml
,描述如何构建crates - 只能包含
1或0
个library crate - 可以有任何数量的binary crate
- 文件可以放在
src/bin
,每个文件是单独的binary crate
- 文件可以放在
- 必须包含至少一个crate(library|binary)
- 包含
crate
:是一个模块树,可以产生一个library或可执行文件- 类型:binary、library
- crate Root:源代码文件
- rust编译器从此处开始,组成crate的根module
- 将相关功能组合到一个作用域内,便于在项目内进行共享,防止冲突
module
、use:控制代码的组织、作用域以及私有路径- 在一个crate中将代码分组
- 增加可读性,易于复用
- 控制项目的私有性。public、private
- 创建module
-
mod father{ mod son { fn eat() {} } mod girl{ fn sleep() {} } }
-
- 私有边界
- 若想把函数或struct等设为私有,可以将其放入模块中
- rust中所有的条目默认都是私有的
- 父模块无法访问子模块中的私有条目
- 子模块可以访问所有祖先模块中的条目
- 使用
pub
关键字可以将某些条目标记为公共的
path
:为struct、function、module等条目命名的方式- 两种形式路径
- 绝对路径:从crate开始,使用crate名或字面值code
- 相对路径:从当前模块开始,使用self、super或当前模块的标识符
- 路径至少由一个标识符组成,标识符之间用
::
隔开
- super:父级
- pub struct
- struct是公共的
- struct的字段默认是私有的,字段需要单独设置pub来变为公共的
- pub enum
- enum是公共的,其所有字段也都是公共的
- 两种形式路径
cargo的惯例
src/main.rs
- binary crate的crate Root
- crate名与package名相同
src/lib.rs
- crate名与package名相同
- package包含一个crate
- library crate的crate root
- use
- 使用
use
关键字将路径引入到作用域内,仍然遵循私有性规则 - 习惯用法
- fn:将函数的父级模块引入作用域(制定到父级)
- struct,enum:指定完整的路径(指定到自身)
- 不同模块的同名条目,需要引入到父级
- as:为引入的路径指定本地的别名
- pub use:重导出
- 将条目引入作用域,该条目可以将外部代码引入到其作用域
- 嵌套路径
路径相同的部分::{路径不同的部分}
-
use std::cmp::Ordering; use std::io; use std::io::Write; use std::{io,cmp::Ordering}; use std::io::{self,Write};
- 使用
9、常用的集合(堆内存)
9.1、Vector
- 格式:
Vec<T>
,由标准库提供,可以存储多个值- 只能存储相同类型的数据
- 值在内存中连续存放
- 创建:
let mut v:Vec<i32> = Vec::new();
- 使用初始值穿件Vec:
let v = vec![1,23,45];
- 更新
- 添加元素,push方法
v.push(100);
- 删除,当其离开作用域之后,就会被删除,其中所有的元素也会被删除
- 添加元素,push方法
- 读取
- 索引
- get
-
fn main() { let v = vec![1,2,46,8]; let t = &v[0]; println!("the get number is {}",t); mathc v.get(1) { Some(t) => println!("the get number is {}",t), None => println!("none"), } }
9.2、String
- 在语言核心层面,只有一个字符串类型
&str
- 是对存储在其它地方,UTF8编码的字符串引用
- 字符串字面值存储在二进制文件中,也是字符串切片
- String来自标准库而不是语言核心层面
- 其可以增长、可以修改、可以获得所有权,采用UTF8编码
- 在heap上分配。能够存储在编译时位置数量的文本
- 创建String类型的值
- 使用form函数从字符串字面值创建出string类型
let s = String::from("rust");
,::
表示from是string类型下的函数let s = "rust".to_string();
,创建String
- 更新
push_str()
,将一个字符串切片附加到Stringlet mut s = String::from("rust"); s.push_str("gogogo");
push
,将单个字符附加到String+
,拼接字符串format!()
,拼接字符串
- String是对
Vec<u8>
的包装 - 遍历
- 对于标量值:
for s in ss.chars() {}
- 对于字节:
for s in ss.bytes() {}
- 对于标量值:
9.3、Hashmap
use std::collections::HashMap; // 导入
// 格式:`HashMap<K,V>`,键值对的形式存储数据,所有的k/v类型必须相同
fn main() {
let mut h:HashMap<String,u32> = HashMap::new(); // 创建空hashmap
h.insert(String::from("zhangsan"),18);// 添加数据,若key已存在则用新v覆盖旧v
h.entry(String::from("zhangsan")).or_insert(666); // 添加数据,key已存在不做任何操作,不存在则插入新的v
let name = vec![String::from("zhangsan"),String::from("lisi")];
let age = vec![19,29];
let person:HashMap<_,_> = name.iter().zip(age.iter()).collect(); // 创建hashmap
let n = String::from("zhangsan");
match person.get(&n) { // 获取值
Some(age) => println!("{}",age),
None => println!("nil"),
}
}
- 对于实现了copy trait的类型(如标量类型),值会被复制到hashmap中
- 对于拥有所有权的值(如String),值会被移动,所有权会交给hashmap
- 若将值的引用传递给hashmap,值本身就不会移动,在hashmap有效的期间内,被引用的值必须保证有效
10、错误处理
- 可恢复的错误:
Result<T,E>
use std::fs::File;
fn main() {
let f = File::open("a.txt");
let file = match f {
Ok(file) => file,
Err(error) => panic!("{}", error),
};
let f = File::open("a.txt").unwrap(); // 类型match,不可自定义错误信息
let f = File::open("a.txt").expect("not found"); // 同上,可以自定义错误描述
read()
}
// ?传播错误的一种方式,只能用于返回Result的函数
// 若Result是OK,ok中的值就是表达式的值,然后继续执行程序
// 若Result是Err,Err就是整个函数的返回值,就像使用了return
fn read() -> Result<String, io::Error> {
let mut s = String::new();
File::open("b.txt")?.read_to_string(&mut s)?;
Ok(s)
}
- 不可恢复的错误:
panic!
- 打印错误信息
- 展开、清理调用栈
- 退出程序
- 当panic发生时
- 展开(unwind)操作(默认)
- rust沿着调用路线往回找,清理每个遇到的函数中的数据
- 中止(abort)操作
- 不进行清理,直接中止
- 内存需要OS定期清理
- 设置,在Cargo.toml文件中添加
[profile.release] panic='abort'
- 展开(unwind)操作(默认)
11、泛型、trait、生命周期
11.1、泛型
- 具体类型或其他属性的抽象代替,相当于类型的占位符,编译器在编译时,会将占位符替换为具体的类型
fn func_name<T>(field:T) -> T {}
:函数中定义泛型struct struct_name<T> {f1:T,f2:T}
:结构体中定义泛型- 为struct或enum实现方法的时候,可以在定义中使用泛型
- 将T放在impl后,表示在类型T上实现方法
impl<T> struct_name<T>
- 只针对具体类型实现方法
impl struct_name<u32>
- 将T放在impl后,表示在类型T上实现方法
11.2、trait
- 抽象的形式定义共享行为
- 定义
- 将方法签名放在一起,来定义实现某种目的的所必须的一组行为
-
// 只有方法签名,没有具体实现 // 可以有多个方法,每个方法签名占一行,以:结尾 // 实现该trait的类型必须提供具体的方法实现 pub trait Person { fn get_age(&self) -> String; fn get_name(&self) -> String { String::from("get_name") } fn get_info(&self) -> String { format!("{}:{}",self.get_age(),self.get_name()) } } pub struct Zhangsan { pub name: String, pub age: u8, } impl Person for Zhangsan { fn get_info(&self) -> String { format!("{}:{}", self.name, self.age) } }
- trait作为参数
fn func_name(t:impl Person) {}
- trait bounds(约束):泛型类型参数指定为实现了特定行为的类型
fn func_name<T:Person>(item:T) {}
- 使用+指定多个trait bound
fn func_name<T:Person+Display>(item:T) {}
- 使用where子句,在方法签名后指定where子句
fn func_name<T>(item:T) where T:Person+Display {}
- trait作为返回类型
fn func_name() -> impl Person {}
- 只能返回一种类型,当返回的类型可能不同时就会报错
11.3、生命周期
- 避免悬垂引用
- 生命周期标注语法
- 生命周期的标注,不会改变引用的生命周期长度
- 当制定了泛型生命周期参数,函数可以接收带有任何生命周期的引用
- 描述多个引用的生命周期的关系,不会影响生命周期
- 参数名
- 以
'
开头,通常是'a
- 以
- 标注的位置
- 在引用
&
之后,使用空格将标注和引用类型分开
- 在引用
fn fn_name<'a>(x:&'a str,y:&'a mut str) -> &'a str {}
- 生命周期
'a
的生命周期实际是:x和y两个生命周期中较小的那个
- 生命周期
- 从函数返回引用时,返回类型的生命周期参数需要与其中一个参数的生命周期匹配
- 若返回的引用未指向任何参数,就只能引用函数内创建的值
- 该值在函数结束时就走出了作用域,这就是悬垂引用
- 生命周期省略的三个规则
- 每个引用类型的参数都有自己的生命周期
- 若只有一个输入生命周期参数,那么该生命周期就被赋值给所有的输出声明周期
- 若有多个生命周期参数,但其中一个是&self或&mut self,那么self的生命周期会被赋值给所有的输出生命周期参数
12.1、编写自动化测试
- 函数体上一行添加
#[test]
即可变为测试函数 - 使用
assert!
检查测试结果,true为测试通过,false会调用panic!测试失败 - 使用
assert_eq!
与assert.ne!
测试是否相等- 断言失败,会输出两个参数的值
- 自定义错误信息
assert!(false,"error message"); assert_eq!(1,2,"not equal");
- 添加
#[should_panic(expected="")]
,验证错误处理的情况 - 控制测试如何运行
- 改变Cargo test的行为:添加参数
- 默认行为
- 并行运行所有测试
- 不显示所有输出,使读取与测试结果相关的输出更容易
- 直接跟在
cargo test
后的参数,如cargo test --help
- 在
cargo test
后跟--
的参数,如cargo test -- --help
,针对二进制文件 --test-threads
,控制线程数--show-output
,在成功的情况下也输出打印的内容
- 默认行为
- 改变Cargo test的行为:添加参数
- 按照测试名称运行测试
- 运行测试的子集
- 选择运行的测试,将测试的名称作为cargo test的参数
- 运行单个测试,直接指定测试名
- 运行多个测试,指定测试名(模块名)的一部分
- 运行测试的子集
- 忽略测试
- 使用
#[ignore]
标记 - 单独运行该测试
cargo test -- --ignored
- 使用
- 测试分类
- 单元测试
- 一次对一个模块进行隔离的测试
- 可以测试private接口
#[cfg(test)]
标注
- 集成测试,单独的tests包,每一个测试文件都是一个单独的crate
- 只能测试public接口
- 可能在每个测试中使用多个模块
- 在库外部,可以像其它外部代码一样使用代码
- 运行指定的集成测试
- 运行一个指定的集成测试
cargo test 函数名
- 运行某个测试文件内的所有测试
cargo test --test 文件名
- 运行一个指定的集成测试
- 单元测试
14、迭代器和闭包
14.1、迭代器
- iterator trait仅要求实现一个方法,
next()
- next方法
- 每次返回迭代器中的一项
- 返回结果包裹在Some中
- 迭代结束返回None
- next方法
- 迭代方法
- iter():在不可变引用上创建迭代器
- into_iter():创建的迭代器会获取所有权
- iter_mut():迭代可变的引用
14.2、闭包
- 可以捕获其所在环境的匿名函数
- 定义:
let closure = |num:u8| {println!("{}",num);};
- 闭包的类型推断
- 不要求标注参数和返回值的类型
- 闭包通常很小,只在狭小的上下文中工作,编译器可以自动推断类型
- 也可以手动添加类型标注
- 使用闭包捕获环境
- filter方法
- 接收一个闭包
- 此闭包在遍历迭代器的每个元素时,返回bool
- 若闭包返回true,当前元素将会包含在filter产生的迭代器中
- 若返回false,当前元素将不会包含在filter产生的迭代器中
- filter方法
15、cargo、crate
- 发布配置(release profile)
- 是预定义的
- 可以自定义,可使用不同的配置对代码编译拥有更多的控制
- cargo主要的两个发布配置
- dev profile:适用于开发,cargo build
- release profile:适用于发布,cargo build --release
- 文档
- 使用
//!
编写抬头 - 使用
///
格式,支持markdown语法 cargo doc --open
生成文档并打开
- 使用
16、智能指针
- 智能指针
- 行为和指针类似
- 有额外的元数据和功能
- 引用技术智能指针类型
- 通过记录所有者的数量,使一份数据被多个所有者同时持有
- 在没有任何调用者的时候自动清理
- 智能指针实现
- 通常使用struct实,并且实现
Deref
和Drop
两个trait - Deref trait:允许智能指针struct的实例像引用一样使用,任何类型都可以实现该trait,需要实现其deref方法即可
- 实现defer trait使我们可以自定义解引用运算符*的行为
- 通过实现deref,只能指针可以像常规引用一样来处理
- 解引用运算符*
- 常规的引用是一个指针
- 函数和方法的隐式解引用转化(deref coercion)
- 隐式解引用转化是为函数和方法提供一种便捷的特性
- 当把某类型的引用传递给函数或方法时,但它的类型与定义的参数类型不匹配,
deref coercion
就会自动发生
- Drop trait:允许自定义当智能指针实例走出作用域时的运行的代码,任何类型都可以实现该trait,需要实现其drop方法即可
- 可以自定义当值将要离开作用域时发生的动作
- 不允许手动调用drop方法
- 可以调用标准库
std::mem::drop
函数,来提前drop值
- 通常使用struct实,并且实现
- 常见智能指针
- Box:在heap内存上分配值
- 允许在heap上存储数据
- stack上存储的是指向heap数据的指针
- 没有性能开销,没有其他额外功能
- 使用场景
- 编译时无法确定某类型的大小,但在使用该类型时,上下文却需要知道其确定的大小
- 有大量数据时,想要移交所有权,但需要确保在操作时数据不会被复制,只移交所有权
- 使用某个值时,只关心其是否实现了特定的trait,而不关心其具体类型
- Rc:启用多重所有权的引用计数类型
- 通过不可变引用,使程序不同部分之间共享只读数据
- 支持多重所有权
- 使用引用计数,追踪到所有值的引用,当为0个引用时,该值就会被清理掉
- 使用场景
- 需要在heap上分配数据,这些数据被程序的多个部分独缺,但在编译时无法确定那个部分最后使用完这些数据
- 只适用于单线程的场景
- 不在预导入模块
std::rc::Rc
Rc::clone(&a)
,增加引用计数RC::Strong_count(&a)
,获得引用计数
- Ref与RefMut,通过RefCell访问,在运行时而不是编译时强制借用规则的类型
- Box:在heap内存上分配值
- 内部可变模式(interior mutability pattern):不可变类型暴露出可修改其内部值的api
- 引用循环(reference cycles):如何泄露内存以及如何防止其发生
17、并发
- 在大部分OS中,代码运行在进程中,OS同时管理多个process
- 在编写的程序中,各个独立的部分可以同时运行,运行这些独立的部分就是线程(thread)
- 可能导致的问题
- 竞争状态,线程以不一致的顺序访问数据或资源
- 死锁,两个线程彼此等待使用完所持有的资源,线程无法继续
- 实现线程的方式
- 通过调用OS的api创建线程,1:1模型,需要比较小的运行时(标准库提供)
- 语言自己实现的线程,M:N模型
- 创建线程
thread::spawn
- 参数是一个闭包,也就是在新线程中运行的代码
- 通过join handle等待所有线程完成
- thread::spawn的返回值类型是joinhandle,其持有值的所有权
- 调用join方法,可以等待对应的线程完成执行
- thread::spawn的返回值类型是joinhandle,其持有值的所有权
- move闭包通常和thread::spawn一起使用,它允许使用其他线程的数据
- 创建线程时,将一个线程的所有权转移到另外一个线程上
- 消息传递
- channel
- 调用发送端的方法接收数据
- 接收端会检查和接收到达的数据
- 若发送端或接收端中任何一端被丢弃,channel就会被关闭
- 创建
- mpsc::channel来创建
- send方法
- 参数是想要发送的数据
- 返回Result<T,E>,若有问题就返回一个错误
- recv方法
- 阻塞当前进程,直到接收到send发送过来的值,一旦接收到值就返回Result<T,E>
- 当发送端关闭,会收到一个错误
- try_recv,不会阻塞当前线程的执行
- mutex,每次只允许一个线程访问数据
- 线程必须先获得mutex,lock可以跟踪谁对数据持有独占访问权
- 在使用数据之前必须先获得lock
- 使用完mutex保护的数据,必须对数据进行解锁,以便其他线程可以获得锁
- 通过
Mutex::new(data)
来创建mutex
- channel
18、模式匹配
- 模式组成
- 字面值
- 解构的数组、enum、struct和tuple
- 变量
- 通配符
- 占位符
- 想要使用模式,需要将其与某个值比较,若匹配就可以在代码中使用这个值的相应部分
- 特殊的模式
_
,会匹配到任何东西,不会绑定到变量,通常用于match的最后一个arm,或用于忽略某些值
- 特殊的模式
- if let表达式主要是作为一种简短的方式来等价的代替只有一个匹配项的match
- 不会检查穷尽性
- while let只要模式继续满足匹配到的条件,while循环就会一直运行
- for循环中,模式就是紧跟for关键字之后的值
- match
- 使用
|
语法,可以匹配多种模式 - 使用
..
匹配某个范围的值 - 使用模式解构struct、enum、tuple,从而引用这些类型值的不同部分
- 使用
fn main() {
let x = 1;
match x {
1|2 => println!("ok"),
3..=5 => println!("3,4,5"),
'a'..='c' => println!("a,b,c"),
_ => println!(“error”),
}
}
19、高级特性
- unsafe
- 使用unsafe关键字切换到unsafe rust,开启一个块,其中存放unsafe代码
- 可执行的动作
- 解引用原始指针
- 可变的
*mut T
,*
是类型的一部分 - 不可变的
*const T
,指针在解引用之后不能直接对其进行赋值 - 允许通过同时具有不可变和可变指针或多个指向同一位置的可变指针来忽略借用规则
- 无法保证能指向合理的内存
- 允许为null
- 不会实现自动清理
- 可变的
- 调用unsafe函数或方法
- 访问或修改可变的静态变量
- 静态变量
- 声明时必须标注类型
- 静态变量只能存储在’static生命周期的引用,无需显式标注
- 访问不可变静态变量是安全的
- 静态变量
- 实现unsafe trait
- 解引用原始指针
- 高级trait
- 关联类型时trait中的类型占位符,可以用于trait的方法签名中
-
trait Iterator { type Item; fn next(&mut self) -> Option<self::Item>; }
- 高级类型
- 类型别名
type Name = String;
- Never类型,使用
!
表示,其在不返回的函数中充当返回类型fn get_name() -> !{}
- 类型别名
- 宏
- 指的是一组相关特性的集合称谓
- 使用macro_rules!构建的声明宏
- 过程宏,宏定义必须放在其自己的包中,并使用特殊的包类型
- 自定义#[derive]宏,用于struct或enum,可以为其指定随derive属性添加的代码
- 类似属性的宏,在任何条目上添加自定义属性
- 类似函数的宏,看起来像函数调用,对其指定为参数的token进行操作
- 指的是一组相关特性的集合称谓
- 函数与宏的区别
- 宏是用来编写可以生成其他代码的代码
- 函数在定义签名时,必须要声明参数的个数和类型,宏可以处理可变的参数
- 编译器会在解释代码前展开宏
- 宏的定义比函数复杂
- 在某个文件调用宏时,必须提前定义宏或者将宏引入当前作用域
- 函数可以在任何位置定义并在任何位置使用
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。