"完美之星----Rust"学习 进阶章节
开篇
1.说句实话,Rust和我以往学习的语言有很大不同。在刚开始学习时还没有意识到。
2.之前的新语言学习(Java、C#、python、...)路径基本上是遵守“这个语法在C/C++中对应的是哪一个”来学习的,基本可以一周搞定,但是Rust几乎撼动了所有学习路径。Rust绝对不是C/C++的模仿者,或大型C/C++语法糖,其应与C/C++并列至系统级开发语言。这几乎是一种全新的编程范式。
3.本博客将继续官方文档“Rust程序设计语言”未完成的学习,但并非终章,而是一部分,一共计划分3章(基础、进阶、终章)完成学习。
包、Crates、模块
1.crate/模块:
编译时一个最小可独立编译单元被视作一个crate,分为两种:
(1)二进制crate:类似于可执行程序,要求必须有main函数。如果一个包下面有多个.rs含有main函数,代表多个二进制crate,编译时会分别生成多个二进制crate。
(2)库crate:类似于library(简称常用代表),没有main函数。
此外,约定src/main.rs是二进制crate的根。src/lib.rs是库crate的根。
如果有多个二进制crate,可以将其它crate文件放在src/bin目录下。
(3)创建纯库项目:cargo new project_name --lib
2.包:package
1至多个crate的捆绑+cargo.toml文件。
3.模块:一个最小业务单元,负责完成某个主题任务
模块声明:
mod module_name; //声明出被调用单元
pub mod module_name; //声明公共的被调用单元,这个关键字表明外部是否可调用该模块中的函数于功能。
声明后,rust会在如下位置寻找代码:
(1)内联,在当前文件内部寻找
(2)src/tmp_mod_name/mod_name.rs
(3)src/tmp_mod_name/mod_name/mod.rs
其中,tmp_mod_name是当前的模块的名字,mod_name是内调用模块名字
如果是根(main.rs/lib.rs)则路径中省略tmp_mod_name。
模块定义:
mod mod_name {
//work code ...
}
模块嵌套定义:
mod mod_name {
mod sub_mod1 {
//work code1 ...
}
mod sub_mod2 {
//work code2 ...
}
}
注意,这里无论是使用mod声明另一个模块,还是内部定义了一个模块,这些模块都被称之为子模块。子模块无法通过声明单独获得,必须声明其父模块+use子模块获得。
为什么这么设计?
主要原因如下:
不可能存在脱离父模块单独存在的子模块,如果有,那说明这种模块可以单独存在,应该单独提出来声明,即其和父模块不是继承而是组合关系。否则,肯定代表子模块无法单独存在,那就必须先声明其父模块,然后才能使用子模块,保证调用逻辑。
这种行为相当于用物理文件存放位置规定实现对代码结构的逻辑约束,路径即架构。
4.use关键字:
use crate::mod1::mod2::mod3;
这样,此位置代码调用mod3中的函数不需要写mod1::mod2::前缀而只写mod3::前缀。
这里的这个路径可以写绝对或相对路径:
(1)绝对路径:crate::mod1::mod2::mod3::func(); //这里认为路径从src文件夹下开始
(2)相对路径:
self::mod1::mod2::mod3::func(); //从当前模块出发
super::mod1::mod2::mod3::func();//从当前模块的父模块出发
mod1::mod2::mod3::func(); //也是从当前模块出发(当前模块称为mod0),属于self的一种变体。
这里还要注意,模块加了pub关键字可供外部使用,但是其中函数仍为私有,如果需要外部调用除了模块需要添加pub,还需要函数和成员也需要pub关键字。
如果子模块分文件定义,则需要在父模块中声明中加上pub,否则需要在定义的位置加上pub
5.use ... as ... 为模块取别名:
use std::io::Result as IoResult;
let a = IoResult::Ok(2);
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
6.这里需要具体说一下,mod声明和use的区别:
(1)mod是声明一个模块,这个模块可以通过.rs文件挂载进来,或者代码中本身自带。搜索的可挂载路径是
1.src/tmp_mod_name/mod_name.rs
2.src/tmp_mod_name/mod_name/mod.rs
(2)use是对路径的一个缩写。
(3)pub是当前代码所在模块A的上层祖先模块(父,祖父,...)B在引入模块A后,对当前代码的可见性。
(4)pub use是当前定义的一个use对的上层祖先模块(父,祖父,...)B的可见性。
(5)可通过use mod1::*来引入mod1下所有pub内容。
(6)此外,子模块可以使用其祖先模块中的项,无论是否是pub
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
还有就是外部引入的库(非源码引入),已经通过Cargo.toml文件自动挂载进来,不需要再使用mod声明了,并且对于外部库自身而言,引入的起点是库名,而非self,super等,这种属于源码引入才会使用的方法。
此外,库名可以在Cargo.toml中找到,加入新的库则需要修改toml文件。
(6)将多个重复路径一起引用的方法:
use mod0::mod1::mod2::mod3;
use mod0::mod1::mod2::mod3_1::mod4;
use mod0::mod1;
这些可写成use mod0::mod1::{self, mod2::mod3, mod3_1::mod4};
关于Copy、move、clone的区别问题
1.首先,clone既可用于实现深拷贝,也可用于实现浅拷贝
2.Copy只能实现浅拷贝,并且组合类型在实现浅拷贝时要求内部类型都实现了浅拷贝Copy。
3.clone和Copy的实现逻辑必须相同
4.Copy和move都可被隐式调用,如果一个类型没有实现copy,那么隐式调用的就是move,否则是copy
5.实现了copy的禁止实现drop,因为drop的本质是数据move进drop函数,然后通过结束函数,结束作用域后自动释放。但是实现了copy的类型,会复制而非move进drop函数,会导致原数据未释放。
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
这里重写一下关于所有权的问题:
1.任何对象离开作用域其值就会被释放。
2.栈对象,其数据全部在栈上。堆对象,指针等基础类型放在栈上,存储数据放在堆上。
3.Copy trait只能用于浅拷贝,clone即可实现浅拷贝也可实现深拷贝。但是clone和copy逻辑需要相同(必须,没有商量余地),并且clone需要被显式调用,而所有权转移和copy不需要,可以隐式调用。
4.实现了copy就不能实现drop,因为如果实现了copy,极易出现多个对象的引用或指针指向相同堆内存,如果实现drop则必将导致double free,为防止这种情况发生,采用了硬编码规则,禁止任何对象尝试同时实现drop+copy。
5.对于所有对象而言,除非是借用行为,否则如果没有实现copy trait,=所代表的一定是所有权转移。如果实现了copy trait,则=代表栈数据复制。
6.区分“遮蔽”和“重新赋值”的区别,遮蔽不会销毁旧值,而重新赋值会导致旧值被立即drop(所谓的变量定义栈)
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
常见集合
这里的集合排除了数组和元组,因为这些基础结构的数据存在栈上。
现在讨论的集合结构的数据将保存在堆上。
(1)Vector存储列表:
1.vector中数据是相邻排列的。此外,如果想要动态插入/删除则需要mut类型
2.声明:let v : Vec
3.添加元素:vecor_ins.push(val);
4.元素读取:let res = vector_ins[index]; / let res : Option<&Type> = vector_ins.get(index);
这里仍旧需要注意一下所有权转移问题,无论vector的具体实例类型是什么,其数据全部会分配在堆上。那么当读取其值的时候需要确认,如果读取的变量如果是栈,则需确保实现了Copy trait来保证从堆到栈的复制。否则最好使用引用来使用。
此外,要注意vector.push是可变引用。读取时如果有其它引用会报错。
这里解释一下,为何rust的push为可变引用的高明之处
首先,有一点可确认,无论高级还是低级语言,对于动态增长的特性本质上是假特性。以C++为例,vector的内部动态增长是用预分配+溢出检测+1.5倍重分配完成的。这会导致一个问题,如果在分配前已经有一个指针/引用指向vector中的一个元素,那么在溢出触发的重分配后,由于销毁了原内存而直接触发野指针问题。Java可能会使用虚拟机自动跟踪,但是会大幅拖累运行速度,链表是真正意义上的动态增长,但是由于指针和动态内存分配存在,必然会导致二次跳转,这同样会拖累运行速度。
综上所述,改为链表/增加虚拟机跟踪都会拖累运行速度,但如果不进行指针跟踪,则必然会触发野指针。所以rust采取第3种策略:编译检查。rust在语法层面强制使用所有权规则让一个数据禁止在拥有一个可变引用的情况下声明其它引用,这将导致在编译时就能发现这种数据读写错误,强制程序员写出正确的代码。
5.vector遍历:
for item in &vector_ins {...}
for item in &mut vector_ins {...}
6.此外还可以让vector的类型为枚举,通过枚举绑定值来让vector中保存多个值。
7.vector数据弹出:vector_ins.pop();//不返回数据
8.vector长度返回:vector_ins.len();//返回usize
9.vector是否为空:vector_ins.is_empty();//返回bool
10.vector判断数据是否存在:vector_ins.contains(&item);//返回值为bool
字符串
1.首先str和String是两个类型,虽然都是字符串。str类型的数据存储在栈上,并且实现了Copy trait。是rust的内置数据类型。UTF-8编码,变长字符。
2.String由std库提供。其数据存储在堆上,并且clone的实现是深拷贝,这意味着Copy trait必然没有实现。UTF-8编码,变长字符。
3.此外to_string不会导致str发生所有权转移。
4."XXX".to_string和String::from("XXX")逻辑相同
5.String在结尾接入新的字符串:string_ins.push_str("XXX"); //这里不会发生所有权转移
6.使用+拼接字符串:
let s1 = String::from("hello");
let s2 = String::from(" world");
let s3 = String::from("!");
let s4 = s1 + &s2 + &s3 + "!"; //这里需要区分s1发生了权限转移,而s2做了复制。逻辑上是s4获取了s1的所有权然
let s5 = format!("{s1},{s2},{s3}");//这个宏专门用于字符串拼接,并且不会转移任何所有权,返回一个结果String.
后在后面附加一份s2和s3的深拷贝。
此外这里的+的双方的值的类型不同,这里的+不支持String+String的方式,仅支持String+&str的方式,所以这里s2和s3被进行了强制类型转换(deref)将&s2和&s3的类型从&String转为&str[..]。
7.关于String与str字符串的索引和遍历:
str.chars().nth(0);
String.chars().nth(0);
这里的含义是会返回一个Option
此外还可以通过切片来获取数据,但是要确保你知道字符长度:
let hello = "Здравствуйте";
let s = &hello[0..4];
关于遍历:
for c in "Зд".chars() {
println!("{c}");
}
for b in "Зд".bytes() {
println!("{b}");
}
如下,为对String / Str中一个字符的提取:
fn main()
{
let mode_str = "hello,world".to_string();
let mut check_res : char = ' ';
check_res = match mode_str.chars().nth(0) {
Some::
_ => ' ',
};
println!("{check_res}");
println!("{mode_str}");
}
8.一种快速字符串解析手段:split_whitespace
split_whitespace的意思是将原字符串按空格切分,并返回一个迭代器
for word in text.split_whitespace() {
//handle each word
}
Hash Map结构
1.使用时的路径:use std::collections::HashMap;
2.如果不具体写明HashMap的键值类型,会默认键是String, 值是i32。
3.HashMap的声明:
let mut map_ins : HashMap::<type1, type2> = HashMap::new();
4.HashMap插入键值对:map_ins.insert(val1, val2); //注意,堆对象元素做键值会发生所有权转移。
5.HashMap值访问:let value = map_ins.get(&key_name).copied(); //此函数返回一个Option<value_type>类型数据。(如果不调用copied的话,返回的是Option<&value_type>)
6.for循环:
for (key, value) in &map_ins {
println!("{key} : {value}");
}
7.更新键值:直接使用insert插入一个同键不同值的键值对即可。
8.若键不存在时插入键值对:map_ins.entry(val1).or_insert(val2); //只在val1不存在时,插入val1-val2,然后返回最新的改键的对应的值的可变引用。
9.解引用:对于引用而言无法直接参与运算,所以需要先对引用解引用,然后才能更新引用的值。例子:
fn main()
{
let mut a = 4;
let mk : &mut i32 = &mut a;
*mk = 1;
println!("{mk}");
}
错误处理
1.首先,如果想在程序panic时直接abort程序而非展开程序,则需要在Cargo.toml中配置如下字段来开启abort功能:
//在debug程序中,Cargo.toml可按照如下进行配置:
[profile]
panic = 'abort'
//在release程序中,Cargo.toml可按照如下进行配置:
[profile.release]
panic = 'abort'
//这里说一下abort和unwinding的区别:abort是终止程序,不清除数据,后续工作交由操作系统处理。而unwinding会让rust自动回溯清理相关内存。
2.手动触发panic:panic!("Crash the system.");
3.可引起panic的类型:Result。此类型通过将Err实例传入expect来引起panic
Result为枚举:
enum Result<T, E> {
Ok(T),
Err(E),
}
4.文件打开函数:
(1)引用路径:use std::fs::File;
(2)文件打开:let file_open_result = File::open("./path1/path2/test.txt");
(3)该函数返回的是Result,其中T是std::fs::File类型,E为std::io::Error类型。
可按照如下方案来处理打开失败和打开成功的内容的代码:
let open_result = File::open("foo.txt");
let file_handle = match open_Result {
Ok(handle) => handle,
Err(error) => return () / panic!("error:{error:?}"), //或者其它处理方案,合理即可,也可以引起panic
};
(4)通过unwrap函数来处理返回的Err。
let file_handler = File::open("foo.txt").unwrap();
//当存在文件时,直接返回一个文件句柄,当文件不存在时,自动调用panic!函数。
(5)通过expect函数来处理Err。
let file_handler = File::open("foo.txt").expect("This is an error");
//当文件存在时,直接返回一个文件句柄,当文件不存在时,自动调用panic!函数,其中错误信息由expect入参字符串决定。
5.Err中的类型E一般是std::io::Error
6.使用"?"来处理Result错误:
let mut file_handler = File::open("foo.txt")?;
//这里的含义是如果该文件存在则返回文件句柄,否则直接return该错误。即,与下面的代码等价
let mut file_handler = match File::open("foo.txt") {
Ok(handler) => handler,
Err(e) => return Err(e),
};
//这里说一下数组入参怎么写,fn func_name(mode_array : &[i32]) -> () {...}
7.使用"?"来处理Option返回的None:调用方式与Result相同,逻辑改为如果返回的是Some则,将值抛出来,如果返回值是None,则直接从当下开始return None。下面代码是一个调用实例:不使用size函数和for来遍历vector:
fn Loop_i32_Array(mode_array : &Vec
let mut i = 0;
let mut val : &i32 = &0;
while true {
val = mode_array.get(i)?;
println!("{val}");
i += 1;
}
None
}
let my_array : Vec
Loop_i32_Array(&my_array);
泛型的声明与使用
1.泛型函数的声明与使用:
fn func_name
}
func_name(...); //T的类型会进行自动推导,但是也可以手动指定
func_name::
其中T的使用范围是入参,返回值和函数体内部。此外,需要注意的是,如果入参没有用到泛型的类型,则调用时必须手动指定。
2.泛型结构体的声明与使用:
struct Block
var1 : type1,
var2 : type2,
}
let blk1 = Block::
与上述相同,当T被用于参数时,::
此外,如果想要定义一个以上的泛型类型时,可有如下写法:struct Block<type1, type2, type3> {...},这种写法对所有泛型通用。
3.泛型枚举的声明与使用:
enum Block<T, U> {
ele_1(T),
ele_2(U),
}
let blk : Block::<i32, f32> = Block::<i32, f32>::ele1(1);
let blk : Block_Type::<i32, f32> = Block_Type::ele1(1);
let blk = Block_Type::<i32, f32>::ele1(1);
特点与上述相同不在赘述。
4.泛型结构体中,方法使用泛型类型:
struct Block
var1 : T,
var2 : i32,
}
impl
fn Block_func(&self, ...) -> () { ... /可以在这个位置使用泛型类型/}
}
let blk : Block::
blk.Block_func(...);
特征与其它泛型相同,不再重复
这里还需要提一嘴,和C++相同,泛型的定义的实例化是在编译期完成的不占用运行时间,按照rust自己的话来讲,叫做编译时单态化。
Trait的定义与使用
Trait的本质其实就是接口(小点儿声,Rustacean们可不愿意听这种话),但是与C++中的纯虚函数不同的是Trait将在编译期对所有接口进行实例化,而非像C++需要虚表跳转从而增加运行时间。此外,Trait并非通过继承而是组合的形式实现接口,这意味着相较于传统的继承方式实现,该方式对接口和其implement(这里使用了Java的说法)的类而言在语法层面做了进一步的解耦。
1.Trait的声明和使用:
pub trait trait_name {
fn interface_func1(&self, ...) -> String;
} //类似于接口声明,这里还可以写一个接口函数的默认实现,就是把“;”换成“{...}”。如果有默认实现,则当当前没有实现接口时,会走默认实现。
struct Block {
var1 : i32,
} //需要实现接口的类/结构体
impl Block {
fn Block_Func(&self, ...) -> () {
}
}//结构体自己的函数
impl trait_name for Block {
fn interface_func1(&self, ...) -> String {
//implemented code ...
}
}//实现接口
let blk = Block {var1 : 4};
blk.interface_func1();//调用接口
fn Get_Trait_Obj(item : &impl trait_name) -> ()
{
//handle code ...
}//这是在将接口类型作为入参
fn Get_Trait_Obj
{
//handle code ...
}//这是另一种将接口类型作为入参的写法,一种泛型编程的写法,更加自然和可读性更高
fn Get_Trait_Obj(item : &(impl trait_name1 + trait_name2)) -> ()
{
//handle code ...
}//接受的入参必须同时实现了两个接口才可
fn Get_Trait_Obj<T : trait_name1 + trait_name2>(item : &T) -> ()
{
//handle code ...
}//同上,这是另一种泛型风格写法,此外这种写法还可以套用在泛型结构体、泛型枚举等结构中。
2.where简化函数签名(实际没剩多少字符,但是代码可读性更强了)
fn Get_Trait_Obj<T, U>(item1 : &T, item2 : U) -> ()
where
T : trait_name1 + trait_name2,
U : trait_name3 + trait_name4,
{
//handle code
}
//这里的意思是函数接受两个入参item1与item2,其中item1需要实现trait_name1与trait_name2两个接口,而item2需要实现trait_name3和trait_name4两个接口。
fn return_a_trait() -> impl trait_name {
//handle code ...
}//这种是返回一个实现了接口trait_name的对象 //目前这种写法还不完善
关于变量绑定的另一种写法
这是基础篇被拉下的一个内容,即rust允许变量声明后处于未绑定状态。
可以按照如下方法声明与绑定:
let r;
r = 4;
//注意着并不代表r是可变的,这里的r再一开始是未绑定状态,而任何变量必须要有一次绑定,所以在未绑定状态下,可以使用赋值进行第一次绑定。
生命周期的理解与骚操作使用
1.生命周期可理解为变量/数据的有效作用域。
2.在很多时候,编译器可自动推导变量/数据的生命周期,进而判断访问是否有效,但是会有时无法判断变量的声明周期,这时需要显式声明生命周期辅助编译器进行判断。
3.生命周期注释语法并不改变任何引用的生命周期的长短,它仅用于描述多个引用生命周期的相互关系。
4.对一个引用注释其生命周期:
(1)&i32 //参数或返回值仅为引用
(2)&'a i32 //这里标注了i32类型引用的生命周期注释为a
(3)&'a mut i32 //与(2)相同,但为可变引用
5.生命周期注释的使用:泛型生命周期参数
fn test_func<'a>(x : &'a i32, y : &'a i32) -> &'a i32 {
//handle code ...
}
这个函数表明,声明了一个声明周期注释a,这个a代表的生命周期大小为x和y所在生命周期的交集。
此外,还表明返回的值的生命周期也会是二者的交集。这里暗含了当前函数的作用域是x与y交集的一部分,以及强制要求返回值的生命周期是二者交集(因为很难出现只有一部分相交的情况,所以基本相交出来的结果是较小的呢一个作用域)。如果违反上述规则,讲导致编译报错。
这里解释一下,rust为何如此设计。
假设没有生命周期注释的话,代码如下:
fn test_func(x : &i32, y : &i32) -> &i32 {
//handle code
}
现在假设,函数是这样设计的:
fn test_func(x : &i32, y : &i32) -> () {
//handle code
}
(1)那么,首先可以确定的是现在函数所在的作用域一定存在于x和y相交的子作用域中,所以函数内部可保证所有变量与引用均有效,此时不需要显式声明,编译器会自动推导是否正确。
(2)但是如果写成第一种情况,那么返回值其实有四种来源:
1.x:可以,原因在于当前函数本身所在的作用域一定被x的生命周期包裹,所以返回值可保证有效
2.y:可以,原因同上。
3.全局变量:可以,因为全局变量拥有最大的生命周期,返回值一定可保证有效
4.函数内部生成的一个值的引用:不可,当函数结束返回后,会导致内部值被释放,进而导致引用无效,那么函数就返回了一个无效引用,这是错误的。
由于编译器在判断生命周期的时候不会关注具体算法,所以编译器无法判断返回值究竟属于哪一类,rust不允许这种情况出现,即“函数返回的引用或指针可能无效”。所以,当不清楚时,编译器要求开发者明确给出返回引用的值的生命周期。如果有一种办法指明返回值引用与哪一个变量的声明周期相同,编译器便可判断返回时,该引用是否还有效。
所以,需要显式给出一个声明周期,遂函数改为如下描述:
fn test_func<'a>(x : &'a i32, y : &'a i32) -> &'a i32 {
//handle code ...
}
在这个改后的代码中,我们声明了一个声明周期a,那么a的范围是多大呢?答案是它是x与y中较小的那一个(即x与y的生命周期的交集),而我们又说返回值引用的值的生命周期也是'a,这表明返回引用的值的来源仅可能是x、y、全局变量,这意味着返回必然有效,遂编译器通过编译。所以,标注生命周期的根据是我们的代码逻辑来让我们自行标注,但是标注后是否真的合理则由编译器检查。这便是生命周期注释的真正含义。
简而言之:程序员标注生命周期,编译器检查生命周期是否合理。
6.在结构体和方法中使用生命周期注释:
struct Block_Name<'a> {
var1 : &'a i32,
...
}
impl<'a> Block_Name<'a> {
fn self_func(&self) -> () {...}
}
7.生命周期注释可以与泛型参数进行混用,以及泛型约束等混用。
rust测试代码编写
1.属性:一种用于标识代码片段的元数据,写法#[attribute_name]。
2.测试函数:在函数定义上面加上#[test]标识该函数属于测试代码的一部分
3.测试执行命令:cargo test
4.测试模块编写:
[cfg(test)]
mod tests {
use super:😗;
[test]
fn test_func1() {
//test handle code1
}
[test]
fn test_func2() {
//test handle code2
}
}
5.测试代码中的断言:
(1)assert!(experssion, info); 如果expression结果为true,则通过测试,反之测试失败,并且会调用panic!其中info是同时测试用例失败时返回的信息,类型为&str。
(2)assert_eq!(expression, val);如果expression的结果为val,则测试通过,反之测试失败
(3)测试函数中引发panic!将直接导致测试失败,打屏日志中将显示因panic导致测试失败
(4)assert_ne!(expression, val);与assert_eq!相反,相等报错,否则通过。
6.should_panic属性:#[should_panic(expected = "XXX")]
这个标签将放在#[test]下面,当一个函数触发了panic并且报错信息中包含"XXX"时,让测试代码不产生panic。但是测试仍会失败,只是不会产生panic而已。
7.使用Result代替断言:
测试函数可返回Result,那么测试系统会根据返回的Result判断是否通过,如果是Ok则通过,反之不通过,并将Err绑定的字符串作为错误信息输出。
8.测试命令:
(1)cargo test -- --test-threads=1 //指明执行测试用例时需要的线程数
(2)cargo test -- --show-output //指明rust测试成时内部代码的println!()的打印
(3)cargo test func_name //这里的func_name是、测试函数的函数名
9.ignore属性:#[ignore]
(1)标注在#[test]下面,被标注的函数除非特别指明,否则在运行测试时不会运行该函数。
(2)只运行ignored命令:cargo test -- --ignored
10.rust中的测试组织模板:
(1)这里属于rust独有的一些行话,或者说基于rust的这套测试系统而提出的一些更合适的测试结构。
(2)单元测试与集成测试(只是对于rust而言,和软件工程中说的不太一样):
1.单元测试:即每次测试一个模块。单元测试的规定是测试代码是一个被cfg(test)标注的test模块(模块名可随意,但是为了可读性最好取一些比较有含义的名字),test模块与对应的被测模块在一个.rs文件中。
2.集成测试:集成测试的目的是完全从外部视角调用库,看是否正常运行。这些文件被放在与src同级的tests目录下,不需要cfg(test)标注,rust默认tests目录下的模块都是测试模块。如果需要多模块集成,则需要/module_name/mod.rs这种老式写法(这个与src中的代码不同,src中支持/module_name/mod.rs和/module_name.rs两种写法)。但是运行用例时仍旧以cargo test来执行运行。如果仅指定某个集成测试执行,可使用cargo test --test module_name来执行。
这里不会讲解第12章的rust项目实例开发,此内容会在rust---项目实战 中说明。
浙公网安备 33010602011771号