Rust
配置开发环境
- 一键安装
对于ArchLinux及类ArchLinux
sudo pacman -S rust
- 通过
rustup
安装
由于Clion强依赖与rustup,所以推荐通过rustup
安装Rust,流程如下:
sudo pacman -S rustup
# 设置rustup清华镜像源(https://mirrors.tuna.tsinghua.edu.cn/help/rustup/)
# 我这里长期启用清华镜像
# for zsh
echo 'export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup' >> ~/.zshrc
echo 'export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup' >> ~/.zshrc
# 这里安装的是稳定版,还有beta版和nightly版
rustup toolchain install stable
Cargo
创建项目
cargo new new_project
在Rust里代码的包称为crate
猜数游戏
猜数游戏是进入Rust前一个小例子。
导入rand
库
在Cargo.toml
文件的[dependencies]
下插入下面行
rand = "0.8.5"
输入命令Cargo build
,将从远端仓库拉取包。
示例代码
// 猜数游戏
use std::io;
use rand::Rng;
use std::cmp::Ordering;
fn main() {
println!("猜数游戏开始!!");
let screct_num = rand::thread_rng().gen_range(1..101);
// println!("the screct num is:{}",screct_num);
loop {
println!("猜测一个数:");
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("读取行失败!!");
println!("你猜测的数为:{}",guess);
// shadow
// let guess:u32= guess.trim().parse().expect("Please type a number");
let guess:u32 = match guess.trim().parse(){
Ok(num) => num,
Err(_) => {
println!("You must input a num");
continue;
}
};
match guess.cmp(&screct_num){
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
Rust通用编程概念
变量与可变性
- 声明变量使用
let
关键字 - 默认情况下,变量是不可变的(Immutable)
- 声明变量时,在变量前加上
mut
,使变量可变
变量与常量
- 不可使用
mut
- 使用
const
关键词,它的类型必须被标注 - 常量可以在任何作用域声明,包括全局作用域
- 常量只可以绑定到常量表达式
- Rust里常量使用全大写,每个单词之间使用下划线分开
数据类型
标量类型
一个表量类型代表一个单个的值
Rust有四个主要的表量类型
- 整数类型
- 浮点类型
- 布尔类型
- 字符类型
整数类型
长度 | 有符号 | 无符号 |
---|---|---|
8-bit | i8 |
u8 |
16-bit | i16 |
u16 |
32-bit | i32 |
u32 |
64-bit | i64 |
u64 |
128-bit | i128 |
u128 |
arch | isize |
usize |
isize
和 usize
类型依赖运行程序的计算机架构:64 位架构上它们是 64 位的, 32 位架构上它们是 32 位的。
整数的字面值
数字字面值 | 例子 |
---|---|
Decimal (十进制) | 98_222 |
Hex (十六进制) | 0xff |
Octal (八进制) | 0o77 |
Binary (二进制) | 0b1111_0000 |
Byte (单字节字符)(仅限于u8 ) |
b'A' |
整数溢出
比方说有一个
u8
,它可以存放从零到255
的值。那么当你将其修改为256
时会导致“整型溢出”(“integer overflow” )。导致以下两种行为之一的发生当在 debug 模式编译时,Rust 检查这类问题并使程序 panic,这个术语被 Rust 用来表明程序因错误而退出。
在 release 构建中,Rust 不检测溢出,相反会进行一种被称为二进制补码包装(two’s complement wrapping)(环绕操作)的操作。简而言之,值
256
变成0
,值257
变成1
,依此类推。
浮点类型
Rust 也有两个原生的 浮点数(floating-point numbers)类型
f32
,32位,单精度f64
,64位,双精度
默认类型是 f64
布尔类型
Rust 中的布尔类型有两个可能的值:true
和 false
。占一个字节
字符类型
char
类型被用来描述语言中最基础的单个字符- 字符类型的字面值使用单引号
- 是Unicode标量值
复合类型
Rust 有两个原生的复合类型:元组(tuple)和数组(array)。
Tuple
- Tuple可以将多个类型的多个值放在一个类型里
- Tuple的长度是固定的:一旦声明就无法改变
创建Tuple
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
获取Tuple的元素值
使用模式匹配结构一个Tuple
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {}", y);
}
访问Tuple的元素
使用点标记法
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {} {} {}", tup.0, tup.1, tup.2);
}
Array
- 数组可以将多个值放在一个类型里面
- 数组中每个元素的类型必须相同
- 数组的长度是固定的
声明一个数组
fn main() {
let a = [1, 2, 3, 4, 5];
let b:[i32;5]=[1, 2, 3, 4, 5];
let c = [3;5]; // => let c = [5, 5 ,5];
}
数据元素放在栈上
如果访问的索引超过了数组的范围,那么:
-
编译会通过 (复杂情况下)
-
运行时没报错(runtime时会panic)
函数
参数
- 函数参数的类型必须指明
函数体中的语句和表达式
语句(Statements)是执行一些操作但不返回值的指令。表达式(Expressions)计算并产生一个值。让我们看一些例子。
- 使用
let
关键字创建变量并绑定一个值是一个语句。let y = 6;
是一个语句。 - 函数定义也是语句
语句不返回值。因此,不能把 let
语句赋值给另一个变量
函数的返回值
- 在
->
符号后面声明函数返回值的类型,但是不可以为返回值命名 - 在Rust里,返回值就是函数体里面最后一个表达式的值
- 若想提前返回,需要使用
return
关键字,并指明一个值
fn add_five(value:i32)->i32{
value+5
}
控制流
if表达式
if number < 5{
println!("condition was true");
} else {
println!("condition was false");
}
// else if
if number < 5{
println!("1");
} else if number > 7{
println!("2");
} else {
println!("3");
}
如果代码里使用了多于一个else if
,那么最好使用match
来重构代码
由于if是一个表达式,所以可以放在等号右边
let number = if condition {5} else {6}
循环
Rust提供了3种循环loop
,while
和for
// loop
loop{
}
// while
while condition {
}
// for
let a = [1, 2, 3, 4, 5];
for elem in a.iter(){
println!("the value is: {}",elem);
}
for num in (1..4){
println!("the value is: {}",num);
}
for num in (1..4).rev() {
println!("the value is: {}",num);
}
所有权
什么是所有权?
Stack vs Heap
和C++ 基本一致
所有权解决的问题:
- 跟踪代码的哪些部分正在使用Heap的哪些数据
- 最小化Heap上的重复数据量
- 清理Heap上未使用的数据以避免空间不足
管理Heap数据才是所有权存在的原因
所有权的规则
- Rust 中的每一个值都有一个被称为其 所有者(owner)的变量。
- 值在任一时刻有且只有一个所有者。
- 当所有者(变量)离开作用域,这个值将被丢弃。
String类型(Heap上数据结构)
创建String类型的值
let mut s = String::from("Hello");
s.push_str(",World");
内存和分配
-
就字符串字面值来说,我们在编译时就知道其内容,所以文本被直接硬编码进最终的可执行文件中。这使得字符串字面值快速且高效。(不可变性)
-
对于
String
类型,为了支持一个可变,可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容。这意味着:-
必须在运行时向内存分配器(memory allocator)请求内存。
-
需要一个当我们处理完
String
时将内存返回给分配器的方法。
-
Rust采用不同方式: 对于某个值来说,当拥有他的变量走出作用范围时,内存会立即自动的交还给操作系统。
drop()
函数:变量离开作用域时会自动调用drop
函数
变量和数据的交互方式:移动(Move)
多个变量可以与同一数据使用一种独特的方式来交互
let x = 5;
let y = x;
整数是已知且固定大小的简单的值,这两个5被压到stack中
let s1 = String::from("hello");
let s2 = s1;
String类型的组成
Rust采取的策略是:让s1失效(类似于C++的移动语意)
上述代码会报出如下错误:
fn main(){
let s1 = String::from("hello");
let s2 = s1;
println!("{}",s1);
}
// error[E0382]: borrow of moved value: `s1`
克隆
相当于深拷贝
fn main(){
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{}",s1);
}
复制
对于Stack上的数据,仅为复制
- Copy trait,可以用于像整数这样完全存放在stack上面的leixing
- 如果一个类型实现了Copy这个trait,那么旧的变量在赋值后仍然可用
- 如果一个类型或者该类型的一部分实现了Drop trait,那么Rust不允许让他再去实现Copy trait了
一些拥有Copy trait的类型
- 任何简单标量的组合类型都可以是Copy的
- 任何需要分配内存或某种资源的都不是Copy的
- 一些拥有Copy trait的类型
- 所有的整数类型
- bool
- char
- 所有的浮点类型
- Tuple,如果其所有的字段都是Cpoy的
所有权与函数
在语意上,将值传递给函数和把值赋给变量是类似的。
-
将值传递给函数要么会发生移动,要么会发生复制
fn main(){ let s1 = String::from("hello"); take_ownership(s1); // s1被移动 let x =5; make_copy(x); // x被拷贝 println!("x:{}",x); } fn take_ownership(str_value:String){ println!("{}", str_value); } fn make_copy(val:i32){ println!("{}",val); }
返回值与作用域
-
函数在返回值的过程中同样也会发生所有权的转移
fn main(){ let s1 = String::from("hello"); let s2 = give_ownership(); let s3 = take_and_get(s1); } fn give_ownership()-> String{ let res = String::from("hello"); return res; } fn take_and_get(val: String)->String{ val }
引用与借用
// 引用
fn main(){
let s1 = String::from("hello");
let len = get_length(&s1);
println!("the len is {}",len);
}
fn get_length(val: &String)-> usize{
val.len()
}
这些 &
符号就是 引用,它们允许你使用值但不获取其所有权。下图展示了一张示意图
借用
-
我们把引用作为函数参数的这个行为成为借用。
-
我们是否可以修改借用的东西?(不行)
-
和变量一样,引用默认也是不可变的
可变引用
& mut value
可变引用有一个重要的限制:在特定的作用域内,对某一块数据,只能有一个可变的引用
- 可在编译时防止数据竞争
以下三种行为下会发生数据竞争
- 两个或多个指针同时访问同一数据
- 至少有一个指针用于写数据
- 没有任何机制来同步对数据的访问
不可以在同一作用域中同时拥有一个可变引用和不可变引用。
悬空引用
悬空引用:一个指针引用了内存中的某个地址,而这块内存可以已经释放并分配给别人
在Rust里,编译期可保证引用永远不是悬空引用
引用的规则
- 在任意给定的时刻,只能满足下列条件之一:
- 一个可变的引用
- 任意数量的不可变引用
- 引用必须一直有效
切片
Rust的另一种不持有所有权的数据类型:切片(slice)
字符串 slice(string slice)是 String
中一部分值的引用,它看起来像这样:
fn main(){
let s = String::from("hello world");
let worldindex = get_substr(&s);
let hello = &s[0..5];
let world = &s[6..11];
}
字符串字面值被直接存储在二进制程序中
let s = "Hello, World";
变量s
的类型是&str
,他是一个指向二进制程序特定位置的切片
&str
是不可变引用,所以字符串字面值也是不可变的
将字符串切片作为参数传递
fn first_world(s :&String) -> &str{}
有经验的开发者会采用&str
作为参数类型,因为这样就可以同时接受String
和&str
类型的参数了
fn first_world(S: &str) -> &str {}
- 使用字符串切片直接调用该函数
- 使用
String
,可以创建一个完整的String
切片来调用该函数。
其他类型的切片
let a = [1,2,3,4,5];
let b = &a[1..2];
struct
定义并实例化struct
例子
定义:
struct User{
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
初始化:
let user1 = User{
email: String::from("violet@126.com"),
username: String::from("violet"),
sign_in_count: 12,
active: true,
}
取值
User.email
赋值
要给struct
某个属性赋新的值,该struct
应该是可变的。
Rust不允许单独设置struct
某个字段的可变性
let mut user1 = User{
email: String::from("violet@126.com"),
username: String::from("violet"),
sign_in_count: 12,
active: true,
}
User.email = String::from("overflow@126.com");
字段初始化简写
当字段名与字段值对应变量名想同时,我们可以使用 字段初始化简写语法(field init shorthand)
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
struct更新语法
当你想基于某个struct
实例来创建一个新的实例的时候,可以使用struct
更新语法
fn main() {
// --snip--
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};
}
更新时
fn main() {
// --snip--
let user2 = User {
email: String::from("another@example.com"),
..user1
};
}
Tuple struct
Tuple struct整体有个名,但里面的元素没有名
适用:想给整个tuple起名,并让他不同于其他tuple,而且又不需要给每个元素起名。
struct Color(i32,i32,i32);
let black = Color(0,0,0);
Unit-Like Struct
- 可以定义没有任何字段的
struct
叫做Uint-Like struct(因为与(),单元类型类似) - 适用于需要在某个类型上实现某个
trait
,但是在里面又没有想要存储的数据
struct格式化输出
// #[derive(Debug)]
struct User{
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
fn main(){
let user1 = User{
email: String::from("violet@126.com"),
username: String::from("violet"),
sign_in_count: 12,
active: true,
};
println!("{:?}",user1);
println!("{:#?}",user1);
}
// User { username: "violet", email: "violet@126.com", sign_in_count: 12, active: true }
// User {
// username: "violet",
// email: "violet@126.com",
// sign_in_count: 12,
// active: true,
// }
struct的方法
方法和函数类似:fn
关键字,名称,参数,返回值
方法与函数不同之处:
- 方法是在
struct
(或者enum
、trait
对象)的上下文中定义 - 第一个参数是self,表示方法被调用的
struct
实例
定义方法
#[derive(Debug)]
struct Rect{
height: u32,
width: u32,
}
// 定义Rect的方法
impl Rect{
fn area(&self)->u32{
self.height * self.width
}
fn can_hold(&self, other: &Rect)->bool{
self.height > other.height && self.width > other.width
}
}
fn main(){
let rec = Rect{
height: 50,
width: 30,
};
let rec2 = Rect{
height:40,
width: 20,
};
println!("{}",rec.can_hold(&rec2));
println!("{}",rec.area());
}
关联函数
可以在impl块里定义不把self
作为第一个参数的函数,它们叫关联函数(不是方法)
#[derive(Debug)]
struct Rect{
height: u32,
width: u32,
}
// 定义Rect的关联函数
impl Rect{
fn square(size: u32)-> Rect{
Rect{
height:size,
width:size,
}
}
}
fn main(){
// 关联函数调用
let s = Rect::square(20);
println!("{:#?}",s);
}
注意
一个
struct
的方法(或关联函数)可以不同的impl
块中
枚举
枚举允许我们列举所有可能的值来定义一个类型
定义枚举
-
IP地址:IPV4,IPV6
enum IpAddrKind{ V4, V6, } fn main(){ let four = IpAddrKind::V4; let six = IpAddrKind::V6; route(four); route(six); route(IpAddrKind::V4); } fn route(ip_kind: IpAddrKind){}
枚举的变体部分都位于标识符的命名空间下,使用两个冒号
::
进行分割 -
将数据附加到枚举的变体中
enum IpAddrKind{ V4, V6, } // 常规方法 struct IpAddr{ kind: IpAddrKind, address: String, } // 数据附加枚举 enum IpAddr_e{ V4(u8,u8,u8,u8), V6(String), } fn main(){ let home = IpAddr{ kind: IpAddrKind::V4, address:String::from("127.0.0.1"), }; let home_e = IpAddr_e::V4(127,0,0,1); let loopback = IpAddr_e::V6(String::from("::1")); }
为枚举定义方法
enum Massage{ Quit, Move{x:i32, y:i32}, Write(String), ChangeColor(i32,i32,i32), } impl Massage{ fn call(&self){ } } fn main(){ let m = Massage::Move{x:12, y:24}; m.call(); }
Option枚举
- 定义与标准库中
- 在
Prelude
(预导入模块)中 - 描述了:某个值可能存在(某种类型)或不存在的情况
Rust没有Null
Rust中类似Null概念的枚举—Option
标准库中的定义:
enum Option<T>{
Some(T),
None,
}
fn main(){
let some_number = Some(5);
let some_string = Some("A String");
let absent_num: Option<i32> =None;
}
好处:
Option<T>
和T
是不同的类型,不可以把Option<T>
直接当作T
- 若想使用
Option<T>
中的T
,必须将他转换为T
match
match
表达式
enum Coin{
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin:Coin)->u8{
match coin{
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter =>25,
}
}
fn main(){
let Penny:u8 = value_in_cents(Coin::Penny);
println!("{}",Penny);
}
绑定值的模式
#[derive(Debug)]
enum States{
Henan,
Hubei,
}
enum Coin{
Penny,
Nickel,
Dime,
Quarter(States),
}
fn value_in_cents(coin:Coin)->u8{
match coin{
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) =>{
println!{"the state is {:#?}",state};
5
},
}
}
fn main(){
let Penny:u8 = value_in_cents(Coin::Penny);
let Henan = Coin::Quarter(States::Henan);
value_in_cents(Henan);
println!("{}",Penny);
}
Option
fn main(){
let five =Some(5);
let six = add_one(five);
let none = add_one(None);
}
fn add_one(x:Option<i32>) -> Option<i32>{
match x{
None => None,
Some(i) => Some(i+1);
}
}
match匹配必须穷举所有的可能
- _通配符:替代其余没列出的值
fn main(){
let v = 0u8;
match v{
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
// 匹配其余情况,要放在最后
_ => (),
}
}
if let
处理只关心一种匹配而忽略其他匹配的情况
例子:
fn main(){
let v = Some(0u8);
if let Some(3) = v{
println!("three");
}
// 相当于
match v{
Some(3) => println!("three"),
// 匹配其余情况,要放在最后
_ => (),
}
}
Package, Crate, Moudle
- 包(Packages): Cargo 的一个功能,它允许你构建、测试和分享 crate。
- Crates :一个模块的树形结构,它形成了库或二进制项目。
- 模块(Modules)和 use: 允许你控制作用域和路径的私有性。
- 路径(path):一个命名例如结构体、函数或模块等项的方式
Package和Crate
-
Crate的类型
- binary
- library
-
Crate Root
- 是源代码文件
- Rust编译器从这里开始,组成你的Crate的跟Module
-
一个Package
- 包含了1个Cargo.toml,他描述了如果构建这写Crates
- 只能包含0-1个library crate
- 可以有任意数量的binary crate
- 但必须至少包含一个crate(library或binary)
路径和module
例子:
mod front_of_house {
mod hosting {
pub fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
pub fn eat_at_restaurant(){
// 绝对路径
crate::front_of_house::hosting::add_to_waitlist();
// 相对路径
front_of_house::hosting::add_to_waitlist();
}
私有边界
- 模块不仅可以组织代码,还可以定义私有边界
- 如果想把函数或者struct等设为私有,可以将它放着某个模块中
- Rust中所有的条目(函数,方法,struct,enum,模块,常量)默认是私有的
- 父级模块无法访问子模块中的私有条目
- 子模块可是使用所有祖先模块中的条目
pub
关键词
- 将某些条目标记为公共的
super
关键词
- 用来访问父级模块路径中的内容,类似文件系统中的
..
pub struct
struct
在mod
中
pub
放在struct
前:
struct
是公共的struct
的字段默认是私用的struct
的字段需要单独设置pub
来变成共有
pub enum
pub
放在enum
前:
enum
是公共的enum
的变体也都是公共的
use
关键字
可以使用use
关键字将路径导入到作用域内
- 仍遵守私有性规则
mod front_of_house {
pub mod hosting{
pub fn add_to_waitlist(){}
}
}
// 绝对
use crate::front_of_house::hosting;
// 相对
use front_of_house::hosting;
pub fn eat_at_res(){
hosting::add_to_waitlist();
}
as
关键字
as
关键字可以为引入的路径制定本地的别名
mod front_of_house {
pub mod hosting{
pub fn add_to_waitlist(){}
}
}
// 绝对
use crate::front_of_house::hosting as Fhosting;
// 相对
use front_of_house::hosting as Fhosting;
pub fn eat_at_res(){
hosting::add_to_waitlist();
}
使用pub use
重新导出名称
mod front_of_house {
pub mod hosting{
pub fn add_to_waitlist(){}
}
}
// 外部作用域可以使用hosting::add_to_waitlist()
pub use crate::front_of_house::hosting;
pub fn eat_at_res(){
hosting::add_to_waitlist();
}
pub use
:重导出
- 将条目引入作用域
- 该条目可以被外部代码引入到他们的作用域
使用外部包
- Cargo.toml添加依赖的包
- http:://crates.io/
use
将特定条目引入
使用嵌套路径清理大量的use
语句
use std::{cmp::Ordering,io};
//use std::io
//use std::io::Write
use std::io::{self, Write};
//通配符* :引入下属路径所有条目
/*应用场景
*测试: 将所有测试代码引入到tests模块
*有时被用于预导入(prelude)模块
use std::collections::*;
将模块内容移动到其他文件
模块定义时,如果模块名后面是;
,而不是代码快
- Rust会从与模块同名的文件中加载内容
- 模块树的结构不会发生变化
随着模块的逐渐变大,该技术让你可以吧模块的内容移动到其他文件中。
常用的集合
存储在Heap上
Vector
创建Vector
fn main(){
let v:Vec<i32> = Vec::new();
// 初始值创建Vector
let v1 = vec![1,2,3]; // macro
// 不用显示声明Vector类型
let mut v2 = Vec::new();
v2.push(1);
}
删除Vector
与所有其他struct一样,当Vector
离开作用域后
- 它就被清理掉了
- 它所有的元素也被清理掉了
读取Vector中的元素
-
两种方式可以引用Vector中的值
- 索引
fn main(){ // 初始值创建Vector let v1 = vec![1,2,3,4,5]; // macro let third: i32 = v1[2]; println!("{}",third); println!("{}",v1[2]); }
越界时发出
panic
-
get方法
get方法返回一个
Option<T>
fn main(){ // 初始值创建Vector let v1 = vec![1,2,3,4,5]; // macro let third: &i32 = &v1[2]; println!("{}",third); match v1.get(2){ Some(val) => println!("{}",val), None => println!("None"),白鸟 }; }
所有权和借用规则
- 不可以在同一作用域中同时拥有一个可变引用和不可变引用。
在引用Vector引用时同样适用
fn main(){
// 初始值创建Vector
let mut v1 = vec![1,2,3,4,5]; // macro
// 不可变引用
let third: &i32 = &v1[1];
// 可变引用
v1.push(6);
// 不可变引用
println!("{}",third);
}
// error[E0502]: cannot borrow `v1` as mutable because it is also borrowed as immutable
遍历Vector中的值
fn main(){
// 初始值创建Vector
let mut v1 = vec![1,2,3,4,5]; // macro
// 不可变引用
for i in &mut v1{
*i += 50;
}
for i in v1{
println!("{}",i);
}
for (i,elem) in v1.iter().enumerate(){
println!("{}=={}",i,elem);
}
}
Vector实例
// Vector与enum搭配
enum SpreadsheetCell{
Int(i32),
Float(f64),
Text(String),
}
fn main(){
// 模拟存放多种类型
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Float(10.0),
SpreadsheetCell::Text(String::from("apple")),
];
}
String
字符串是什么?
- Byte的集合
- 提供了一些方法,可以将字节解析为文本
在Rust核心语言层面,只有一个字符串类型:字符串切片str
(或&str
)
字符串切片:对存储在其他地方,UTF-8编码的字符串的引用。
String类型:
- 来自标准库而不是核心语言
- 可增长,可修改,可拥有
- UTF-8编码
Rust的标准库还包括了很多其他的字符串类型,例如:OsString,OsStr,CString,CStr.
- String vs Str后缀:拥有或借用变体
- 可存储不同编码的文本或在内存中以不同的形式展现
创建一个新的字符串(String)
let mut s = String::new();
使用初始值(字符串字面值)来创建String
to_string()
方法,可用于实现Display trait的类型,包括在字符串字面值
fn main(){
let data = "hello world";
let s1 = data.to_string();
let s2 = "hello world".to_string();
let s3 = String::from("hello world");
}
更新String
push_str
方法:把一个字符串切片附加到String
fn main(){
let mut s = String::from("apple");
s.push_str(" banana");
println!("{}",s);
}
push
方法:把单个字符附加到String上
fn main(){
let mut s = String::from("apple");
s.push_str(" banana");
s.push('p');
println!("{}",s);
}
+
连接字符串
fn main(){
let s1 = String::from("hello ");
let s2 = String::from("world");
let s3 = s1 + &s2;
// s1被所有权被转移
// println!("{}",s1);
println!("{}",s2);
println!("{}",s3);
}
format!
:连接多个字符串
fn main(){
let s1 = String::from("hello ");
let s2 = String::from("world");
let s3 = String::from("yes");
let newstr = format!("{}-{}-{}",s1,s2,s3);
println!("{}",newstr);
}
对String按索引的形式进行访问(不可)
- 按索引访问String的某部分,会报错
- Rust的字符串不支持索引语法访问
// String的本质是字符的向量
// 存放Unicode标量值
pub struct String {
vec: Vec<u8>,
}
字节、标量值和字形簇
Rust有三种看待字符串的方法
- 字节
- 标量值
- 字形簇(接近于单词)
fn main(){
let s = String::from("你好世界");
// 字节
for b in s.bytes(){
println!("{}",b);
}
/* 228
* 189
* 160
* 229
* 165
* 189
* 228
* 184
* 150
* 231
* 149
* 140
*/
// 标量
for b in s.chars(){
println!("{}",b);
}
/* 你
* 好
* 世
* 界
*/
// rust标准库没有提供字形簇的获取方法
}
切割String
可以使用[]
和一个范围来创建字符串的切片
切割时要沿着字符串边界进行切割,不然会panic
fn main(){
let s = String::from("你好世界");
let news = &s[0..2];
println!("{}",news);
}
// thread 'main' panicked at 'byte index 2 is not a char boundary; it is inside '你' (bytes 0..3) of `你好世界`', src/main.rs:3:17
// note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
HaspMap<K,V>
创建HaspMap
- 创建新的HashMap:
new()
函数 - 添加数据:
insert()
方法
use std::collections::HashMap;
fn main(){
let mut scores: HashMap<String, i32> = HashMap::new();
scores.insert(String::from("violet"),12);
}
HaspMap是同构的
- 所有的K必须是同一种类型
- 所有的V必须是同一类型
collect
方法
在元素类型为Tuple的Vector使用collect
方法,可以组建一个HashMap
- 要求Tuple有两个值:一个作为K,一个作为V
collect
方法可以把数据整合成多个集合类型,包括HashMap
use std::collections::HashMap;
fn main(){
let teams = vec![String::from("Blue"),String::from("Yellow")];
let inital_scores = vec![10,50];
let scores: HashMap<_,_> = teams.iter().zip(inital_scores.iter()).collect();
}
HashMap和所有权
- 对于实现Copy trait的类型(例如i32),值会被复制到HashMap中
- 对于拥有所有权的值(例如String),值会被移动,所有权会转移给HashMap
use std::collections::HashMap;
fn main(){
let name = String::from("overflow");
let value = String::from("Blue");
let mut s = HashMap::new();
s.insert(name,value);
// 错误,name被移动
// println!("{}",name);
// println!("{}",value);
}
访问HashMap
值
get()
传入key,返回Option<T>
use std::collections::HashMap;
fn main(){
let mut scores = HashMap::new();
scores.insert(String::from("Blue"),10);
scores.insert(String::from("Yellow"),50);
let score = scores.get(&String::from("Blue"));
match score{
Some(val)=>println!("{}",val),
None => println!("None"),
}
}
遍历HashMap
use std::collections::HashMap;
fn main(){
let mut scores = HashMap::new();
scores.insert(String::from("Blue"),10);
scores.insert(String::from("Yellow"),50);
for(k,v) in &scores{
println!("{}:{}",k,v);
}
}
更新HashMap
HashMap 大小可变
每个K只能对应一个V
更新HashMap中的数据
-
K已经存在,对应一个V
- 替换现在的V
- 保留现在的V,忽略新的V
- 合并现在的V和新的V
use std::collections::HashMap; fn main(){ let mut scores = HashMap::new(); // 替换现有的V scores.insert(String::from("Blue"),10); scores.insert(String::from("Blue"),50); // 只有K不对应任何值,才插入V // entry方法: 检查指定的K是否对应一个V scores.entry(String::from("Yellow")).or_insert(50); scores.entry(String::from("Yellow")).or_insert(60); for(k,v) in &scores{ println!("{}:{}",k,v); } }
更新现有的V
use std::collections::HashMap;
fn main(){
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace(){
let count = map.entry(word).or_insert(0);
*count +=1;
}
println!("{:#?}",map);
}
// entry()返回值
/// Returns a reference to the value corresponding to the key.
Hash函数
默认情况下,HashMap
使用加密功能强大的Hash函数,可以抵抗Dos攻击
- 不是可用最快的Hash算法
- 具有较好的安全性
可以指定不同的hasher来切换到另一个函数
- hasher是实现了BuildHasher trait的类型
错误处理
错误的分类
- 可恢复错误
- 例如文件未找到,可再次尝试
- 不可恢复错误
- bug,例如访问的索引超出范围
Rust没有类似异常的机制
- 可恢复错误:
Rusult<T,E>
- 不可恢复错误:
panic!
宏
panic!
不可恢复的错误
当panic!
宏执行
- 你的程序会打印一个错误信息
- 展开(unwind),清理调用栈(Stack)
- 退出程序
默认情况下,当panic
发生:
- 程序展开调用栈(工作量大)
- Rust沿着调用栈往回走
- 清理每个遇到的函数中的数据
- 或立即中止调用栈
- 不进行清理,直接停止程序
- 内存需要OS进行清理
想让二进制文件更小,可设置从“展开”改为“中止”
- 在
Cargo.toml
中适当的profile
部分设置:panic = 'abort'
panic!
可能出现在:
- 我们写的代码中
- 我们所依赖的代码中
通过设置环境变量RUST_BACKTRACE=1
可得到回溯信息。
Result
枚举
enum Result<T,E>{
Ok(T),
Err(E),
}
// T:操作成功时,Ok变体里返回的数据的类型
// E:操作失败时,Err变体里面返回的错误类型
处理Result的一种方式:match表达式
- Result及其变体也是prelude带入作用域
use std::fs::File;
use std::io::ErrorKind;
fn main(){
// match 表达式
let f =match File::open("hello.txt"){
Ok(file)=>file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt"){
Ok(fc) => fc,
Err(e) => panic!("Error creating file {:?}",e),
},
other_error => panic!("Error opening the file: {:?}", other_error),
},
};
// unwarp 表达式
let f = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error|{
panic!("Error creating file :{:?}", error);
})
} else {
panic!("Error opening file: {:?}", error);
}
});
// expect表达式
let f = File::open("hello.txt").expect("无法打开文件");
}
unwrap
unwarp
:match
表达式的一种快捷方式
expect
expect
:和unwrap
类似,但可制定错误信息
传播错误
- 不在函数中处理错误,将__错误返回给调用者__
use std::fs::File;
use std::io;
use std::io::Read;
fn main(){
let s = match read_username_from_file("hello.txt"){
Ok(s) => s,
Err(e) => {
panic!("the error is {:?}",e);
},
};
println!("{}",s);
}
fn read_username_from_file(path: &str) -> Result<String, io::Error>{
let f = File::open(path);
let mut f = match f{
Ok(file) => file,
Err(e) => return Err(e),
};
let mut s = String::new();
match f.read_to_string(&mut s){
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
?
运算符
?
运算符:传播错误的一种快捷方式
- 如果Result是Ok:Ok中的值就是表达式的结果,然后继续执行程序
use std::fs::File;
use std::io;
use std::io::Read;
fn main(){
let s = match read_username_from_file("hello.txt"){
Ok(s) => s,
Err(e) => {
panic!("the error is {:?}",e);
},
};
println!("{}",s);
}
fn read_username_from_file(path: &str) -> Result<String, io::Error>{
let mut f = File::open(path)?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
// 链式调用
fn read_username_from_file(path: &str) -> Result<String, io::Error>{
let mut s = String::new();
File::open(path)?.read_to_string(&mut s)?;
Ok(s)
}
?
与from
函数
Trait std::convert::From 上的from函数:
- 用于错误之间的转换
被?
所应用的错误,会隐式的被from函数处理。
当?
调用from
函数时:
- 它所接收的错误类型会被转化为当前函数所返回类型所定义的错误类型
from
用途:针对不同错误原因,返回同一种错误类型。(只要每个错误类型现实了转换为所返回错误类型的from函数)
?
运算符只能由于返回值类型为Result
的函数。
注意:
main函数的返回类型也可以是Result
什么时候应该用panic!
总结原则
- 在定义一个可能失败的函数时,优先考虑返回Result
- 否则就
panic!
错误处理的指导性建议:
- 当代码最终可能处于损坏状态时,最好使用
panic!
.损坏状态:某些假设、保证、约定或不可变性被打破。
泛型,Trait, 生命周期
泛型
泛型:提高代码复用能力
泛型是具体类型或其他属性的抽象替代:
- 你编写的代码不是最终代码,而是一种模板,里面有一些”占位符“
- 编译器在编译时将“占位符”替换为具体类型
//例如
fn largest<T>(list:&[T])->T{}
参数类型
Struct和Enum定义中的泛型
// Struct泛型
struct Point<T>{
x:T,
y:T,
}
//enum泛型
enum Result<T,E>{
Ok(T),
Err(E),
}
方法定义中的泛型
struct Point<T>{
x:T,
y:T,
}
impl<T> Point<T>{
fn x(&self)-> &T{
&self.x
}
}
// 类似于偏特化
impl Point<i32>{
fn x1(&self) -> &i32{
&self.x
}
}
两个参数
#[derive(Debug)]
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:4};
let p2 = Point{x:"Hello", y:'c'};
let p3 = p1.mixup(p2);
println!("{:#?}",p3);
}
泛型代码的性能
- 使用泛型的代码和使用具体类型的代码速度是一样的
- 单态化
- 在编译时将泛型替换成具体类型的过程
Trait
Trait告诉Rust编译器
- 某种类型具有哪些并且可以与其他类型共享的功能
Trait:抽象的定义共享行为
Trait bounds(约束):泛型类型参数指定为实现了特定行为的类型
定义一个Trait
Trait定que ding义:把方法签名放在一起,来定义实现某种目的所必须的一组行为
- 关键字
trait
- 只有方法签名,没有具体实现
trait
可以有多个方法:每个方法签名占一行,以;
结尾- 实现该
trait
的类型必须提供具体的方法实现
pub trait Summary{
fn summarize(&self) -> String;
fn foo(&self) -> String;
}
在类型上实现trait
// @File:lib.rs
pub trait Summary{
fn summarize(&self) -> String;
}
pub struct NewsArticle{
pub headline : String,
pub location : String,
pub auther : String,
pub content : String,
}
// 实现tarit
impl Summary for NewsArticle{
fn summarize(&self) -> String{
format!("{}, by {} ({})",self.headline, self.auther, self.location)
}
}
pub struct Tweet{
pub username : String,
pub content : String,
pub reply : bool,
pub retweet : bool,
}
impl Summary for Tweet{
fn summarize(&self) -> String{
format!("{}: {}",self.username, self.content)
}
}
//@File:main.rs
use rust_learn::Summary;
use rust_learn::Tweet;
fn main(){
let tweet = Tweet{
username : String::from("violet"),
content :String::from("keep learn"),
reply : false,
retweet: false,
};
println!("1 new tweet: {}", tweet.summarize());
}
实现Trait的约束
可以在某个类型上实现某个trait
的前提条件是:
- 这个类型或这个
trait
是在本地crate里定义的
无法为外部类型来实现外部的trait
- 这个限制是程序属性的一部分(也就是一致性)
- 更准确说是__孤儿原则__ : 之所以这样命名是因为父类型不存在
- 此规则确保其他人的代码不会破坏你的代码
- 如果没有这个原则,两个crate可以为同一类型实现同一个
trait
,Rust就不知道应该使用哪个实现了
默认实现
// @File:lib.rs
pub trait Summary{
fn summarize(&self) -> String{
// summarize有默认实现
String::from("(Read more ...)")
}
}
pub struct NewsArticle{
pub headline : String,
pub location : String,
pub auther : String,
pub content : String,
}
// 实现tarit
impl Summary for NewsArticle{
fn summarize( headline: String::from("this is demo"),
content: String::from("ababab"),
auther: String::from("ice"),
location: String::from("China"),&self) -> String{
// 对默认实现的重写
format!("{}, by {} ({})",self.headline, self.auther, self.location)
}
}
pub struct Tweet{
pub username : String,
pub content : String,
pub reply : bool,
pub retweet : bool,
}
impl Summary for Tweet{
// fn summarize(&self) -> String{
// format!("{}: {}",self.username, self.content)
// }
}
// @File:main.rs
use rust_learn::Summary;
use rust_learn::NewsArticle;
use rust_learn::Tweet;
fn main(){
let tweet = Tweet{
username : String::from("violet"),
content :String::from("keep learn"),
reply : false,
retweet: false,
};
let article = NewsArticle{
headline: String::from("this is demo"),
content: String::from("ababab"),
auther: String::from("ice"),
location: String::from("China"),
};
println!("1 new tweet: {}", tweet.summarize());
println!("1 new article: {}", article.summarize());
}
/************ out ************************/
// 1 new tweet: (Read more ...)
// 1 new article: this is demo, by ice (China)
默认实现的方法可以调用trait
中的其他方法,即使这些方法没有默认实现
// @File:lib.rs
pub trait Summary{
fn summarize(&self) -> String{
format!("Read more from{}...", self.summarize_auther())
}
// summarize_auther没有默认实现
fn summarize_auther(&self) -> String;
}
pub struct NewsArticle{
pub headline : String,
pub location : String,
pub auther : String,
pub content : String,
}
// 实现tarit
impl Summary for NewsArticle{
//只有实现summarize_auther才可调用summarize
fn summarize_auther(&self) -> String{
format!("@{}",self.auther)
}
}
// @File:main.rs
use rust_learn::Summary;
use rust_learn::NewsArticle;
fn main(){
let article = NewsArticle{
headline: String::from("this is demo"),
content: String::from("ababab"),
auther: String::from("ice"),
location: String::from("China"),
};
println!("1 new article: {}", article.summarize());
}
trait
参数
-
impl trait
语法:适用于简单情况// item为实现了Summary的struct pub fn notify(item: impl Summary){ println!("Breaking news! {}", item.summarize()); }
-
trait bound
:语法,可用于复杂情况pub fn notify<T:Summary>(item: T){ println!("Breaking news! {}", item.summarize()); }
-
使用
+
指定多个trait bound
use std::fmt:Display pub fn notify(item: impl Summary + Display){ println!("Breaking news! {}", item.summarize()); } pub fn notify<T:Summary+Display>(item: T){ println!("Breaking news! {}", item.summarize()); }
-
trait bound
使用where
字句pub fn notify<T:summary+display, U:clone+debug>(a:T, b:U) -> String{ println!("breaking news! {}", a.summarize()) } pub fn notify<T, U>(a:T, b:U) -> String where T:Summary + Display, U:Clone + Debug, { println!("Breaking news! {}", a.summarize()) }
-
实现
trait
作为返回类型-
impl trait
语法impl trait
语法只能返回确认的同一种类型,返回可能不同类型的代码会报错(编译期确定?)
// impl trait pub fn notify(s:&str) -> impl Summary{ NewsArticle{ headline: String::from("this is demo"), content: String::from("ababab"), auther: String::from("ice"), location: String::from("China"), } }
-
例子:实现max()
函数
fn max<T : PartialOrd+ Clone>(list:&[T]) -> &T{
let mut max = &list[0];
for item in list.iter() {
if item > max{
max = item;
}
}
max
}
fn main(){
let str_list = vec![String::from("hello"),String::from("world")];
let res = max(&str_list);
println!("the res is {}",res);
}
使用trait bound
有条件的实现方法
在使用泛型类型参数的impl
块上使用trait bound
,我们可以有条件的为实现了特定trait
的类型实现方法
use std::fmt::Display;
struct Pair<T>{
x:T,
y:T,
}
impl<T> Pair<T>{
fn new(x:T, y:T) -> Self{
Self{x,y}
}
}
// 只有在实现了这样的条件的T才会有这种方法
impl<T: Display+PartialOrd> Pair<T>{
fn cmp_display(&self){
if self.x >= self.y{
println!("the max num is {}",self.x);
} else{
println!("the max num is {}",self.y);
}
}
}
也可以为实现了其他Trait
的任意类型有条件的实现某个Trait.
为满足trait bound
的所有类型上实现Trait
叫做覆盖实现(blanket implememtations)
// 为所有实现了fmt::Display + ?Sized的类型实现 ToString
impl<T: fmt::Display + ?Sized> ToString for T {
// A common guideline is to not inline generic functions. However,
// removing `#[inline]` from this method causes non-negligible regressions.
// See <https://github.com/rust-lang/rust/pull/74852>, the last attempt
// to try to remove it.
#[inline]
default fn to_string(&self) -> String {
let mut buf = String::new();
let mut formatter = core::fmt::Formatter::new(&mut buf);
// Bypass format_args!() to avoid write_str with zero-length strs
fmt::Display::fmt(self, &mut formatter)
.expect("a Display implementation returned an error unexpectedly");
buf
}
}
生命周期
Rust每个引用都有自己的生命周期(引用保持有效的作用域)
大多数情况下,生命周期是隐式的,可被推导的
当引用的生命周期可能以不同的方式相互关联时:手动标注生命周期
生命周期——避免悬垂引用
借用检查器
fn main(){
let r;
{
let x = 1;
r = &x;
}
println!{"{}",r};
}
// 错误:悬垂引用
生命周期标注语法
-
生命周期的标注并不会改变引用的生命周期的长度
-
当指定了泛型生命周期参数,函数可以接受带有任何生命周期的引用
-
生命周期的标注:描述了多个引用的生命周期间的关系,但不影响生命周期
-
语法
- 以
'
开头 - 通常全小写且非常短
- 很多人使用
'a
- 标注位置:在引用的
&
符号后,使用空格将标注和引用分开
&i32 //一个引用 &'a i32 //带有显式生命周期的引用 &'a mut i32 // 带有显式生命周期的可变引用
- 以
函数签名中的生命周期标注
-
泛型生命周期参数声明在:函数名和参数列表之间的
<>
里。fn main(){ let string1 = String::from("abcd"); let string2 = "xyz"; let res = longest(string1.as_str(),string2); println!("the longest string is {}",res); } // 'a的生命周期是x和y的生命周期重叠的部分 fn longest<'a>(x:&'a str, y:&'a str) -> &'a str{ if x.len() > y.len(){ x } else { y } }
深入理解生命周期
- 指定生命周期参数的方式依赖于函数所做的事情
- 从函数返回引用时,返回类型的生命周期参数需要与其中一个参数的生命周期匹配
- 如果返回的引用没有指向任何参数,那么它只能引用函数内创建的值:
- 这就是悬垂引用:该值在函数结束时就走出了作用域
struct定义中的生命周期标注
struct里面可包括
- 自持有的类型
- 引用:需要在每个引用上添加生命周期标注
struct ImportantExcept<'a>{
part: &'a str
}
fn main(){
let novel = String::from("Call me Ishmael.Some years ago...");
let first = novel.split('.').next().expect("could not found a .");
let i = ImportantExcept{
part: first
}
}
生命周期的省略
在Rust引用分析中所编入的模式称为__生命周期省略规则__
- 这些规则无需开发者遵守,是一些特殊情况,由编译器考虑,如果你的代码符合这些情况,那么就无需显示标注生命周期
- 生命周期省略规则不会提供完整的推断
- 如果应用规则后,引用的生命周期依然模糊不清,就会编译错误
生命周期省略的三个规则
- 适用于
fn
定义和impl
块
- 每个引用类型的参数都有自己的生命周期(应用于输入生命周期)
- 如果只有一个输入生命周期参数,那么该生命周期被赋给所有的输出生命周期参数(应用于输出生命周期)
- 如果有多个输入生命周期参数,但其中一个是
&self
或者&mut self
(是方法),那么self的生命周期被赋给所有的输出生命周期参数(应用于输出生命周期)
方法定义中的生命周期标注
在struct上使用生命周期实现方法,语法和泛型参数语法一样
在哪里声明和使用生命周期参数,依赖于:生命周期参数是否和字段、方法的参数和返回值有关
struct字段的生命周期名:
- 在impl后声明
- 在struct名后使用
- 这些生命周期是struct类型的一部分
impl块内的方法签名中:
- 引用必须绑定于struct字段引用的生命周期,或者引用是独立的也可以
- 生命周期省略规则经常使得方法中的生命周期标注不是必须的
struct ImportantExcept<'a>{
part: &'a str
}
impl<'a> ImportantExcept<'a>{
fn level(&self) -> i32{
3
}
fn announce_and_return_part(&self,announcement: &str ) -> &str{
println!("Attention please: {}",announcement);
self.part
}
}
静态生命周期
'static
是一种特殊的生命周期:整个程序的持续时间
- 所有的字符串字面值都拥有
'static
生命周期 - 在为引用指定
'static
生命周期时要三四:- 是否需要引用在程序整个生命周期内都存活
use std::fmt::Display;
fn longest_with_an_announcement<'a, T>(x: &'a str, y:&'a str, ann: T) -> &'a str
where
T:Display,
{
println!("Announcement! {}",ann);
if x.len() > y.len(){
x
}else{
y
}
}
编写自动化测试
测试函数体(通常)执行的3个操作(3A操作):
- 准备数据/状态
- 运行时测试的代码
- 断言(Assert)的结果
解剖测试函数
测试函数需要使用test
属性(attribute)进行标注
- Attribute就是一段Rust代码的元数据
- 在函数上面加上
#[test]
,可把函数变成测试函数 - 使用
cargo test
命令运行所有测试函数
当使用cargo创建library项目时,会生成一个test module, 里面有一个test函数
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
let result = 2 + 2;
assert_eq!(result, 4);
}
}
如果测试引起panic!
则测试失败
断言(Assert)
使用assert!
宏检查测试结果
assert!
宏,来自标准库,用于确定某个状态是否为true
#[derive(Debug)]
pub struct Rect{
length: u32,
width : u32,
}
impl Rect {
pub fn can_hold(&self, other: &Rect) -> bool{
self.length>other.length && self.width > other.width
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn larger_can_hold_smaller(){
let larger = Rect{
length: 8,
width: 7,
};
let smaller = Rect{
length: 5,
width: 1,
};
assert!(larger.can_hold(&smaller));
}
}
使用assert_eq!
(相等)和assert_ne!
(不相等)测试相等性
- 实际上,它们使用的就是
==
和!=
运算符 - 断言失败:自动打印出两个参数的值
- 使用debug格式打印参数
- 要求参数实现了
PartialEq
和Debug Traits
(所有的基本类型和标准库里的大部分让你类型都实现了)
自定义错误信息
可以向assert!
、assert_eq!
和assert_ne!
添加可选的自定义消息
-
assert!
:第一个参数必填,自定义消息作为第2个参数 -
assert_eq!
和assert_ne!
:前两个参数必填,自定义消息作为第三个参数 -
自定义消息参数会传递给
format!
宏,可以使用{}
占位符pub fn greeting(name: &str) -> String{ format!("hello {}",name) } #[cfg(test)] mod tests { use super::*; #[test] fn greeting_contain_name(){ let res = greeting("Carol"); assert!(res.contains("Carol"), "Greeting didn't contain name, vlaue was '{}'", res); } }
用should_panic
检查恐慌
验证错误处理的情况
- 除了检查代码是否返回期望的正确的值之外,还需要检查代码是否按照期望处理错误。
- 可以验证代码在特定情况下是否发生了panic
should_panic
属性(attribute)
-
函数
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 between 1 and 100, got{}." ,value}; } Guess{value} } } #[cfg(test)] mod test{ use super::*; #[test] #[should_panic] fn greater_than_100(){ Guess::new(200); } }
让should_panic
更精确
pub struct Guess{
value: u32,
}
impl Guess{
pub fn new(value :u32) -> Guess{
if value < 1 {
panic!("Guess value must be greater than 1 , got{}." ,value);
} else if value > 100{
panic!("Guess value must be less than 100, got{}", value);
}
Guess{value}
}
}
#[cfg(test)]
mod test{
use super::*;
#[test]
//与panic内容进行匹配
#[should_panic(expected = "Guess value must be less than 100")]
fn greater_than_100(){
Guess::new(0);
}
}
在测试中使用Result<T,U>
无需panic,可使用Result<T,U>作为返回类型编写测试
-
返回Ok:测试通过
-
返回Err:测试失败
#[cfg(test)] mod test{ #[test] fn it_works() -> Result<(),String>{ if 2+2 == 3{ Ok(()) } else{ Err(String::from("two plus does not equal four")) } } }
不要再使用
Result<T,U>
编写的测试上标注#[should_panic]
控制测试如何运行
改变cargo test
的行为:添加命令行参数
默认行为
- 并行运行
- 所有测试
- 捕获(不显示)所有输出,使读取与测试结果相关的输出更容易
命令行参数:
- 针对
cargo test
的参数:紧跟cargo test
后 - 针对测试可执行程序:放在–之后
-
默认并行运行测试。你应该确保:
-
测试不能相互依赖,
-
不依赖任何共享的状态,包括依赖共享的环境,比如当前工作目录或者环境变量。
如果你不希望测试并行运行,或者想要更加精确的控制线程的数量,可以传递
--test-threads
参数和希望使用线程的数量给测试二进制文件。例如: -
cargo test -- --test-threads=1
-
显式函数输出
- 默认情况下,当测试通过时,Rust 的测试库会截获打印到标准输出的所有内容
- 比如在测试中调用了
println!
- 测试通过了,我们将不会在终端看到
println!
的输出:只会看到说明测试通过的提示行。 - 如果测试失败了,则会看到所有标准输出和其他错误信息。
- 测试通过了,我们将不会在终端看到
如果你希望也能看到通过的测试中打印的值,也可以在结尾加上 --show-output
告诉 Rust 显示成功测试的输出。
cargo test -- --show-output
-
通过指定名字来运行部分测试
你可以向
cargo test
传递所希望运行的测试名称的参数来选择运行哪些测试。pub fn add_two(a: i32) -> i32 { a + 2 } #[cfg(test)] mod tests { use super::*; #[test] fn add_two_and_two() { assert_eq!(4, add_two(2)); } #[test] fn add_three_and_two() { assert_eq!(5, add_two(3)); } #[test] fn one_hundred() { assert_eq!(102, add_two(100)); } } // 运行单个测试:指定测试名 // cargo test one_hundred // 运行多个测试:指定测试名的一部分(模块名也可以) // cargo test add // <--- 将测试 add_two_and_two()和add_three_and_two()
忽略测试
忽略某些测试,运行剩余测试
-
ignore
属性(attrbute)#[test] fn it_works() { assert_eq!(2 + 2, 4); } #[test] //忽略这个测试 #[ignore] fn expensive_test() { // 需要运行一个小时的代码 } // 运行被忽略的测试 // cargo test -- --ignored
测试的组织
Rust对测试的分类
- 单元测试
- 集成测试
单元测试
- 小、专注
- 一次针对一个模块进行测试
- 可测试private接口
集成测试
- 在库外部和其他代码一样使用你的代码
单元测试
#[cfg(test)]
标注
- test模块上的
#[cfg(test)]
标注- 只有运行cargo test才会编译和运行代码
- 运行cargo build则不会
测试私有函数
Rust 的私有性规则确实允许你测试私有函数。
pub fn add_two(a: i32) -> i32 {
internal_adder(a, 2)
}
// 私有的函数
fn internal_adder(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn internal() {
assert_eq!(4, internal_adder(2, 2));
}
}
集成测试
在Rust中,集成测试完全位于被测试库的外部
目的:测试被测试库的多个部分是否能正确工作在一起
集成测试的覆盖率很重要
test
目录
- 创建
tests
目录 tests
目录下的每个测试文件都是单独的一个crate
// @File: addr/src/lib.rs
pub fn add_two(a: i32) -> i32 {
internal_adder(a, 2)
}
// 私有的函数
fn internal_adder(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn internal() {
assert_eq!(4, internal_adder(2, 2));
}
}
// @File: addr/tests/integration.rs
use addr;
#[test]
fn it_add_to(){
assert_eq!(4,addr::add_two(2));
}
// cargo test
// 如果要只运行integration_test
// cargo test --test integration_test
集成测试下的子模块
- tests目录下每个文件被编译成单独的crate,这些文件不共享行为(与src下的文件规则不同)
- tests下面的子目录不会被视为单独的crate进行编译
// @File: addr/tests/integration.rs
use addr;
mod common;
#[test]
fn it_add_to(){
common::setup();
assert_eq!(4,addr::add_two(2));
}
// @File: addr/tests/common/mod.rs
pub fn setup(){}
针对binary crate的集成测试
- 如果项目是binary crate 只含有
src/mian.rs
没有src/lib.rs
:- 不能在
tests
目录下创建集成测试,无法把mian.rs的函数导入作用域 - 只有library crate才能暴露函数给其他crate用
- binary crate 意为着独立运行
- 不能在
项目实例——命令行程序
二进制程序关注点分离的指导性原则
- 将程序拆分成main.rs和lib.rs,将业务逻辑放入lib.rs
- 当命令解析逻辑较少时,将他放在main.rs也可以
- 当命令行逻辑变复杂时,需要将它从main.rs提取到lib.rs
测试驱动开发TDD(Test-Drive Development)
- 编写一个失败的测试,并运行它以确保它失败的原因是你所期望的。
- 编写或修改足够的代码来使新的测试通过。
- 重构刚刚增加或修改的代码,并确保测试仍然能通过。
- 从步骤 1 开始重复!
// @File:src/main.rs
use std::env; // collect
use minigrep::Config;
use std::process;
use minigrep::*;
fn main() {
// 接收命令行参数
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err|{
eprintln!("Problem parsing arguments : {}",err);
process::exit(1)
});
println!("{}", format!("############# search <{}> in <{} ###############", config.query, config.filename));
if let Err(e) = run(config){
eprintln!("Application error:{}",e);
process::exit(1);
}
}
// @File:src/lib.rs
use std::fs;
use std::error::Error;
use std::env;
pub struct Config{
pub query: String,
pub filename: String,
pub case_sensitive: bool,
}
pub fn run(config :Config)-> Result<(),Box<dyn Error>>{
let contents = fs::read_to_string(&config.filename)?;
let res = if config.case_sensitive{
search(&config.query, &contents)
}else{
search_case_insensitive(&config.query,&contents)
};
if res.len() == 0{
println!("Not Found!!! <{}> haven't <{}>", config.filename, config.query);
} else{
for line in res { println!("{}",line);}
}
// println!("With text: \n{}",contents);
Ok(())
}
impl Config{
pub fn new(args: &[String]) -> Result<Config,&'static str>{
if args.len() < 3{
return Err("not enough arguments");
}
Ok(
Config{
query: args[1].clone(),
filename: args[2].clone(),
case_sensitive: env::var("CASE_INSENSITIVE").is_ok(),
}
)
}
}
// 区分大小写匹配
pub fn search<'a> (query: &str, contents: &'a str) -> Vec<&'a str>{
let mut res = Vec::new();
for line in contents.lines(){
if line.contains(query){
res.push(line);
}
}
res
}
// 不区分大小写匹配
pub fn search_case_insensitive<'a> (query: &str, contents: &'a str) -> Vec<&'a str>{
let mut res = Vec::new();
let query=query.to_lowercase();
for line in contents.lines(){
if line.to_lowercase().contains(&query){
res.push(line);
}
}
res
}
// 单元测试
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive(){
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
DUct";
assert_eq!(vec!["safe, fast, productive."],search(query,contents))
}
#[test]
fn case_insensitive(){
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me";
assert_eq!(vec!["Rust:","Trust me"],search_case_insensitive(query,contents))
}
}
函数式语言特性——迭代器和闭包
闭包——使用闭包创建抽象行为
- 闭包(Closures), 可以捕获其所在环境的匿名函数
- 是匿名函数
- 保存变量/作为参数
- 可以在一个地方创建闭包,然后在另一个上下人文中调用闭包来完成运算
新建闭包
例子——生成自定义运动计划的程序
use std::thread;
use std::time::Duration;
fn main() {
let simulated_user_specified_value = 10;
let simulated_random_number = 7;
generate_workout(simulated_user_specified_value,simulated_random_number);
}
fn generate_workout(intensity: u32, random_number: u32){
// 闭包
let expensive_closure = |num|{
println!("claculation slowly.... ");
thread::sleep(Duration::from_secs(2));
num
};
if intensity < 25 {
println!("Today, do {} pushups!",expensive_closure(intensity));
println!("Next. do {} situps!",expensive_closure(intensity));
}else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!(
"Today, run for {} minutes!",expensive_closure(intensity));
}
}
}
闭包的类型推断和标注
- 闭包不要求标注类型和返回值类型
- 闭包通常很短小,只在狭小的上下文中工作,编译器通常能推断出类型
- 可以手动添加类型标注
// 手动标注类型
let expensive_closure = |num :u32| -> u32{
println!("claculation slowly.... ");
thread::sleep(Duration::from_secs(2));
num
};
函数和闭包的定义语法
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
注意:闭包的定义最终只会为参数/返回值推断出__唯一一种类型__.
使用泛型参数和Fn Trait来存储闭包
创建一个struct,它持有闭包及其调用结果
- 只有在需要结果时才执行闭包
- 可缓存结果
这种模式通常叫做记忆化或延迟计算
如何让结构体持有闭包
- struct定义需要知道所有字段类型
- 需要指出闭包类型
- 每个闭包实例都有自己唯一的匿名类型,即使两个闭包签名完全一样
Fn Trait
Fn Trait
由标准库提供
所有闭包都至少实现了以下trait
之一
Fn
FnMut
FnOnce
use std::thread;
use std::time::Duration;
struct Cacher<T>
where T: Fn(u32) -> u32
{
calculation : T,
value : Option<u32>,
}
// Fn trait
impl<T> Cacher<T>
where T: Fn(u32) -> u32
{
fn new(calculation: T)-> Cacher<T>{
Cacher{
calculation,
value: None,
}
}
fn value(&mut self ,arg: u32) -> u32{
match self.value{
Some(v) => v,
None => {
let v =(self.calculation)(arg);
self.value = Some(v);
v
}
}
}
}
fn main() {
let simulated_user_specified_value = 10;
let simulated_ scrcpyrandom_number = 7;
generate_workout(simulated_user_specified_value,simulated_random_number);
}
// 解决函数只调用一次的问题(类似与单例)
fn generate_workout(intensity: u32, random_number: u32){
let mut expensive_closure = Cacher::new(|num|{
println!("claculation slowly.... ");
thread::sleep(Duration::from_secs(2));
num}
);
if intensity < 25 {
println!("Today, do {} pushups!",expensive_closure.value(intensity));
println!("Next. do {} situps!",expensive_closure.value(intensity));
}else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!("Today, run for {} minutes!",expensive_closure.value(intensity));
}
}
}
使用缓存器(Cacher)实现的限制
- Cacher实例针对不同的参数
arg
,value
方法总会得到同样的值。- 可以使用
HashMap
代替单个值
- 可以使用
- 只能接受一个u32类型的参数和u32类型的返回值
使用闭包捕获上下文环境
fn main(){
let x=4;
let equal_to_x = |z| z ==x;
let y = 4;
assert!(equal_to_x(y));
}
闭包可以捕获他们所在的环境
- 闭包可以访问定义他的作用域内的变量,而普通函数不能
- 会产生内存开销
闭包从环境捕获值的方式:
与函数获得参数的三种方式一样
- 取得所有权:FnOnce
- 可变借用:FnMut
- 不可变借用:Fn
创建闭包时,通过闭包对环境值的使用,Rust推断出具体使用哪个trait:
- 所有的闭包都实现了FnOnce
- 没有移动捕获变量的闭包实现了FnMut
- 无需可变访问捕获变量的闭包实现了Fn
move
关键字
- 在参数列表前使用
move
关键字,可以强制闭包取得它所使用的环境值的所有权- 当将闭包传递给新线程以移动数据使其归新线程所有时,此技术最为有用
fn main(){
let x = vec![1,2,3];
let equal_to_x = move |z| z ==x;
// value borrowed here after move
println!("can't use x here: {:?}",x);
let y = vec![1,2,3];
assert!(equal_to_x(y));
}
最佳实践
当指定Fn Trait bound之一时,首先用Fn,基于闭包体里的情况,如果需要FnOnce或FnMut,编译器会告诉你。
迭代器
迭代器模式:对一系列项执行某些任务
迭代器负责:
- 遍历每个项
- 确定序列(遍历)何时完成
Rust的迭代器
- 懒惰的:除非调用消费迭代器的方法,否则迭代器本身没有任何效果
fn main() {
let v1 =vec![1,2,3];
// 获取v1的迭代器
let v1_iter = v1.iter();
for val in v1_iter{
println!("{}",val);
}
}
iterator trait
和next
方法
所有迭代器均实现了iterator trait
iterator trait
定义于标准库下,定义大致如下:
pub trait Iterator {
/// The type of the elements being iterated over.
#[stable(feature = "rust1", since = "1.0.0")]
type Item;
#[lang = "next"]
#[stable(feature = "rust1", since = "1.0.0")]
fn next(&mut self) -> Option<Self::Item>;
}
-
type item
和Self::item
定义了与该trait
关联的类型- 实现
iterator trait
需要你定义一个item
类型,它用于next
方法的返回类型(迭代器的返回类型)
- 实现
-
iterator trait
仅要求实现一个方法:next -
next
- 每次返回迭代器中的一项
- 返回结果包裹在
some
中 - 迭代结书,返回
None
- 可直接在迭代器上调用
next
方法
fn main() { let v1 =vec![1,2,3]; // 获取v1的迭代器 // 注意这里要是可变的 let mut v1_iter = v1.iter(); assert_eq!(v1_iter.next(),Some(&1)); assert_eq!(v1_iter.next(),Some(&2)); assert_eq!(v1_iter.next(),Some(&3)); // 这里不加mut的原因是for in 取得了v1_iter的所有权,并在其内部把他变为可变的 for val in v1_iter{ println!("{}",val); } }
-
注意:
iter
获得的元素的不可变引用(并不是指迭代器本身不可变)
几种迭代方法
iter
方法:在不可变引用上创建迭代器into_iter
方法:创建的迭代器会获得所有权iter_mut
方法:迭代可变的引用
消耗和产生迭代器
消耗迭代器的方法
在标准库中iterator trait
有一些带默认实现的方法,其中有一些方法会调用next
方法
- 实现
iterator trait
时必须实现next
方法的原因之一
调用next
的方法的方法叫做“消耗型迭代器“
-
因为调用他们会把迭代器消耗尽
-
例如:
sum
方法(会耗尽迭代器)-
取得迭代器的所有权
-
通过反复调用
next
,遍历所有元素 -
每次迭代,把当前元素添加到一个总和里,迭代结束,返回总和
fn main() { let v1 =vec![1,2,3]; // 获取v1的迭代器 let v1_iter = v1.iter(); let res : i32= v1_iter.sum(); println!("{}",res); }
-
产生其他迭代器的方法
定义在iterator trait
上的另外一些方法叫做“迭代器适配器”
- 把迭代器转换成不同种类的迭代器
可以通过链式调用使用多个迭代器适配器来执行复杂的操作,这种操作可读性较高
例如map
-
接受一个闭包,闭包作用于每个元素
-
产生一个新的迭代器
fn main() { let v1 =vec![1,2,3]; let v2: Vec<_> = v1.iter().map(|x| x+1).collect(); println!("{:?}",v2); }
使用闭包捕获环境
filter
方法
-
接收一个闭包
-
这个闭包在遍历迭代器的每个元素时,返回
bool
类型 -
如果闭包返回
ture
,当前元素将会包含在filter
产生的迭代器中 -
如果闭包返回
false
,当前元素将不会包含在filter
产生的迭代器中#[derive(PartialEq,Debug)] pub struct Shoe{ size: u32, style : String, } pub fn shoes_in_my_size(shoes : Vec<Shoe>, shoe_size : u32) -> Vec<Shoe>{ shoes.into_iter().filter(|x| x.size == shoe_size).collect() } #[test] fn filter_by_size(){ let shoe = vec![ Shoe{ size:10, style:String::from("snaker"), }, Shoe{ size:13, style:String::from("apple"), }, Shoe{ size:10, style:String::from("boot"), }, ]; let in_my_size = shoes_in_my_size(shoe,10); assert_eq!( in_my_size, vec![ Shoe{ size:10, style:String::from("snaker"), }, Shoe{ size:10, style:String::from("boot"), },]); }
创建自定义迭代器
- 要实现
next
方法
struct Counter{
count: u32,
}
impl Counter{
fn new()->Counter{
Counter{
count:0,
}
}
}
impl Iterator for Counter{
type Item = u32;
fn next(&mut self)->Option<Self::Item>{
if self.count < 5{
self.count += 1;
Some(self.count)
} else {
None
}
}
}
改进命令行程序
// @File: minigrep/src/lib.rs
use std::fs;
use std::error::Error;
use std::env;
pub struct Config{
pub query: String,
pub filename: String,
pub case_sensitive: bool,
}
pub fn run(config :Config)-> Result<(),Box<dyn Error>>{
let contents = fs::read_to_string(&config.filename)?;
let res = if config.case_sensitive{
search(&config.query, &contents)
}else{
search_case_insensitive(&config.query,&contents)
};
if res.len() == 0{
println!("Not Found!!! <{}> haven't <{}>", config.filename, config.query);
} else{
for line in res { println!("{}",line);}
}
// println!("With text: \n{}",contents);
Ok(())
}
impl Config{
pub fn new(mut args : std::env::Args) -> Result<Config,&'static str>{
if args.len() < 3{
return Err("not enough arguments");
}
args.next();
let query = match args.next(){
Some(s) => s,
None => return Err("Didn't get a quary string"),
};
let filename = match args.next(){
Some(s) => s,
None => return Err("Didn't get a filename"),
};
Ok(
Config{
query,
filename,
case_sensitive: env::var("CASE_INSENSITIVE").is_ok(),
}
)
}
}
// 区分大小写匹配
pub fn search<'a> (query: &str, contents: &'a str) -> Vec<&'a str>{
// 使用迭代器
contents.lines().filter(|line| {line.contains(query)}).collect()
}
// 不区分大小写匹配
pub fn search_case_insensitive<'a> (query: &str, contents: &'a str) -> Vec<&'a str>{
let query=query.to_lowercase();
// 使用迭代器
contents.lines().filter(|line| line.to_lowercase().contains(&query)).collect()
}
// @File: minigrep/src/main.rs
use std::env; // collect
use minigrep::Config;
use std::process;
use minigrep::*;
fn main() {
// 接收命令行参数
// let args: Vec<String> = env::args().collect();
// 改为传入迭代器
let config = Config::new(env::args()).unwrap_or_else(|err|{
eprintln!("Problem parsing arguments : {}",err);
process::exit(1)
});
println!("{}", format!("############# search <{}> in <{} ###############", config.query, config.filename));
if let Err(e) = run(config){
eprintln!("Application error:{}",e);
process::exit(1);
}
}
cargo
和crates.io
使用release proflie配置来自定义构建
release proflie
- 预定义的
- 可自定义:可使用不同的配置,对代码编译有更多的控制
每个profile的配置都独立于其他的profile
Cargo主要的两个profile
- dev profile: 适用于开发,
cargo build
- release profile:适用于发布,
cargo build --release
自定义profile
-
如果想自定义profile
-
可以在
Cargo.toml
里添加[profile.xxx]
区域,在里面覆盖默认配置的子集[profile.dev] # 优化等级 opt-level = 0 [profile.release] opt-level = 3
-
crates.io
- 可以通过发布包来共享你的代码
文档注释
-
用于生成文档
-
生成HTML文档
-
显示公共API的文档注释,如何使用API
-
使用
///
-
支持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
生成文档(实际上他会运行rustdoc
工具),文档放在target/doc
目录下 -
cargo doc -open
- 构建当前
crate
文档(也包含crate
依赖项文档 - 在浏览器中打开文档
- 构建当前
示例 中使用了
# Examples
Markdown 标题在 HTML 中创建了一个以 “Examples” 为标题的部分。其他一些 crate 作者经常在文档注释中使用的部分有:- Panics:这个函数可能会
panic!
的场景。并不希望程序崩溃的函数调用者应该确保他们不会在这些情况下调用此函数。 - Errors:如果这个函数返回
Result
,此部分描述可能会出现何种错误以及什么情况会造成这些错误,这有助于调用者编写代码来采用不同的方式处理不同的错误。 - Safety:如果这个函数使用
unsafe
代码,这一部分应该会涉及到期望函数调用者支持的确保unsafe
块中代码正常工作的不变条件(invariants)。
文档注释作为测试
- 示例代码块的附加值
- 运行
cargo test
:将把文档注释中的示例代码作为测试来运行
- 运行
为包含注释的项添加文档注释
-
符号:
//!
-
这类注释通常描述
crate
和模块 -
一个模块内,将
crate
或模块作为一个整体进行记录//! # My Crate //! //! `my_crate` is a collection of utilities to make performing certain //! calculations more convenient. /// Adds one to the number given. // --snip-- /// /// # Examples /// /// ``` /// let arg = 5; /// let answer = my_crate::add_one(arg); /// /// assert_eq!(6, answer); /// ``` pub fn add_one(x: i32) -> i32 { x + 1 }
-
使用 pub use
导出合适的公有 API
问题: crate
的程序结构在开发时对于开发者很合理,但对于使用者,不够方便
例如:
- 麻烦:
my_crate::some_module::another_module::UsefulType;
- 方便:
use my_crate::UsefulType;
解决方法:
- 不需要重新组织内部代码结构
- 使用
pub use
:可以重新导出,创建一个和内部私有结构不同的对外公共结构
例子:
-
不使用
pub use
// @File : example/src/lib.rs pub mod kinds { pub enum PrimaryColor{ Red, Yellow, Bule, } pub enum SecondaryColor { Orange, Green, Purple, } } pub mod utils { use crate::kinds::*; pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor { SecondaryColor::Green } } // @File : example/src/main.rs // 使用了过多层数的引入 use example::kinds::PrimaryColor; use example::utils::mix; fn main(){ let red = PrimaryColor::Red; let yellow = PrimaryColor::Yellow; mix(red,yellow); }
-
使用
pub use
导出// @File : example/src/lib.rs // 导出到顶层 pub use self::kinds::PrimaryColor; pub use self::kinds::SecondaryColor; pub use self::utils::mix; pub mod kinds { pub enum PrimaryColor{ Red, Yellow, Bule, } pub enum SecondaryColor { Orange, Green, Purple, } } pub mod utils { use crate::kinds::*; pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor { SecondaryColor::Green } } // @File : example/src/main.rs // 直接引入 use example::PrimaryColor; use example::mix; fn main(){ let red = PrimaryColor::Red; let yellow = PrimaryColor::Yellow; mix(red,yellow); }
发布package到crate.io
首先,到crate.io获取token,使用token登陆,如:
cargo login abcdefghijklmnopqrstuvwxyz012345
有了账号之后,比如说你已经有一个希望发布的 crate。在发布之前,你需要在 crate 的 Cargo.toml 文件的 [package]
部分增加一些本 crate 的元信息(metadata)。
之后用cargo publish
命令发布到crate.io
当你修改了 crate 并准备好发布新版本时,改变 Cargo.toml 中 version
所指定的值。请使用 语义化版本规则 来根据修改的类型决定下一个版本号。接着运行 cargo publish
来上传新版本。
虽然你不能删除之前版本的 crate,但是可以阻止任何将来的项目将他们加入到依赖中。这在某个版本因为这样或那样的原因被破坏的情况很有用。对于这种情况,Cargo 支持 撤回(yanking)某个版本。
撤回某个版本会阻止新项目开始依赖此版本,不过所有现存此依赖的项目仍然能够下载和依赖这个版本。从本质上说,撤回意味着所有带有 Cargo.lock 的项目的依赖不会被破坏,同时任何新生成的 Cargo.lock 将不能使用被撤回的版本。
为了撤回一个 crate,运行 cargo yank
并指定希望撤回的版本:
cargo yank --vers 1.0.1
也可以撤销撤回操作,并允许项目可以再次开始依赖某个版本,通过在命令上增加 --undo
:
cargo yank --vers 1.0.1 --undo
撤回 并没有 删除任何代码。举例来说,撤回功能并不意在删除不小心上传的秘密信息。如果出现了这种情况,请立即重新设置这些秘密信息。
Cargo工作空间
Cargo工作空间:帮助管理多个相互关联且需要协同开发的crate
Cargo工作空间是一套共享同一个Cargo.lock
和输出文件夹的包
创建工作空间
有多种方式来组建工作空间
例如:1个二进制crate,2个库crate
├── Cargo.lock
├── Cargo.toml
├── add_one
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
- 二进制crate:main函数,依赖于其他两个库crate
- 其中一个库crate提供
add_one
函数 - 另外一个库crate提供
add_two
函数
工作空间只有一个顶层Cargo.lock
,在工作空间的顶层目录
- 保证工作空间内所有的crate使用的依赖的版本都相同
- 工作空间内所有的crate相互兼容
# 顶层Cargo.toml
[workspace]
members = [
"add_one"
"adder",
]
# add_one/Cargo.toml
[dependencies]
rand = "0.8.3"
# adder/Cargo.tml
[dependencies]
add_one = { path = "../add_one" }
使用 cargo install
从 Crates.io 安装二进制文件
命令:cargo install
只能安装具有二进制目标的crate
二进制目标binary target:是一个可运行程序
- 由拥有src/mian.rs或其他被指定为二进制文件的crate生成
智能指针
指针:一个变量在内存中包含的是一个地址(指向其他数据)
Rust中最常用的指针就是“引用”
引用
- 使用
&
- 借用它指向的值
- 没有其余开销
- 最常用的指针类型
智能指针:
- 智能指针是这样的一些数据结构:
- 行为和指针相似
- 有额外的元数据和额外的功能
引用计数智能指针类型
- 记录所有者数量,使一份数据被多个所有者同时持有
- 并在没有任何所有者自动清理数据
引用和智能指针的不同
- 引用: 只借用数据
- 智能指针: 很多时候都拥有它所指向的数据
智能指针的实现:
- 智能指针通常使用
struct
实现,并且实现了:Deref
和Drop
这两个trait
Deref
:允许智能指针struct
的实例像引用一样使用Drop
:允许自定义当智能指针实例走出作用域时的代码
使用Box<T>
来指向Heap
上的数据
Box<T>
是最简单的智能指针
- 允许你在
heap
上存储数据(而不是stack
)
Box<T>
的常用场景
- 在编译时,某类型的大小无法确定。但使用该类型时,上下文却需要知道它确切的大小
- 当你有大量数据时,想移交所有权,但要确保在操作时,数据不会被复制
- 当使用某个值时,你只关心它是否实现了特定的
trait
,而不关心它的具体类型
使用Box<T>
在heap上存储数据
fn main() {
let b = Box::new(5);
println!("b = {}",b);
}
使用Box
赋能递归类型
-
在编译时,Rust需要知道一个类型所占空间大小
-
而递归类型的大小无法在编译时确定
但Box类型的大小可以确定
在递归类型中使用box
就可以解决上述问题
上面的数据类型是,函数式语言Cons List
- Cons List是来自Lisp语言的一种数据结构
- Cons List里每个成员由两个元素组成
- 当前项的值
- 下一个元素
- Cons List里最后一个元素只包含一个
Nil
值,没有下一个元素 - 与链表类似
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
// 错误,无法确定大小
enum List{
Cons(i32, List),
Nil,
}
使用Box
来获取确定大小的递归类型
Box<T>
是一个指针,Rust知道它需要多少空间
use crate::List::Cons;
use crate::List::Nil;
fn main() {
let list = Cons(1,Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
enum List{
Cons(i32, Box<List>),
Nil,
}
总结
Box<T>
- 只提供了“间接”存储和heap内存分配的功能
- 没有其他额外功能
- 没有性能开销(?)
Deref Trait
Deref结引用的意思
实现Deref Trait
使我们可以__自定义解引用运算符*
的行为__
通过实现Deref Trait
,智能指针可以像常规引用一样来处理
解引用运算符
-
常规的引用也是一种指针
fn main() { let x =5; let y = &x; assert_eq!(5,x); assert_eq!(5,*y); }
把
Box<T>
当作引用 -
Box<T>
可以代替上例中的引用fn main() { let x =5; let y = Box::new(x); assert_eq!(5,x); assert_eq!(5,*y); }
定义自己的智能指针
Box<T>
被定义成拥有一个元素的tuple struct
例子:定义MyBox<T>
实现Deref Trait
标准库中的Deref trait
要求我们实现一个deref
方法:
- 该方法借用
self
- 返回一个指向内部数据的引用
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> MyBox<T>{
fn new(x:T) -> MyBox<T>{
MyBox(x)
}
}
impl<T> Deref for MyBox<T>{
type Target = T;
fn deref(&self)-> &T{
&self.0
}
}
fn main(){
let x = 5;
let y = MyBox::new(5);
assert_eq!(5,x);
assert_eq!(5,*y);
}
函数和方法的隐式解引用转换(Deref Coercion)
隐式解引用转化(Deref Coercion)是为函数和方法提供的一种便携特性
假设T
实现了Deref Trait
:
Deref Coercion
可以把T
的引用转化成T
经过Deref操作后生成的引用
当把类型的引用传递给函数或方法时名单他的类型与定义的参数类型不匹配:
-
Deref Coercion
就会自动发生 -
编译器会对
deref
进行一系列调用,用来把它转化为所需要的参数类型- 编译期完成
use std::ops::Deref; struct MyBox<T>(T); impl<T> MyBox<T>{ fn new(x:T) -> MyBox<T>{ MyBox(x) } } impl<T> Deref for MyBox<T>{ type Target = T; fn deref(&self)-> &T{ &self.0 } } fn hello(name : &str){ println!("Hello, {}",name); } fn main(){ let x = 5; let y = MyBox::new(5); let m = MyBox::new(String::from("Rust")); // MyBox 实现了deref // &m: &MyBox<String> ---> deref &String ---> deref &str hello(&m); hello("Rust"); assert_eq!(5,x); assert_eq!(5,*y); }
解引用与可变性
可使用DerefMut trait重载可变引用的*
运算符
在类型和trait
在下列三种情况发生时,Rust会执行 Deref Coercion
-
当
T:Deref<Target=U>
,允许&T
转换成&U
-
当
T:DerefMut<Target=U>
,允许&mut
转换成&mut U
-
当
T:Deref<Target=U>
,允许&mut T
转换成&U
use std::ops::Deref; use std::ops::DerefMut; struct MyBox<T>(T); impl<T> MyBox<T>{ fn new(x:T) -> MyBox<T>{ MyBox(x) } } impl<T> DerefMut for MyBox<T>{ // type Target = T; fn deref_mut(& mut self) -> &mut T{ &mut self.0 } } fn hello(name : &mut str){ println!("Hello, {}",name); } fn main(){ let x = 5; let y = MyBox::new(5); let mut m = MyBox::new(String::from("Rust")); // MyBox 实现了deref // &m: &MyBox<String> ---> deref &String ---> deref &str hello(& mut m); // hello("Rust"); assert_eq!(5,x); assert_eq!(5,*y); }
Drop Trait
实现了Drop Trait,可以让我们自定义当值将要离开作用域时发生的动作
- 例如:文件,网络资源释放的
- 任何类型都可以实现
Drop trait
Drop trait只要求你实现drop
方法
- 参数:对
self
的可变引用
Drop Trait在预导入模块里(prelude)
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!("end");
}
// end
// Dropping CustomSmartPointer with data `other stuff` !
// Dropping CustomSmartPointer with data `my stuff` !
使用std::mem::drop
来提前drop值
- 很难直接禁用自动的
drop
功能,也没必要- Drop trait的目的就是进行自动的释放处理逻辑
- rust不允许手动调用Drop trait的drop方法
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"),
};
drop(_c);
let _d = CustomSmartPointer{
data: String::from("other stuff"),
};
println!("end");
}
// Dropping CustomSmartPointer with data `my stuff` !
// end
// Dropping CustomSmartPointer with data `other stuff` !
Rc<T>
引用计数智能指针
有时,一个值会有多个所有者
例如:一个图数据中,一个节点被多个边共有
功能类似于C++中的shared_ptr
-
需要在heap上分配数据,这些数据被程序的多个部分读取(只读),但在编译时无法确定哪个部分最后用完这些数据
-
Rc<T>
只能用于单线程场景 -
Rc<T>
不在预导入模块中 -
Rc::clone(&a)
函数:增加运用计数 -
Rc::strong_count(&a)
函数:获取引用计数(强引用shared_ptr
)- 还有
Rc::weak_count
(弱引用weak_ptr
)
- 还有
-
例子:两个List共享另一个List的所有权
use std::rc::Rc;
enum List{
Cons(i32,Rc<List>),
Nil,
}
use crate::List::{Cons,Nil};
fn main(){
let a = Rc::new(Cons(5,
Rc::new(Cons(10,
Rc::new(Nil)))));
println!("a strong = {}", Rc::strong_count(&a));
let b = Cons(3, Rc::clone(&a));
println!("a strong = {}", Rc::strong_count(&a));
{
let c = Cons(4, Rc::clone(&a));
println!("a strong = {}", Rc::strong_count(&a));
}
println!("a strong = {}", Rc::strong_count(&a));
}
// a strong = 1
// a strong = 2
// a strong = 3
// a strong = 2
Rc<T>
通过__不可变引用__,使你可以在程序不同部分之间共享__只读__数据
但是,如何允许数据变化呢?
RefCell<T>
和内部可变性
- 内部可变性是Rust的设计模式之一
- 它允许你在只持有不可变引用的前提下对数据进行修改
- 数据中使用了
unsafe
代码来绕过Rust正常的可变性和借用规则
- 数据中使用了
RefCell<T>
与Rc<T>
不同,RefCell<T>
类型代表了其持有数据的唯一所有权
RefCell<T>
和Box<T>
区别
RefCell<T> |
Box<T> |
---|---|
编译阶段强制代码遵守借用规则 | 只会在运行时检查借用规则 |
否则出现错误 | 否则触发panic |
借用规则在不同阶段进行检查的比较
编译阶段 | 运行时 |
---|---|
尽早暴露问题 | 问题暴露延后,甚至到生产环境 |
没有任何运行开销 | 因借用技术产生些许性能损失 |
对大多数场景是最佳选择 | 实现某些特定的内存安全场景(不可变环境中修改自身数据) |
是Rust默认行为 |
RefCell<T>
只能用于__单线程的场景__
选择Box<T>
,Rc<T>
,RecCell<T>
的依据
Box<T> |
Rc<T> |
RecCell<T> |
|
---|---|---|---|
同一数据的所有者 | 一个 | 多个 | 一个 |
可变性,借用检查 | 可变、不可变借用(编译时检查) | 不可变借用(编译时检查) | 可变、不可变借用(运行时检查) |
其中:即使RecCell<T>
本身不可变,但仍能修改其中存储的值
内部可变性:可变的借用一个不可变的值
RefCell<T>
两个方法
borrow
方法
- 返回智能指针
Ref<T>
,它实现了Derrf
borrow_mut
方法
- 返回智能指针
RefMut<T>
,它实现了Deref
使用RefCell<T>
在运行时记录借用信息
RefCell<T>
会记录当前存在多少个活跃的Ref<T>
和RefMut<T>
智能指针- 每次调用
borrow
方法:不可变借用计数加1 - 任何一个
Ref<T>
的值离开作用域被释放时:不可变借用技术减1 - 每次调用
borrow_mut
方法:可变借用计数加1 - 任何一个
RefMut<T>
的值离开作用域被释放时:可变借用技术减1
- 每次调用
以此技术来维护借用检查规则:
- 任何一个给定时间里,只允许拥有多个不可变借用或一个可变借用
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T:'a + Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &T, max: usize) -> LimitTracker<T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
//使用RefCell
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
// 修改不可变借用
self.sent_messages.borrow_mut().push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
// --snip--
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
将Rc<T>
和RecCell<T>
结合使用,来实现一个拥有多重所有权的可变引用
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
enum List{
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let value = Rc::new(RefCell::new(5));
let a = Rc::new(Cons(Rc::clone(&value),Rc::new(Nil)));
let b = Rc::new(Cons(Rc::new(RefCell::new(6)),Rc::clone(&a)));
let c = Rc::new(Cons(Rc::new(RefCell::new(10)),Rc::clone(&a)));
// *value 将Rc解引用为RefCell
*value.borrow_mut() += 10;
huan ch w n g
println!("a after = {:?}",a);
println!("b after = {:?}",b);
println!("c after = {:?}",c);
}
// a after = Cons(RefCell { value: 15 }, Nil)
// b after = Cons(RefCell { value: 6 }, Cons(RefCell { value: 15 }, Nil))
// c after = Cons(RefCell { value: 10 }, Cons(RefCell { value: 15 }, Nil))
其他可实现内部可变性的类型
Cell<T>
:通过复制来访问数据Mutex<T>
:用于实现跨线程情形下的内部可变性模式
循环引用可以导致内存泄漏
Rust的内存安全机制可以保障很难发生内存泄漏,但不是不可能
例如Rc<T>
和RefCell<T>
就可能创造出循环引用,从而发生内存泄漏
-
每个项的引用数量都不能变为0,值也不会被处理掉
use std::cell::RefCell; use std::rc::Rc; #[derive(Debug)] enum List{ Cons(i32, RefCell<Rc<List>>), Nil, } use crate::List::{Cons, Nil}; impl List{ fn tail(&self) -> Option<&RefCell<Rc<List>>>{ match self{ Cons(_,item) => Some(item), Nil => None, } } } fn main() { let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil)))); println!("a initial rc count = {}", Rc::strong_count(&a)); println!("a next item = {:?}", a.tail()); let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a)))); println!("a rc count after b creation = {}", Rc::strong_count(&a)); println!("b initial rc count = {}", Rc::strong_count(&b)); println!("b next item = {:?}", b.tail()); if let Some(link) = a.tail() { *link.borrow_mut() = Rc::clone(&b); } println!("b rc count after changing a = {}", Rc::strong_count(&b)); println!("a rc count after changing a = {}", Rc::strong_count(&a)); // Uncomment the next line to see that we have a cycle; // it will overflow the stack // println!("a next item = {:?}", a.tail()); }
防止内存泄漏的解决方法
- 依靠开发者保障,不能依靠Rust
- 重新组织数据结构:一些引用来表达所有权,一些引用不表达所有权
- 循环引用中的一部分具有所有权关系,另一部分不涉及所有权关系
- 而只有所有权关系才影响值的清理
- 防止循环引用,把
Rc<T>
换成Weak<T>
Rc::clone
把Rc<T>
实例的strong_count
加1,Rc<T>
的实例只有在strong_count
为0的时候才会被清理掉Rc<T>
实例通过调用Rc::downgrade
方法可以创建值的Weak Reference(弱引用)- 返回类型是
Weak<T>
- 调用
Rc::downgrade
会为weak_count
加1
- 返回类型是
- Rc使用
weak_count
来追踪存在多少个Weak<T>
weak_count
不为0并不影响Rc<T>
实例的清理
Strong VS Weak
Strong Reference
(强引用)是关于如何分享Rc<T>
实例的所有权Weak Reference
(弱引用)并不表达上述意思- 使用
Weak Reference
并不会创建循环引用- 当
Strong Reference
数量为0的时候,Weak Reference
会自动断开
- 当
- 在使用
Weak<T>
前,确保它指向的值仍然存在:- 在
Weak<T>
实例上调用upgrade
方法,返回Option<Rc<T>>
- 在
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
{
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!(
"branch strong = {}, weak = {}",
Rc::strong_count(&branch),
Rc::weak_count(&branch),
);
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
}
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
}
// leaf strong = 1, weak = 0
// branch strong = 1, weak = 1
// leaf strong = 2, weak = 0
// leaf parent = None
// leaf strong = 1, weak = 0
无畏并发
- Concurrent: 程序的不同部分之间独立的运行
- Parallel:程序的不同部分同时运行
Rust无畏并发:允许你编写没有细微Bug的代码,并在不引入新bug的情况下易于重构
使用线程同时运行代码
多线程可导致的问题
- 竞争状态:线程以不一致的顺序访问数据或资源
- 死锁:两个线程彼此等待对方释放所持有的资源
- 只在某些情况下发生的Bug,很难可靠地复制现象和修复
实现线程的方式:
- 通过调用系统OS的API来创建线程:1:1模型
- 需要较小的运行时
- 语言自己实现的线程(绿色线程): M:N模型
- 需要更大的运行时
- Rust标准库仅提供1:1模型的线程
通过spawn
创建新线程
通过thread::spawn
可以创建新线程
- 参数:一个闭包(在新线程里运行的代码)
//类似与C++中的detch
use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(||{
for i in 1..10{
println!("Hi number {} from the spawn thread! ", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5{
println!("hi number {} from the main thread!",i);
thread::sleep(Duration::from_millis(1));
}
}
通过join Handle
来等待所有线程的完成
thread::spawn
函数的返回值类型是JoinHandle
JoinHandle
持有值的所有权- 调用
join
方法,可以等待对应的其他线程的完成 join
方法:调用handle
的join
方法会阻止当前运行线程的执行,直到handle
所表示的这些线程终结
- 调用
使用move
闭包
move
闭包通常允许你和thread::spawn
函数一起使用,它允许你使用其他线程的数据
- 创建线程时吧值的所有权从一个线程转移到另一个线程
use std::thread;
fn main() {
let v = vec![1,2,3];
//v的所有权被转移到闭包
let handle = thread::spawn(move ||{
println!("here's a vector: {:?}",v);
});
handle.join().unwrap();
}
使用消息传递来跨线程传递数据
消息传递
- 一种流行的且能保证安全并发的技术就是:消息传递
- 线程(或Actor)通过彼此发送消息(数据)来进行通信
Rust:Channel
Channel
Channel
包括:发送端、接收端
- 调用发送端的方法,发送数据
- 接受端会检查和接受到达的数据
- 如果发送端、接受端中任意一端被丢弃,那么
Channel
就关闭了
创建Channel
使用mpsc::channel
函数来创建Channel
mpsc
表示multiple producer, single consumer(多个生产者,一个消费者)- 返回一个tuple(元组):里面元素分别是发送端、接受端
use std::thread;
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
let v = vec![1,2,3];
let handle = thread::spawn(move ||{
let val = String::from("hi");
tx.send(val).unwrap();
});
let received = rx.recv().unwrap();
println!("Got: {}",received);
}
发送端的send
方法
- 参数:想要发送的数据
- 返回:
Ressult<T,E>
,如果有问题(例如接受端已经被丢弃)就返回一个错误
接收端的方法
-
recv
方法:阻止当前线程执行,直到Channel中有值被送来- 一旦有值受到,就返回
Ressult<T,E>
- 当发送端关闭,就会受到一个错误
- 一旦有值受到,就返回
-
try_recv
方法:不会阻塞- 立即返回
Ressult<T,E>
- 有数据到达:就返回Ok,里面包含着数据
- 否则返回错误
- 通常会使用循环调用来检查
try_recv
的结果
- 立即返回
Channel和所有权转移
use std::thread;
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
let v = vec![1,2,3];
let handle = thread::spawn(move ||{
let val = String::from("hi");
tx.send(val).unwrap();
println!("{}",val);
//Error: ^^^ value borrowed here after move
});
let received = rx.recv().unwrap();
println!("Got: {}",received);
}
发送多个值,看到接受者在等待
use std::thread;
use std::sync::mpsc;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
let _handle = thread::spawn(move ||{
let vals = vec![
String::from("this"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_millis(200));
}
});
for received in rx{
println!("Get: {}",received);
}
}
通过克隆创建多个发送者
use std::thread;
use std::sync::mpsc;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
// 克隆创建多个发送者
let tx1 = mpsfa song zhec::Sender::clone(&tx);
thread::spawn(move ||{
let vals = vec![
String::from("this"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_millis(200));
}
});
thread::spawn(move ||{
let vals = vec![
String::from("1: this"),
String::from("1: from"),
String::from("1: the"),
String::from("1: thread"),
];
for val in vals {
tx1.send(val).unwrap();
thread::sleep(Duration::from_millis(200));
}
});
for received in rx{
println!("Get: {}",received);
}
}
共享状态的并发
Rust支持通过共享状态来实现并发
Channel类似单所有权:一旦将值的所有权转移至Channel,就无法使用它了
共享内存并发类似多所有权:多个线程可以同时访问同一块内存
使用Mutex
来每次只允许一个线程来访问数据
类似与C++的std::mutex
Mutex<T>
的API
-
通过
Mutex::new(数据)
来创建Mutex<T>
Mutex<T>
是一个智能指针
-
访问数据前,通过
lock
方法来获取锁- 会阻塞当前线程
- 可能会失败
- 返回的是
MutexGuard
(智能指针,实现了Deref和Drop),指向内部数据
use std::sync::Mutex; fn main() { let m = Mutex::new(5); { let mut num = m.lock().unwrap(); *num = 6; } println!("m = {:?}",m); }
多线程共享Mutex<T>
使用Arc<T>
来进行原子引用计数
Arc<T>
和Rc<T>
类似,它可以用于并发情景- A: atomic
use std::sync::{Mutex,Arc};
use std::thread;
fn main() {
let counter =Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10{
let counter = Arc::clone(&counter);
let handle = thread::spawn(move ||{
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles{
handle.join().unwrap();
}
println!("Result : {}", *counter.lock().unwrap());
}
RefCell<T>
/Rc<T>
vs Mutex<T>
/Arc<T>
Mutex<T>
提供了内部可变性,和Cell家族一样- 我们可以使用
RefCell<T>
来改变Rc<T>
里面的内容 - 我们可以使用
Mutex<T>
来改变Arc<T>
里面的内容 - 注意:
Mutex<T>
有死锁风险
send
和sync trait
在Rust语言中有两个并发概念
std::marker::Sync
和std::marker::Send
这两个trait(标签trait,没有定义任何方法)
Send
:允许线程间转移所有权
实现Send
trait的类型可以在线程间转移所有权
Rust中几乎所有类型都实现了Send
- 但
Rc<T>
没有实现Send
,它只用于单线程情景
任何完全由Send
类型组成的类型也被标记为Send
除了原始指针之外,几乎所有的基础类型都是Send
Sync
:允许从多线程访问
实现Sync
的类型可以安全的被多个线程引用
与就是说:如果T是Sync
,那么&T
就是Send
- 引用可以被安全的送往另一个线程
基础类型都是Sync
完全由Sync
类型组成的类型也是Sync
- 但是,
Rc<T>
不是Sync
的 RecCell<T>
和Cell<T>
家族也不是Sync
的- 而,
Mutex<T>
是Sync
的
手动来实现Send
和Sync
是不安全的
Rust的面向对象编程特性
面向对象语言特性
命名对象,封装,继承
继承
使用继承的原因:
- 代码复用
- Rust:默认的trait方法来进行代码共享
- 多态
- Rust: 泛型和Trait约束
使用trait对象来存储不同类型的值
有这样一个需求:
创建一个GUI工具:
- 它会遍历某个元素的列表,一次调用元素的draw方法进行绘制
- 例如:Button,TextField等元素
为共有行为定义一个trait
- rust避免将struct或enum称为对象,因为他们与impl块是分开的
- trait对象有些类似于其他语言中的对象
- 它们某种程度上组合了数据和行为
- trait对象和传统对象不同点:
- 无法为trait对象添加数据
- trait对象被专门用于抽象某些共有行为,他没有其他语言中的对象那么通用
// @File:draw/src/lib.rs
pub trait Draw {
fn draw(&self);
}lei xing
pub struct Screen{
// Box<dyn draw> 定义trait对象
// 只要实现了Draw tarit的"对象",就可以放入Box中
pub components : Vec<Box<dyn Draw>>,
}
impl Screen{
// run在运行时不关心传进去了什么类型,只要给类型实现了Draw这个trait
pub fn run(&self){
for component in self.components.iter(){
component.draw();
}
}
}
pub struct Button{
pub width : u32,
pub height: u32,
pub label : String,
}
impl Draw for Button{
fn draw(&self){
println!("button be celled");
}
}
// @File:draw/src/main.rs
use draw::Draw;
use draw::{Button,Screen};
struct SelectBox{
width: u32,
height : u32,
options: Vec<String>,
}
impl Draw for SelectBox{
fn draw(&self){
println!("SelectBox be celled");
}
}
fn main(){
let screen = Screen{
components: vec![
Box::new(SelectBox{
width: 75,
height: 10,
options: vec![
String::from("Yes"),
String::from("Maybe"),
]
}),
Box::new(Button{
width: 50,
height: 10,
label: String::from("Ok"),
}),
]
};
screen.run();
}
Trait对象执行的是动态派发
将trait约束作用于泛型是,Rust编译器会执行单态化
- 编译器为每一个被泛型类型参数代替的具体类型生成了非泛型的函数和方法实现。
通过单态化生成的代码会执行静态派发(static dispatch),在编译过程中,确定调用的具体方法
- 动态派发(dynamic dispatch)
- 无法在编译过程中确定你调用的究竟是哪一种方法
- 编译器会产生额外的代码以便在运行时找出希望调用的方法
- 使用trait对象.会执行动态派发
- 产生运行时开销
- 阻止编译器内联方法代码,是部分优化无法进行
tarit对象必须保证对象安全
- 只能把满足对象安全的trait转化成trait对象
- rust采用了一系列规则来判断某个对象是否安全,只需记住两条:
- 方法的返回类型不是self
- 方法中不包含任何泛型类型参数
实现面向对象的设计模式
状态模式
一个值拥有的内部状态由数个状态对象(state object)表达而成,而值的行为则随着内部状态的改变而改变
使用状态模式意味着:
- 当程序的业务需求改变时,无需改变值持有状态或者使用值的代码。
- 我们只需更新某个状态对象中的代码来改变其规则,或者是增加更多的状态对象。
略
模式匹配
模式是Rust中的一种特殊的语法,用于匹配复杂和简单类型的结构
将模式与匹配表达式和其他构造结合使用,可以更好地控制程序的控制流
模式由以下元素(的一些组合)组成:
- 字面值
- 解构的数组、enum、struct和tuple
- 变量
- 通配符
- 占位符
想要使用模式,需要将其与某个值进行比较
- 如果模式匹配,就可以在代码中使用这个值的相应部分
用到模式的地方
match
的Arm
match VALUE {
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
}
match
表达式的要求:
- 详尽(包含所有的可能性)
- 一个特殊的模式
_
(下划线),它会匹配任何东西,不会绑定到变量- 通常用于
match
的最后一个arm;或者用于忽略某些值
- 通常用于
if let
表达式
if let
表达式主要是作为一种简短的方式来等价的代替只有一个匹配项的match
if let
可选的还可拥有else
,包括:
else if
else if let
但if let
不会检查穷尽性
fn main() {
let favorite_color: Option<&str> = None;
let is_tuesday = false;
let age: Result<u8, _> = "34".parse();
if let Some(color) = favorite_color {
println!("Using your favorite color, {}, as the background", color);
} else if is_tuesday {
println!("Tuesday is green day!");
} else if let Ok(age) = age {
if age > 30 {
println!("Using purple as the background color");
} else {
println!("Using orange as the background color");
}
} else {
println!("Using blue as the background color");
}
}
while let
条件循环
- 只要模式能够满足匹配条件,他就允许while循环一直运行
fn main() {
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);
while let Some(top) = stack.pop(){
println!("{}",top);
}
}
for
循环
for
循环是Rust中最常见的循环for
循环中,模式就是紧随for
关键字后的值
fn main() {
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);
for (index, value) in stack.iter().enumerate() {
println!("{} is at index {}",value, index);
}
}
let
语句
let
语句也是模式
let PATTERN = EXPRESSION;
fn main{
let a = 5;
let (x, y, z) = (1, 2, 3)
}
函数参数
fn print_coordinates(&(x, y): &(i32, i32)) {
println!("Current location: ({}, {})", x, y);
}
fn main() {
let point = (3, 5);
print_coordinates(&point);
}
Refutability(可反驳性): 模式是否会匹配失效
模式有两种形式:refutable(可反驳的)和 irrefutable(不可反驳的)。
能匹配任何传递的可能值的模式被称为是 不可反驳的(irrefutable)。
let x = 5;
对某些可能的值进行匹配会失败的模式被称为是 可反驳的(refutable)。
if let Some(x) = a_value
函数参数,let
语句,for
循环只接受__不可反驳__的模式
if let
和while let
接受可反驳的和不可反驳的的模式
模式语法
匹配字面值
模式可以直接匹配字面值
fn main() {
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}
}
匹配命名变量
- 命名的变量是可匹配任何值的无可反驳模式
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(y) => println!("Matched, y = {:?}", y),
_ => println!("Default case, x = {:?}", x),
}
println!("at the end: x = {:?}, y = {:?}", x, y);
多重模式
在match表达式中,使用|语法(就是或的意思),可以匹配多个模式
](https://kaisery.github.io/trpl-zh-cn/ch18-03-pattern-syntax.html#使用嵌套的-_-忽略部分值)
](https://kaisery.github.io/trpl-zh-cn/ch18-03-pattern-syntax.html#使用嵌套的-_-忽略部分值)
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
使用..=
来匹配某个范围的值
let x = 5;
match x {
1..=5 => println!("one through five"),
_ => println!("something else"),
}
let s = 'c';
match x {
'a'..='j' => println!("early ASCII letter"),
'k'..='z' => println!("late ASCII letter"),
_ => println!("something else"),
}
解构以分解值
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
let Point { x: a, y: b } = p;
assert_eq!(0, a);
assert_eq!(7, b);
// 简写
let Point { x, y } = p;
assert_eq!(0, x);
assert_eq!(7, y);
match p {
// 匹配x的值任意,y为0
Point { x, y: 0 } => println!("On the x axis at {}", x),
Point { x: 0, y } => println!("On the y axis at {}", y),
Point { x, y } => println!("On neither axis: ({}, {})", x, y),
}
}
解构枚举
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {
let msg = Message::ChangeColor(0, 160, 255);
match msg {
Message::Quit => {
println!("The Quit variant has no data to destructure.")
}
Message::Move { x, y } => {
println!(
"Move in the x direction {} and in the y direction {}",
x, y
);
}
Message::Write(text) => println!("Text message: {}", text),
Message::ChangeColor(r, g, b) => println!(
"Change the color to red {}, green {}, and blue {}",
r, g, b
),
}
}
解构嵌套的结构体和枚举
enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(Color),
}
fn main() {
let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));
match msg {
Message::ChangeColor(Color::Rgb(r, g, b)) => println!(
"Change the color to red {}, green {}, and blue {}",
r, g, b
),
Message::ChangeColor(Color::Hsv(h, s, v)) => println!(
"Change the color to hue {}, saturation {}, and value {}",
h, s, v
),
_ => (),
}
}
解构struct和tuple
fn main() {
struct Point {
x: i32,
y: i32,
}
let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
}
在模式中忽略值
有几种方式可以在模式中忽略整个值或部分值
_
_
配合其他模式..
(忽略值的剩余部分)
使用_
来忽略整个值
fn foo(_: i32, y: i32) {
println!("This code only uses the y parameter: {}", y);
}
fn main() {](https://kaisery.github.io/trpl-zh-cn/ch18-03-pattern-syntax.html#使用嵌套的-_-忽略部分值)
foo(3, 4);
}
使用嵌套的 _
忽略部分值
fn main() {
let mut setting_value = Some(5);
let new_setting_value = Some(10);
match (setting_value, new_setting_value) {
(Some(_), Some(_)) => {
println!("Can't overwrite an existing customized value");
}
_ => {
setting_value = new_setting_value;
}
}
println!("setting is {:?}", setting_value);
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, _, third, _, fifth) => {
println!("Some numbers: {}, {}, {}", first, third, fifth)
}
}
}
通过在名字前以一个下划线开头来忽略未使用的变量
fn main() {
let _x = 5;
let y = 10;
}
注意, 只使用 _
和使用以下划线开头的名称有些微妙的不同:比如 _x
仍会将值绑定到变量,而 _
则完全不会绑定。
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{:?}", s);
}
我们会得到一个错误,因为 s
的值仍然会移动进 _s
,并阻止我们再次使用 s
。然而只使用下划线本身,并不会绑定值。
使用..
来忽略值的剩余部分
fn main() {
struct Point {
x: i32,
y: i32,
z: i32,
}
let origin = Point { x: 0, y: 0, z: 0 };
match origin {
Point { x, .. } => println!("x is {}", x),
}
}
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, .., last) => {
println!("Some numbers: {}, {}", first, last);
}
}
}
//错误:发生歧义
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(.., second, ..) => {
println!("Some numbers: {}", second)
},
}
}
使用match的守卫来提供额外的条件
匹配守卫(match guard)是一个指定于 match
分支模式之后的额外 if
条件,它也必须被满足才能选择此分支。
匹配守卫用于表达比单独的模式所能允许的更为复杂的情况。
fn main() {
let num = Some(4);
match num {
// match的守卫
Some(x) if x < 5 => println!("less than five: {}", x),
Some(x) => println!("{}", x),
None => (),
}
}
// 使用匹配守卫来测试与外部变量的相等性
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(n) if n == y => println!("Matched, n = {}", n),
_ => println!("Default case, x = {:?}", x),
}
println!("at the end: x = {:?}, y = {}", x, y);
}
// 多重匹配的match守卫
fn main() {
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"),
_ => println!("no"),
}
}
@
绑定
运算符(@
)允许我们在创建一个存放值的变量的同时测试其值是否匹配模式。
fn main() {
enum Message {
Hello { id: i32 },
}
let msg = Message::Hello { id: 5 };
match msg {
Message::Hello {
// 会判断值是否在3-7中间,并把值保存在id_variable中
id: id_variable @ 3..=7,
} => println!("Found an id in range: {}", id_variable),
Message::Hello { id: 10..=12 } => {
println!("Found an id in another range")
}
Message::Hello { id } => println!("Found some other id: {}", id),
}
}
高级特性
不安全的rust
Rust 还隐藏有第二种语言,它不会强制执行这类内存安全保证:这被称为 不安全 Rust(unsafe Rust)。它与常规 Rust 代码无异,但是会提供额外的超能力。
Unsafe Rust存在的原因
- 静态分析是保守的
- 使用unsafe Rust:我知道自己在做什么,并承担相应风险
Unsafe超能力
使用unsafe关键字来切换到unsafe Rust,开启一个块,里面放着unsafe 代码
Unsafe Rust里可执行的四个动作(unsafe超能力)
- 解引用原始指针
- 调用unsafe函数和方法
- 访问或修改可变的静态变量
- 实现unsafe trait
注意:
- unsafe并没有关闭借用检查或停用其他安全检查
- 任何内存安全相关的错误代码必须留在unsafe块中
- 尽可能隔离unsafe代码,最好将其封装在安全的抽象里,提供安全的API
解引用的原始指针
原始指针
- 可变的:
*mut T
- 不可变的:
*const T
.意味着指针在引用后不能直接对其进行赋值 - 注意,这里的
*
不是解引用符号,是类型名的一部分
原始指针和引用的不同之处
- 允许通过同时具有不可变和可变指针或多个指向同一位置的可变指针来忽略借用规则
- 无法保证能够指向合理的内存
- 允许为null
- 不实现任何自动清理
放弃这些安全保证后,可以换取更好的性能/与其他语言或硬件接口的能力
fn main() {
let mut num = 5;
// 不可变的原始指针
let r1 = &num as *const i32;
// 可变的原始指针
let r2 = &mut num as *mut i32;
unsafe{
// 打印指针地址
println!("r1: {:?}",r1);
// 打印值
println!("r2: {}",*r2);
}
let address = 0x7ffea14ad294usize;
let r = address as *const i32;
unsafe{
println!("r: {}",*r);
}
}
为什么要有原始指针:
- 与C语言进行接口
- 构建借用检查器无法理解的安全抽象
调用unsafe函数或方法
unsafe函数或方法:在定义前加上unsafe关键字
- S调用前需要手动满足一些条件(主要靠看文档),因为Rust无法对这些条件进行验证
- 将unsafe代码包裹在安全函数中是一种常见的抽象
//将字符串从给定位置分开,分为两个切片
//Error: 同时存在两个可变的借用
fn split_at_mut(slice :&mut[i32], mid:usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
assert!(mid <= len);
(&mut slice[..mid], &mut slice[mid..])
}
fn main() {
let mut v = vec![1,2,3,4,5,6];
let r = &mut v[..];
let (a,b) = r.split_at_mut(3);
assert_eq!(a,&mut[1,2,3]);
assert_eq!(b,&mut[4,5,6]);
}
//将unsafe代码包裹在安全函数中是一种常见的抽象
use std::slice;
fn split_mut(slice :&mut[i32], mid:usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
// 返回原始指针 *mut i32
let ptr = slice.as_mut_ptr();
assert!(mid<=len);
unsafe{(
slice::from_raw_parts_mut(ptr,mid),
slice::from_raw_parts_mut(ptr.add(mid),len-mid),
)}
}
fn main() {
let mut v = vec![1,2,3,4,5,6];
let (a,b) = split_mut(&mut v,3);
assert_eq!(a,&mut[1,2,3]);
assert_eq!(b,&mut[4,5,6]);
}
使用extern
函数调用外部代码
extern
关键字:简化创建和使用外部函数接口(FFI)的过程
外部函数接口(FFI):允许一种编程语言定义函数,并让其他编程语言能调用这些函数
任何在extern
中声明的代码都是不安全的
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
在 extern "C"
块中,列出了我们希望能够调用的另一个语言中的外部函数的签名和名称。"C"
部分定义了外部函数所使用的 应用二进制接口(application binary interface,ABI) —— ABI 定义了如何在汇编语言层面调用此函数。"C"
ABI 是最常见的,并遵循 C 编程语言的 ABI。
从其它语言调用 Rust 函数
也可以使用 extern
来创建一个允许其他语言调用 Rust 函数的接口。不同于 extern
块,就在 fn
关键字之前增加 extern
关键字并指定所用到的 ABI。还需增加 #[no_mangle]
注解来告诉 Rust 编译器不要 mangle 此函数的名称。
Mangling 发生于当编译器将我们指定的函数名修改为不同的名称时,这会增加用于其他编译过程的额外信息,不过会使其名称更难以阅读。每一个编程语言的编译器都会以稍微不同的方式 mangle 函数名,所以为了使 Rust 函数能在其他语言中指定,必须禁用 Rust 编译器的 name mangling。
在如下的例子中,一旦其编译为动态库并从 C 语言中链接,call_from_c
函数就能够在 C 代码中访问:
#![allow(unused)]
fn main() {
#[no_mangle]
pub extern "C" fn call_from_c() {
println!("Just called a Rust function from C!");
}
}
// extern 的使用无需 unsafe。
访问或修改一个可变的全局变量
rust支持全局变量,但因为所有权机制可能产生某些问题,例如数据竞争
- 在Rust里全局变量叫做静态变量(static)
static HELLO_WORLD: &str = "Hello, World";
fn main() {
println!("{}",HELLO_WORLD);
}
通常静态变量的名称采用 SCREAMING_SNAKE_CASE
写法。
静态变量只能储存拥有 'static
生命周期的引用,这意味着 Rust 编译器可以自己计算出其生命周期而无需显式标注。
访问不可变静态变量是安全的。
常量和静态变量的区别:
- 静态变量:有固定的内存地址,使用它的值总会访问同样的数据
- 常量: 允许使用他们的时候对数据进行复制
- 静态变量:可以是可变的,访问和修改静态可变变量是不安全(unsafe)的
static mut COUNTER: u32 = 0;
fn add_to_count(inc: u32){
unsafe{
COUNTER += inc;
}
}
fn main() {
add_to_count(3);
unsafe{
println!("COUNTER: {}",COUNTER);
}
}
实现不安全(unsafe) trait
当 trait 中至少有一个方法中包含编译器无法验证的不变式(invariant)时 trait 是不安全的。
声明不安全的trait:在定义前加unsafe关键字
- 该trait只能在unsafe代码块中使用
unsafe trait Foo{
}
unsafe impl Foo for i32{
}
高级Trait
在Trait定义中使用关联类型来指定占位类型
关联类型(associated types)是一个将类型占位符与 trait 相关联的方式,这样 trait 的方法签名中就可以使用这些占位符类型。
-
可以定义出包含某些类型的Trait,而在实现前无需知道这些类型是什么
pub trait Iterator{ // Item 类型占位符 type Item; fn next(&mut self) -> Option<Self::Item>; }
关联类型和泛型的区别
泛型 | 关联类型 |
---|---|
每次实现Trait时标注类型 | 无需标注类型 |
可以为一个类型多次实现某个Trait(不同的泛型参数) | 无法无单个类型多次实现某个Trait |
默认泛型参数和运算符重载
- 可以在使用泛型参数时为泛型指定一个默认的具体类型
- 语法`<PlaceholerType=ConcreteType>
- 这种技术常用于运算符重载
- Rust不允许创建自己的运算符以及重载任意的运算符
- 但可以通过实现
std::ops
中列出的那些trait来重载一部分相应的运算符
use std::ops::Add;
#[derive(Debug, PartialEq)]
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,
}
}
}
fn main() {
println!("{:?}",Point{x:1, y:2} + Point{x:2, y:1});
}
使用默认泛型参数
// 实现毫米和米相加
use std::ops::Add;
struct Millimeters(u32);
struct Meters(u32);
impl Add<Meters> for Millimeters {
type Output = Millimeters;
fn add(self, other: Meters) -> Millimeters {
Millimeters(self.0 + (other.0 * 1000))
}
}
默认泛型参数的主要应用场景
- 扩展一个类型而不破坏现有代码
- 允许在大部分用户都不需要的特定场景下进行自定义
完全限定语法(Fully Qualified Syntax)如何调用同名方法
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
// 如果不为Human实现fly方法将报错
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
fn main() {
let person = Human;
person.fly();
//调用两个trait的fly方法
Pilot::fly(&person);
Wizard::fly(&person);
}
完全限定语法使用场景如下:
完全限定语法:
<Type as Trait>::function(receiver_if_method, next_arg, ...);
可以在任何调用函数或方法的地方使用
允许忽略那些从其他上下文能推导出来的部分
当Rust无法区分你期望调用哪个具体实现时,才需要这种语法
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
// 如何调用Animal trait的baby_name方法呢?
println!("A baby dog is called a {}", Dog::baby_name());
// 完全限定语法
println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
使用supertrait
来要求trait附带其他trait的方法
需要在一个trait中使用其他trait的功能
- 需要被依赖的trait也被实现
- 那个被间接依赖的trait就是当前trait的supertrait
// 冒号后面为被依赖的trait
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {} *", output);
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
struct Point {
x: i32,
y: i32,
}
impl OutlinePrint for Point {}
use std::fmt;
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
fn main() {
let p = Point { x: 1, y: 3 };
p.outline_print();
}
使用newtype模式在外部类型上实现外部trait
孤儿规则: 只有当trait或类型定义在本地包时,才能为该类型实现这个trait
可以使用newtype模式来绕过这一规则
- 利用tuple struct(元组结构体)创建一个新的类型
// 在vec上包裹一层(元组结构体
// 为(元组结构体实现相应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(", "))
}
}
fn main() {
let w = Wrapper(vec![String::from("hello"), String::from("world")]);
println!("w = {}", w);
}
高级类型
使用newtype模式实现类型安全和抽象
newtype模式可以:
- 用来静态的保证各种值之间不会混淆并表明值的单位
- 为类型的某些细节提供抽象能力
- 通过轻量级的封装来隐藏内部实现细节
使用类型别名创建类型同义词
Rust提供了类型别名的功能
- 为现有类型生产另外的名称(同义词)
- 并不是一个独立的类型
- 使用
type
关键字
主要用途,减少代码的类型重复
type Kilometers = i32;
fn main() {
let x: i32 = 5;
let y: Kilometers = 5;
println!("x+y={}", x+y);
}
// 简化代码
fn main() {
type Thunk = Box<dyn Fn() + Send + 'static>;
let f: Thunk = Box::new(|| println!("hi"));
fn takes_long_type(f: Thunk) {
// --snip--
}
fn returns_long_type() -> Thunk {
// --snip--
Box::new(|| ())
}
}
类型别名也经常与 Result<T, E>
结合使用来减少重复。考虑一下标准库中的 std::io
模块。I/O 操作通常会返回一个 Result<T, E>
,因为这些操作可能会失败。标准库中的 std::io::Error
结构体代表了所有可能的 I/O 错误。std::io
中大部分函数会返回 Result<T, E>
,其中 E
是 std::io::Error
,比如 Write
trait 中的这些函数:
use std::fmt;
use std::io::Error;
use std::fmt;
// 类型别名
type Result<T> = std::result::Result<T, std::io::Error>;
pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize>;
fn flush(&mut self) -> Result<()>;
fn write_all(&mut self, buf: &[u8]) -> Result<()>;
fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
}
Never
类型
有一个名为!
的特殊类型
- 它没有任何值,行话称为空类型(empty type)
- 我们倾向于叫他
never
类型,因为它在不返回的函数中充当返回类型
- 我们倾向于叫他
- 不返回值的函数也被称作发散函数(diverging function)
// 正确
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
// 错误
// 这里的 guess 必须既是整型 也是 字符串,而 Rust 要求 guess 只能是一个类型。
let guess = match guess.trim().parse() {
Ok(_) => 5,
Err(_) => "hello",
};
正如你可能猜到的,continue
的值是 !
。也就是说,当 Rust 要计算 guess
的类型时,它查看这两个分支。前者是 u32
值,而后者是 !
值。因为 !
并没有一个值,Rust 决定 guess
的类型是 u32
。
__描述 !
的行为的正式方式是 never type 可以强转为任何其他类型。__允许 match
的分支以 continue
结束是因为 continue
并不真正返回一个值;相反它把控制权交回上层循环,所以在 Err
的情况,事实上并未对 guess
赋值。
动态大小和Sized Trait
Rust需要在编译时确定为一个特定类型的值分配多少空间
动态大小的类型(Dynamically Sized Types,DST)的概念
- 编写代码时使用只有的运行时才能确定大小的值
str
是动态大小的类型(注意不是&str
):只有在运行时才能确定字符串的长度
下列代码无法正常工作
let s1 : str = "heello there";
// ^^ doesn't have a size known at compile-time
let s2 : str = "How's it going?";
// 使用&str来解决
// &str 存入
// str的地址
// str的长度
let s1 : &str = "heello there";
let s2 : &str = "How's it going?";
Rust使用动态大小类型的通用方式
- 附带一些额外的元数据来存储动态大小
- 使用动态大小类型时总会把它的值放在某种指针后面
Box<T>
另外一种动态大小的类型:trait
每个trait
都是一个动态大小的类型,可以通过名称对其进行引用
为了将trait作为trait对象,必须将它放置在某种指针之后
- 例如
&dyn Trait
或Box<dyn Trait>(Rc<dyn Trait>)
之后
Sized trait
为了处理动态大小的类型,Rust提供了一个Sized trait
来确定一个类型的大小在编译时是否已知
编译时可以计算出大小的类型会自动实现这一trait
Rust还会为每一个泛型函数隐式的添加Sized
约束
默认情况下,泛型函数只能被用于编译时已经知道大小的类型,可以通过特殊语法解除这一限制
?Sized trait
约束
fn generic<T>(t: T) {
}
// 被隐式转换成下列代码
fn generic<T: Sized>(t: T) {
}
// ?只能作用于Sized
// T可能是Sized,也可能不是Sized
fn generic<T :?Sized>(t: &T) {
}
高级函数和闭包
函数指针
- 可以将函数传递给其他函数
- 函数在传递过程中会被强制转换成
fn
类型 fn
类型就是函数指针(function pointer)
fn add_one(x: i32) -> i32{
x+1
}
fn do_twice(f: fn(i32)-> i32, arg: i32) -> i32{
f(arg) + f(arg)
}
fn main() {
let res = do_twice(add_one, 5);
println!("the answer is :{}",res);
}
函数指针和闭包的不同
fn
是一个类型,不是一个trait
- 可以直接指定
fn
为参数类型,不需声明一个以Fn Trait
为约束的泛型参数
- 可以直接指定
- 函数指针实现了全部3种闭包trait(Fn,FnMut,FnOnce)
- 总是可以把函数指针作为参数传递给一个接受闭包的函数
- 所以,倾向于搭配闭包
trait
的泛型编写函数:可以同时接受闭包和普通函数
use std::thread;
fn print_something() -> i32{
for _ in 0..10{
println!("hello world");
}
10
}
}
fn main() {
let handle = thread::spawn(print_something);
let res = handle.join().unwrap();
println!("{}",res);
}
某些情形下,只想接受fn
而不想接受闭包:
- 与外部不支持闭包的代码交互:C函数
fn main() {
let list_of_numbers = vec![1,2,3];
let list_of_string : Vec<String> = list_of_numbers.iter().map(|i| i.to_string()).collect();
let list_of_numbers1 = vec![1,2,3];
let list_of_string1 : Vec<String> = list_of_numbers1.iter().map(ToString::to_string).collect();
println!("{:?}",list_of_string);
println!("{:?}",list_of_string1);
}
另一个例子:
fn main() {
#[derive(Debug)]
enum Status {
Value(u32),
Stop,
}
// enum的构造器,可以看成一个函数,也实现了闭包trait
let _v = Status::Value(2);
let list_of_status : Vec<Status> = (0u32..20).map(Status::Value).collect();
println!("{:?}",list_of_status);
}
返回闭包
闭包使用了trait进行表达,无法在函数中直接返回一个闭包,可以将__一个实现了该trait的具体类型__作为返回值
// 错误
fn return_closure() -> Fn(i32) -> i32{
// ^^^^^^^^^^^^^^ doesn't have a size known at compile-time
|x| x+1
}
// 正确
fn return_closure() -> Box <dyn Fn(i32) -> i32>{
Box::new(|x| x+1)
}
宏 macro
宏在Rust里指的是一组相关特性的集合称谓
- 使用
macro_rules!
构建的声明宏(declarative macro) - 三种过程宏
- 自定义
#[derive]
宏,用于struct或enum,可以为其指定随dervie属性添加的代码 - 类似属性的宏,在任何条目上添加自定义的代码
- 类似函数的宏,看起来像函数调用,对其指定为参数的token进行操作
- 自定义
函数和宏的差别
本质上,宏是用来编写可以生成其他代码的代码(元编程,metaprogramming)
函数在定义签名时,必须声明参数个数和类型,宏可以处理可变的参数
编译器可在解释代码前展开宏
宏的定义要比函数定义复杂的多,难以阅读,理解维护
在某个文件调用宏时,必须提前定义宏或将宏引入当前作用域
函数可以在任意位置定义,并在任意位置调用
macro_rules!
声明宏(弃用)
Rust中最常见的宏形式:声明宏
#[macro_export] // 表示这个宏在它所属的包被引入作用域后才可以使用
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
不深入研究
基于属性来生成代码的过程宏
这种形式更像函数(某种形式的过程)一些:
- 接收并操作输入的Rust代码
- 生成另外一些Rust代码作为结果
三种过程宏:
- 自定义派生
- 属性宏
- 函数宏
创建过程宏时:
- 宏定义必须单独放在他们自己的包中,并使用特殊的包类型
use proc_macro;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
定义过程宏的函数以 TokenStream 作为输入并生成 TokenStream 作为输出。 TokenStream 类型由包含在 Rust 中的 proc_macro crate 定义并表示令牌序列。
这是宏的核心:宏所操作的源代码构成了输入 TokenStream,宏产生的代码是输出 TokenStream。
该函数还附加了一个属性,用于指定我们正在创建的程序宏类型。 我们可以在同一个 crate 中拥有多种程序宏。
自定义derive
宏
让我们创建一个 hello_macro
crate,其包含名为 HelloMacro
的 trait 和关联函数 hello_macro
。
我们创建一个能自动实现trait的过程宏
在它们的类型上标注 #[derive(HelloMacro)]
进而得到 hello_macro
函数的默认实现。
太麻烦了,这里不做展示,有兴趣自行查阅:如何编写自定义 derive
宏
类似属性的宏
属性宏与自定义derive宏类似
- 允许创建新的属性
- 但不是为derive属性生成代码
属性宏更加灵活:
- derive只能用于struct和enum
- 属性宏只能用于任意条目,例如函数
// 属性宏
#[route(GET, "/")]
fn index() {
// 自定义属性宏
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
类似函数的宏
函数宏的定义是类似于函数调用的宏,但比普通函数更加灵活
函数宏可以接受TokenStream作为参数
与另外两种过程宏一样,在定义中使用Rust代码来操作TokenStream
// 函数宏
let sql = sql!(SELECT * FROM posts WHERE id=1);
// 函数宏定义
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
最后的项目:WebServer
// @File: webserver/src/main.rs
use std::net::TcpListener;
use std::thread;
use std::io::prelude::*;
use std::net::TcpStream;
use std::fs;
use std::time::Duration;
use webserver::ThreadPool;
fn main() {
let listen = TcpListener::bind("127.0.0.1:8081").unwrap();
let pool = ThreadPool::new(4);
for stream in listen.incoming() {
let stream = stream.unwrap();
println!("Connection established");
pool.execute(|| {handle_connection(stream)
});
}
}
// 请求
// Method Request-URI HTTP-Version CRLF
// headers CRLF
// message-body
// 响应
// HTTP-version Status-Code Reason_Phrase CRLF
// headers CRLF
// message-body
fn handle_connection(mut stream: TcpStream){
let mut buffer = [0;512];
stream.read(&mut buffer).unwrap();
let get = b"GET / HTTP/1.1\r\n";
let sleep = b"GET /sleep HTTP/1.1\r\n";
let (status_line, filename) = if buffer.starts_with(get){
("HTTP/1.1 200 OK\r\nContent-Length: {}\r\nContent-Type: text/html\r\n\r\n","index.html")
}else if buffer.starts_with(sleep){
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK\r\nContent-Length: {}\r\nContent-Type: text/html\r\n\r\n","sleep.html")
}else{
("HTTP/1.1 404 NOT FOUND\r\nContent-Length: {}\r\nContent-Type: text/html\r\n\r\n","404.html")
};
let contents = fs::read_to_string(filename).unwrap();
let response = format!("{}{}",status_line,contents);
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
// @File: webserver/src/main.rs
use std::thread;
use std::sync::mpsc;
use std::sync::Mutex;
use std::sync::Arc;
enum Massage{
NewJob(Job),
Terminate,
}
struct Worker{
id : usize,
thread: Option<thread::JoinHandle<()>>,
}
type Job = Box<dyn FnOnce()+Send+'static>;
pub struct ThreadPool{
workers: Vec<Worker>,
sender : mpsc::Sender<Massage>,
}
impl Worker {
fn new(id: usize, receiver:Arc<Mutex<mpsc::Receiver<Massage>>>)-> Worker{
let thread = thread::spawn(move ||loop{
let massage = receiver.lock().unwrap().recv().unwrap();
match massage {
Massage::NewJob(job) => {
println!("Worker {} got a job; executing.",id);
job();
}
Massage::Terminate => {
println!("Worker {} was told to Terminate.",id);
break;
}
}
});
Worker{id,
thread: Some(thread)}
}
}
impl ThreadPool{
pub fn new(size: usize) -> ThreadPool{
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size{
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool{ workers,sender}
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static
{
let job = Box::new(f);
self.sender.send(Massage::NewJob(job)).unwrap();
}
}
impl Drop for ThreadPool{
fn drop(&mut self){
println!("Sending Terminate message to all workers");
for _ in &mut self.workers {
self.sender.send(Massage::Terminate).unwrap();
}
println!("Shutting down all workers");
for worker in &mut self.workers{
println!("Shutting down worker {}",worker.id);
if let Some(thread) = worker.thread.take(){
thread.join().unwrap();
}
}
}
}
一些其他东西
单元类型
- unit type是一个类型,有且仅有一个值,都写成小括号
()
- 单元类型
()
类似c/c++/java语言中的void
。当一个函数并不需要返回值的时候,c/c++/java中函数返回void,rust则返回()
。但语法层面上,void仅仅只是一个类型,该类型没有任何值;而单位类型()既是一个类型,同时又是该类型的值。 - 单元类型
()
也类似c/c++/java中的null,但却有很大不同。 null是一个特殊值,可以赋给不可类型的值,例如java中的对象,c中指向struct实例的指针,C++中的对象指针。但在rust中,()
不可以赋值给除单元类型外的其它的类型的变量,()只能赋值给()。 - Rust标准库中使用单元类型
()
的一个例子是HashSet
。一个HashSet
只不过是HashMap
的一个非常简单地包裹,写作:HashMap<T, ()>
。HashMap的第二个泛型类型参数即用了单元类型()
- 可以用
Result<(), MyErrorType>
代替Option,某些开发者认为Result<(), MyErrorType>
语义上能更简明地表示一个“结果”。