Rust简介
特点
即安全又高效,并发
是一种静态编译语言,要在写代码时声明数据类型
擅长领域
高性能Web Service
WebAssembly
命令行工具
网络编程
嵌入式设备
系统编程
操作
更新rust:rustup update
卸载rust:rustup self uninstall
查看rust版本:rustc --version
查看本地文档:rustup doc
用vscode打开某项目:进入项目目录后 code .
文件名
后缀:rs
命名规范:hello_world.rs
编译
rustc hello_world.rs(只适合简单的程序)
运行exe文件
代码组织
代码组织主要包括: 哪些细节可以暴露,哪些细节是私有的
作用域内哪些名称有效
模块系统: Package(包):cargo特性,让你构建、测试、共享crate
Crate(单元包):一个模块树,它可产生一个library或可执行文件
Module(模块)、use:让你控制代码的组织、作用域、私有路径
Path(路径):为struct、function或module等项命名的方式
Package包含Crate,Crate包含Module
Package
1、包含一个Cargo.toml,它描述如何构建这些Crates
2、只能包含0-1个library crate 3、可以包含任意数量的binary crate
4、但必须至少包含一个crate(library或binary)
Crate
crate类型:library和binary
Crate Root: 是源代码文件
Rust编译器从这里开始,组成你的Crate的根Module
Cargo惯例: src/ main.rs: binary crate的crate root
crate名与package名相同 src/lib.rs: package 包含一个library crate library
crate 的 crate root crate名与package名相同
Cargo把crate root 文件交给rustc来构建library 或 binary
一个Package可以同时包含src/main.rs和src/lib.rs 一个binary
crate和一个library crate 名称与package名相同 一个Pacage可以有多个binary
crate: 文件放在src/bin 每个文件都是单独的binary crate
Module
在一个crate内,代码进行分组 增加可读性,易于复用
控制项目(item)的私有性,public、private
建立module: mod关键字 可嵌套
可包含其他项(struct、enum、常量、trait、函数等)的定义
实例代码: 单独创建一个文件lib.rs或其他名.rs
1 2 3 4 5 6 7 8 9 10 11 mod front_of_house{ mod hosting{ fn add_to_waitlist (){} fn seat_at_table (){} } mod serving{ fn take_order (){} fn serve_order (){} fn take_payment (){} } }
src/main.rs和src/lib.rs叫做crate roots:
这两个文件(任意一个)的内容形成了名为crate的模块,位于整个模块树的根部
私有边界
模块不仅可以组织代码,还可以定义私有边界
如果想把函数或结构体等设为私有,可以将它放到某个模块中
Rust中所有的条目(函数、方法、struct、enum、模块、常量)默认是私有的
父级模块无法访问子模块中的私有条目 子模块可以使用祖先模块中的条目
使用pub关键字可以将条目标记为公共的
pub strcut: pub放在struct前:
struct是公共的,但里面的字段默认是私有的
pub enum: enum是公共的,里面的变体也都是公共的
路径
为了在Rust的模块中找到某个条目,需要使用路径
路径的两种形式: 绝对路径:从crate root开始,使用crate名或字面值crate
相对路径:从当前模块开始,使用self,super或当前模块的标识符
路径至少由一个标识符组成,标识符之间使用::
在src下创建lib.rs
1 2 3 4 5 6 7 8 9 10 11 12 mod front_of_house{ pub mod hosting{ pub fn add_to_waitlist (){} } }pub fn eat_at_restaurant (){ crate::front_of_house::hosting::add_to_waitlist (); front_of_house::hosting::add_to_waitlist; }
super关键字: 用来访问父级模块路径中的内容,类似文件系统的..
1 2 3 4 5 6 7 8 9 10 11 fn serve_order (){}mod back_of_house{ fn fix_incorrect_order (){ cook_order (); super::serve_order (); crate::serve_order (); } fn cook_order (){} }
use关键字
可以使用use关键字将路径(可以用绝对路径和相对路径,一般用绝对路径)导入到作用域内,仍遵循私有性规则
use的习惯用法: 函数:将函数的父级模块引入作用域(指定到父级)
struct、enum、其他:指定到完整路径(指定到本身)
1 2 3 4 5 6 7 8 9 10 mod front_of_house{ pub mod hosting{ pub fn add_to_waitlist (){} } }use crate::front_of_house::hosting;pub fn eat_at_restaurant (){ hosting::add_to_waitlist (); }
1 2 3 4 5 6 use std::collection::HashMap;fn main (){ let mut map = HashMap::new (); map.insert (1 ,2 ); }
而对于同名条目,则需要指定到父级。
或者使用as关键字为引入的路径指定本地别名
1 2 use std::fmt::Result ;use std::io::Result as IoResult;
使用pub use重新导出名称
使用use将路径(名称)导入到作用域后,该名称在此作用域内是私有的。 pub
use:重导出
该条目引入作用域,该条目也可以被外部代码引入到它们的作用域
使用嵌套路径清理大量的use语句:
如果使用同一个包或模块下的多个条目,可使用嵌套路径在同一行内将上述条目引入:
路径相同的部分::{路径差异的部分}
如果两个use路径值意是另一个的子路径,使用self
1 2 3 4 5 6 7 use std::{cmp::Ordering, io};use std::io::{self , Write};
通配符\(*\) 使用\(*\) 可以把路径中所有公共条目都引入到作用域
1 use std::collections::*;
应用场景: 测试。将所有测试代码引入到tests模块 有时用于预导入模块
使用外部包
1、Cargo.toml添加依赖的包 2、use将特定条目引入作用域
将模块内容移动到其他文件
模块定义时,如果模块后边是";",而不是代码块:
Rust会从与模块同名的文件中加载内容 模块树的结构不会变化
随着模块逐渐变大,该技术可以让你把模块的内容移动到其他文件中
lib.rs:
1 2 3 4 5 6 7 mod front_of_house;pub use crate::front_of_house::hosting;pub fn eat_at_restaurant (){ hosting::add_to_waitlist (); }
front_of_house.rs:
front_of_house/hosting.rs
1 pub fn add_to_waitlist (){}
宏
简介
宏在rust里指的是一组相关特性的集合称谓:
1、使用macro_rules!构建的声明宏(可能要弃用,不深究) 2、3种过程宏:
自定义#[derive]宏,用于struct或enum,可以为其指定随derive属性添加的代码
类似属性的宏,在任何条目上添加自定义属性
类似函数的共,看起来像函数调用,对其指定为参数的token进行操作
函数与宏的差别:
1、本质上,宏是用来编写可以生成其他代码的代码(元编程)
2、函数在定义签名时,必须声明参数的个数和类型,宏可处理可变的参数
3、编译器会在解释代码前展开宏
4、宏的定义比函数复杂得多,难以阅读、理解、维护
5、在某个文件调用宏时,必须提前定义宏或将宏引入当前作用域内
6、函数可以在任何位置定义并在任何位置使用
macro_rules! 声明宏
类似match模式匹配
1 2 3 4 5 6 7 8 9 10 11 12 #[macro_export] macro_rules! vec { ( $( $x:expr ),* ) => { { let mut temp_vec = Vec ::new (); $( temp_vec.push ($x); )* temp_vec } }; }
1 2 3 4 5 6 7 8 9 10 11 #[macro_use] mod macros { macro_rules! my_macro { () => { println! ("Check out my macro!" ); }; } }fn main () { my_macro!(); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #[rustfmt::skip] macro_rules! my_macro { () => { println! ("Check out my macro!" ); }; ($val:expr) => { println! ("Look at this other macro: {}" , $val); } }fn main () { my_macro!(); my_macro!(7777 ); }
基于属性来生成代码的过程宏
1、这种形式更像函数一些: 接收并操作输入的rust代码
生成另外一些rust代码作为结果 2、三种过程宏: 自定义派生 属性宏 函数宏
3、创建过程宏时:
宏定义必须单独放在它们自己的包中,并使用特殊的包类型
自定义derivr宏: 需求:
1、创建一个hello_macro包,定义一个拥有关联函数hello_macro和HelloMacro
trait 2、提供一个能够自动实现trait的过程宏
3、在它们的类型上标注#[derive(HelloMacro)],进而得到hello_macro的默认实现
(例子看不懂:BV1hp4y1k7SV的P107)
类似属性的宏: 1、属性宏与自定义derive宏类似: 允许创建新的属性
但不是为derive属性生成代码 2、属性宏更加灵活:
derive只能用于struct和enum 属性宏可以用于任意条目,例如函数
类似函数的宏: 1、函数定义类似于函数调用的宏,但比普通股函数更加灵活
2、函数宏可以接收TokenStream作为参数
3、与另外两种过程宏一样,在定义中使用rust代码来操作TokenStream
Cargo
简介
Cargo是Rust的构建系统和包管理工具:用于构建代码,下载依赖的库、构建这些库。(安装rust会安装cargo)
查看cargo版本:cargo --version
操作
创建项目
cargo new 项目名称
Cargo.toml: 1、是Cargo的配置格式
2、package是一个区域标题,表示下方的内容是用来配置包的,name项目名,verstion项目版本,authors项目作者,edition使用的rust版本
3、dependencies是另一个区域的开始,它会列出项目的依赖项(依赖库)
4、在Rust里面,代码的包称作crate
cargo生成的main.rs在src目录下,而cargo.toml在项目顶层下,顶层目录可以防止README、许可信息、配置文件和其他
如果创建项目的时候没有使用cargo,也可以把项目转化为cargo:
1、把源代码移动到src下 2、创建Cargo.toml并填写相应的配置
创建库项目: cargo new 项目名称 --lib
构建项目
在项目里cargo build即可
生成.lock文件和target文件夹
Cargo.lock:
1、首次构建时创建的文件,会找到该代码中符合要求的依赖项并写到lock文件
2、在以后构建,会先使用lock文件中指定的依赖项版本,而不是最新的版本,除非自己手动指定(在toml文件中指定)
为发布构建:cargo build --release
构建并运行项目
cargo run
检查代码
cargo check
检查代码,确保能通过编译,但不产生可执行文件(比cargo build快)
发布配置
通过发布配置(release profile)来自定义构建: 1、是预定义的
2、可自定义:可使用不同的配置,对代码编译拥有更多的控制
3、每个profile的配置都独立于其他的profile
cargo主要的两个profile: 1、dev profile:适用于开发,cargo build
2、release profile:适用于发布,cargo build --release
自定义profile: 1、针对每个profile,cargo都提供了默认的配置
2、如果想自定义xxxx
profile的配置:可以在cargo.toml里添加[profile.xxxx]区域,在里面覆盖默认配置的子集(有[profile.dev]和
[profile.release])
在dev模式下,opt-level默认值是0(不要性能,编译快)
在release模式下,opt-level默认值是3(要性能,编译慢)
发布crate
发布到crate.io: 1、可以通过发布包来共享代码
2、crate注册表在http://crates.io/
文档注释: 生成HTML文档 显式公共API的文档注释,如何使用API 使用///
支持markdowns 放置在被说明条目之前
生成HTML文档的命令:cargo doc 它会运行rustdoc工具(rust安装包自带)
把生成的HTML文档放在target/doc目录下 cargo doc
--open能生成并打开文档
文档注释常用章节:
其他常用章节: Panics:函数可能发生panic的场景
Errors:如果函数返回Result,描述可能的错误种类,以及可导致错误的条件
Safety:如果函数处于unsafe调用,就应该解释函数unsafe的原因,以及调用者确保的使用前提
文档注释作为测试: 示例代码块的附加值: 运行cargo
test:将把文档注释中示例代码作为测试来运行
为包含注释的项添加文档注释: 符号://! 这类注释通常描述crate和模块:
crate root(按惯例为src/lib.rs)
一个模块内,将crate或模块作为一个整体进行记录
上面的示例(lib.rs文件中的代码):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 pub fn add_one (arg: i32 ) -> i32 { arg }
详细关于发布代码到crate.io网站上的操作: 看b站BV1hp4y1k7SV 的P82
pub use
使用pub use导出方便使用的公共API
问题:crate程序结构在开发时对于开发者
很合理,但对于它的使用者不够方便。 例如:
麻烦:my_crate::some_module::another_module::UsefulType
方便:my_crate::UsefulType 解决方案: 使用pub
use,可以重新导出,创建一个与内部私有结构不同的对外公共结构
lib.rs:
1 2 3 4 5 6 7 8 9 pub use self::kinds::PrimaryColor;pub mod kinds{ pub enum PrimaryColor { Red, Yellow, Blue, } }
main.rs
1 2 use crate::PrimaryColor;
工作空间
cargo工作空间:帮助管理多个相互关联且需要协同开发的crate
cargo工作空间是一套共享同一个cargo.lock和输出文件夹的包
例子: 1个二进制crate,2个库crate
在项目最外边创建Cargo.toml文件并设置:
1 2 3 4 5 6 [workspace] members = [ "adder" "add-one" ]
adder是一个二进制crate,add-one是l库crate
指定运行二进制crate: cargo run -p adder
要写依赖项,在对应的crate的Cargo.toml的dependencies属性:
1 2 [dependencies] add-one = {path = "../add-one"}
整个工作空间只有一个Cargo.toml:保证不会出错
指定执行某个文件测试: cargo test -p add-one
关键字
let
定义变量
mut
声明变量为可变的,rust中的变量默认都是不可变的
const
声明常量,常量与变量有很多区别: 1、不可以使用mut,常量永远是不可变的
2、声明常量使用const,而且类型必须被标注
3、常量可以在任何作用域内声明,包括全局作用域
4、常量只可以绑定到常量表达式,无法绑定到函数的调用结果或只能在运行时才能计算出的值
命名规范:Rust中常量使用全大写字母,每个单词之间用下划线分开,例如:
MAX_POINTS
例子: const MAX_POINTS: u32 = 100_000;
fn
声明函数
rust中,针对函数和变量名,rust使用snake
case命名规范,即所有的字母都是小写的,单词之间使用下划线分开
在rust中,不需要先声明函数才能进行调用,只要文件中有对该函数的定义,就能进行调用
在rust中,函数签名中必须声明每个形参的类型
函数的语句和表达式示例代码:
1 2 3 4 5 6 7 8 9 fn main (){ let x = 5 ; let y = { let x = 1 ; x + 3 }; println! ("The value of y is: {}" ,y); }
函数的返回值:
1、在->符号后边声明函数返回的类型,但是不可以为返回值命名
2、在rust中,返回值就是函数体里面最后一个表达式的值
3、若想提前返回,需使用return关键字,并指定一个值(大多数函数都是默认使用最后一个表达式为返回值)
1 2 3 4 5 6 7 8 fn plus_five (x:i32 ) -> i32 { x + 5 }fn main (){ let x = plus_five (6 ); println! ("The value of x is: {}" ,x); }
match
相当于c++中的switch,分支,对应不同情况(不同枚举)有不同的执行语句
match匹配必须穷举所有的可能
常规代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #[derive(Debug)] enum UsState { Alabama, Alaska, }enum Coin { Penny, Nickel, Dime, Quarter (UsState), }fn value_in_cents (coin: Coin) -> u8 { match coin{ Coin::Penny => { println! ("Penny" ); 1 } Coin::Nickel => 5 , Coin::Dime => 10 , Coin::Quarter (state) => { println! ("State quater from {:?}!" ,state); 25 } } }fn main (){ let c = Coin::Quarter (UsState::Alaska); println! ("{}" ,value_in_cents (c)); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 use std::cmp::Ordering;fn main (){ let a = 1 ; let b = 2 ; let c = 3 ; match a.cmp (&b){ Ordering::Less => println! ("a小于b" ), Ordering::Greater => println! ("a大于b" ), Ordering::Equal => println! ("a等于b" ), } }
通配符_:替代其余没列出的值
1 2 3 4 5 6 7 8 9 fn main (){ let v :u8 = 0 ; match v{ 1 => println! ("One" ), 2 => println! ("Two" ), _ => (), } }
1 2 3 4 5 6 7 fn maybe_icecream (time_of_day: u16 ) -> Option <u16 > { match time_of_day { 0 ..=21 => Some (5 ), 22 ..=23 => Some (0 ), _ => None , } }
处理错误示例代码:
1 2 3 4 5 6 7 8 9 fn main (){ let mut guess = String ::new (); std::io::stdin ().read_line (&mut guess).except ("无法读取行" ); let guess :i32 = match guess.trim ().parse (){ Ok (num) => num, Err (_) => continue , } }
if let
相当于只匹配一种模式的match,代码更少但是放弃了穷举的可能
1 2 3 4 5 6 7 8 9 10 11 12 13 fn main (){ let v = Some (0u8 ); match v { Some (3 ) => println! ("Three" ), _ => println! ("Others" ), } if let Some (3 ) = v{ println! ("Three" ); } else { println! ("Others" ); } }
1 2 3 4 5 6 7 8 9 10 11 fn main () { let option_value = Some (5 ); if let Some (value) = option_value { println! ("Option value is Some: {}" , value); } else { println! ("Option value is None." ); } }
while let
while
let循环会反复执行一个块,直到模式匹配失败,当模式匹配成功时,循环会进入块内部执行相关代码;当模式匹配失败时,循环会结束。
1 2 3 4 5 6 7 8 fn main (){ let mut stack = vec! [1 ,2 ,3 ,4 ,5 ]; while let Some (item) = stack.top (){ println! ("Popped item: {}" , item); } println! ("Stack is empty!" ); }
在这个示例中,Some(item) =
stack.top(),他会尝试从stack中弹出一个元素,并将其绑定到item。只要pop方法返回Some枚举的值,就会执行循环体内的代码。一旦pop方法返回None,循环就会终止
loop
相当于c++中的循环
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 use std::cmp::Ordering;fn main (){ loop { let a = 1 ; let b = 2 ; let c = 3 ; match a.cmp (&b){ Ordering::Less => println! ("a小于b" ), Ordering::Greater => println! ("a大于b" ), Ordering::Equal =>{ println! ("a等于b" ); break ; } } } }
1 2 3 4 5 6 7 8 9 10 11 fn main (){ let mut counter = 0 ; let result = loop { counter += 1 ; if counter == 10 { break counter*2 } }; println! ("The result is: {}" ,result); }
if
1 2 3 4 5 6 7 8 9 fn main (){ let number = 3 ; if number < 5 { println! ("condition was true" ); }else { println! ("condition was false" ); } }
1 2 3 4 5 fn main (){ let condition = true ; let number = if condition {5 } else {6 }; }
while
1 2 3 4 5 6 7 fn main (){ let mut number = 3 ; while number != 3 { number -= 1 ; } }
for
1 2 3 4 5 6 fn main (){ let a = [10 ,20 ,30 ,40 ,50 ]; for element in a.iter (){ println! ("The value is: {}" ,element); } }
1 2 3 4 5 6 7 8 9 10 fn main (){ for number in (1 ..4 ).rev (){ println! ("{}!" ,number); } }
struct
定义
1 2 3 4 5 6 struct User { username: String , email: String , sign_in_count: u64 , active: bool , }
实例化
1 2 3 4 5 6 7 8 9 10 11 12 13 fn main (){ let mut user1 = User{ email: String ::from ("1037827@qq.com" ), usename: String ::from ("Mike" ), active: true , sign_in_count: 588 , }; user1.email = String ::from ("103@qq.com" ); }
简写实例化结构:(形参名与字段名相同)
1 2 3 4 5 6 7 8 fn build_user (email: String , username: String ) -> User{ User{ email, username, active: true , sign_in_count: 0 , } }
更新语法
当你想基于某个struct实例来创建一个新实例的时候,可以使用struct更新语法。
1 2 3 4 5 6 7 8 9 10 11 12 13 let user2 = User{ email: String ::from ("103" ), username: String ::from ("Bob" ), active: user1.active, sign_in_count: user1.sign_in_count, }let user2 = User{ email: String ::from ("103" ), username: String ::from ("Bob" ), ..user1 }
Tuple Struct
Tuple Struct整体有名字,但里面的元素没有名字。
适用:想给整个tuple起名字,并让它不同于其他tuple,而且不需要给里面的每个元素起名字。
访问可以用:模式匹配或点标记法
例子:
1 2 3 4 struct Color (i32 ,i32 ,i32 );struct Point (i32 ,i32 ,i32 );let black = Color (0 ,0 ,0 ); black.0 ;
Unit-Like Struct
1、可以定义没有任何字段的struct,叫做Unit-Like Struct
2、适用于需要在某个类型上实现某个trait,但是在里面又没有想要存储的数据
1 2 3 4 5 struct UnitLikeStruct ;fn main (){ let unit_like_struct = UnitLikeStruct; }
struct数据所有权
上面的User使用了String而不是&str: 该struct实例拥有其所有的数据
只要struct实例是有效的,那么里面的字段数据也是有效的
struct里面也可以存放引用,但是需要使用生命周期,生命周期保证只要struct实例是有效的,那么里面的引用也是有效的
调试打印结构体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #[derive(Debug)] struct Rectangle { width: u32 , length: u32 , }fn main (){ let rect = Rectangle{ width: 30 , length: 50 , }; println! ("{}" ,area (&rect)); println! ("{:?}" ,rect); println! ("{:#?}" ,rect); }fn area (rect: &Rectangle) -> u32 { rect.width * rect.length }
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #[derive(Debug)] struct Rectangle { width: u32 , length: u32 , }impl Rectangle { fn area (&self ) -> u32 { self .width * self .length } }fn main (){ let rect = Rectangle{ width: 30 , length: 50 , }; println! ("{}" ,rect.area ()); println! ("{:#?}" ,rect); }
方法调用的运算符:
在c++中,调用指针对象的方法为object->或者(object).
但在rust中会自动引用或解引用,即在调用方法时,rust根据情况自动添加&、&mut或
例子: p1.distance(&p2) 等价于 (*p1).distance(&p2)
关联函数:
可以在impl块里定义不把self作为第一个参数的函数,它们叫关联函数(不是方法)
例如:String::from()
关联函数通常用于构造器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #[derive(Debug)] struct Rectangle { width: u32 , length: u32 , }impl Rectangle { fn area (&self ) -> u32 { self .width * self .length } fn can_hold (&self , other: &Rectangle) -> bool { self .width > other.width && self .length > other.length } fn square (size: u32 ) -> Rectangle{ Rectangle{ width: size, length: size, } } }fn main (){ let rect = Rectangle{ width: 30 , length: 50 , }; let s = Rectangle::square (20 ); println! ("{}" ,rect.area ()); println! ("{:#?}" ,rect); println! ("{:#?}" ,s); }
一个struct可以有多个impl块
输入输出
输出
1 2 3 4 fn main (){ let mut a = 1 ; println! ("a = {}" ,a); }
输入
字符串输入
1 2 3 4 5 use std::io;fn main (){ let mut a = String ::new (); ios::stdin ().read_line (&mut guess).except ("无法读取行" ); }
依赖项
rand
现在toml文件中依赖项区域加入rand = "版本号"
生成1~100的随机数
1 2 3 4 use rand::rng;fn main (){ let number = rand::thread_rng ().gen_range (1 ,101 ); }
数据类型
Shadowing(隐藏)
可以使用相同名字声明新的变量,新的变量就会shadow之前生民的同名变量
新变量的数据类型可以与旧变量的数据类型不同
复合类型
rust提供两种基础的复合类型:元组(tuple)和数组
tuple可以将多个类型的多个值放在一个类型里,长度是固定的,一旦声明就无法修改。
获取Tuple的元素值:
1、可以使用模式匹配来解构(destructure)一个Tuple来获取元素的值
2、点标记法,后接元素的引号
1 2 3 4 5 6 7 8 9 10 fn main (){ let tup :(i32 ,f64 ,u8 ) = (500 ,6.4 ,1 ); let (x,y,z) = tup; println! ("{},{},{}" ,x,y,z); println! ("{},{},{}" ,tup.0 ,tup.1 ,tup.2 ); }
数组也可以将多个值放在一个类型里,但是数组中每个元素的类型必须是相同的,长度也是固定的。
1 2 3 4 5 6 7 fn main (){ let a :[i32 ,5 ] = [1 ,2 ,3 ,4 ,5 ]; let b = [3 ;5 ]; a[0 ],a[1 ],a[2 ]; }
字符串转化成整数
1 2 3 4 5 6 7 8 use std::io;fn main (){ let mut guess = String ::new (); io::stdin ().read_line (&mut guess).except ("无法读取行" ); let guess : i32 = guess.trim ().parse ().except ("不是整数" ); }
1、trim方法是去除"" 2、parse方法是将字符串转化为整型
3、rust中允许同名新变量隐藏旧变量
所有权
简介
Rust的核心特性就是所有权
所有程序在运行时都必须管理它们使用计算机内存的方式
1、有些语言有垃圾收集机制,在程序运行时,它们会不断寻找不再使用的内存
2、在其他语言中,程序员必须显式地分配和释放内存
Rust采用了第三种方式:
1、内存是通过一个所有权系统来管理的,其中包含一组编译器在编译时检查的规则
2、当程序运行时,所有权特性不会减慢程序的运行速度
Stack vs Heap
Stack按值的接收顺序来存储,按相反的顺序将它们移除
所有存储在Stack上的数据必须拥有已知的固定大小,编译时大小未知的数据或运行时大小可能发生变化的数据必须放在heap上
Heap内存组织性差一些: 1、当你把数据放入Heap时,会请求一定数量的空间
2、操作系统在heap里找到一块足够大的空间,把它标记为在用,并返回一个指针,是这个空间的地址
3、这个过程叫做在heap上进行分配
访问heap中的数据要比访问stack中的数据慢,因为需要通过指针才能找到heap中数据
当你的代码调用函数时,值被传入到函数。函数本地的变量被压到stack上。当函数结束后,这些值会从stack上弹出
所有权存在的原因
所有权解决的问题: 1、跟踪代码的哪些部分正在使用heap的哪些数据
2、最小化heap上的重复数据量 3、清理heap上未使用的数据以避免空间不足
所有权规则
1、每个值都有一个变量,这个变量是该值的所有者
2、每个值同时只能有一个所有者
3、当所有者超出作用域(scope)时,该值将被删除
内存和分配
对于某个值来说,当拥有它的变量走出作用范围时,内存会立即自动地交换给操作系统
调用drop函数
移动
存放在stack中数据进行移动时无碍,但是如果时复合类型进行移动,两个变量的数据指向的是同一块heap上的内容,当离开作用域时,会释放两次内存,rust的解决方案是尝试让第一个变量失效,即当第一个变量离开作用域时不会释放内存(此时已经不能使用第一个变量了,因为它已经失效了)
1 2 3 4 fn main (){ let s1 = String ::from ("Hello" ); let s2 = s1; }
克隆
1 2 3 4 5 fn main (){ let s1 = String ::from ("Hello" ); let s2 = s1.clone (); }
函数
把一个值赋给其他变量变量就会发生移动,形参传入实参,那实参就会发生移动
如果是stack上的数据,那么发生拷贝
如果是heap上的数据,那么发生移动,它的值就会被drop清理
如何让函数使用某个值,但不获得其所有权?就是把传入的实参作为返回值返回即可
转移所有权
1 2 3 4 5 6 7 8 9 10 11 12 fn main (){ let s1 = String ::from ("Hello" ); let (s2,len) = calculate_length (s1); println! ("The length of {} is {}" ,s2,len); }fn calculate_length (s: String ) -> (String ,usize ){ let length = s.len (); (s,length) }
引用
1 2 3 4 5 6 7 8 9 fn main (){ let s1 = String ::from ("Hello" ); let len = calculate_length (&s1); println! ("The length of {} is {}" ,s1,len); }fn calculate_length (s: &String ) -> usize { s.len () }
借用: 1、我们把引用作为函数参数的这个行为叫做借用
2、不可以修改借用的东西 3、因为和变量一样,引用默认也是不可变的
加入mut使其引用可变(&mut)
1 2 3 4 5 6 7 8 fn main (){ let mut s1 = String ::from ("Hello" ); let len = calculate_length (&mut s1); }fn calculate_length (s: &mut String ) -> usize { s.push_str (",world!" ); s.len () }
可变引用有一个重要的限制:在特定作用域内,对某一块数据,只能有一个可变的引用
1 2 3 4 5 fn main (){ let mut s = String ::from ("Hello" ); let s1 = &mut s; let s2 = &mut s; }
好处:可在编译时防止数据竞争
三种引发数据竞争的行为: 1、两个或多个指针同时访问同一个数据
2、至少有一个指针用于写入数据 3、没有使用任何机制来同步对数据的访问
解决方案:可以通过创建新的作用域,来允许非同时地创建多个可变引用
1 2 3 4 5 6 7 fn main (){ let mut s = String ::from ("Hello" ); { let s1 = &mut s; } let s2 = &mut s; }
另一个限制: 1、不可以同时拥有一个可变引用和一个不可变的引用
2、多个不可变的引用是允许的
1 2 3 4 5 6 fn main (){ let mut s = String ::from ("Hello" ); let r1 = &s; let r2 = &s; let s1 = &mut s; }
悬空引用(Dangling References):
1、一个指针引用了内存中的某个地址,而这块内存可能已经释放并分配给其他人使用了
2、在Rust中,编译器可保证引用永远都不是悬空引用,如果你引用了某些数据,编译器能保证在引用离开作用域之前数据不会离开作用域
1 2 3 4 5 6 7 fn main (){ let r = dangle (); }fn dangle () -> &String { let s = String ::from ("Hello" ); &s }
切片
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 fn main (){ let mut s = String ::from ("Hello World!" ); let wordIndex = first_word (&s); println! ("{}" ,wordIndex); }fn first_word (s: &String ) -> usize { let bytes = s.as_bytes (); for (i,&item) in bytes.iter ().enumerate () { if item == b' ' { return i; } } s.len () }
针对上面代码的缺陷,rust提供了一种解决方案:字符串切片
字符串切片是指向字符串中一部分内容的引用
1 2 3 4 5 6 7 fn main (){ let s = String ::from ("Hello world" ); let hello = &s[0 ..5 ]; let world = &s[6 ..11 ]; let whole = &s[0 ,s.len ()]; }
形式:[开始索引..结束索引] 开始索引:切片起始位置的索引值
结束索引:切片终止位置的下一个索引值
解决最开始的问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 fn main (){ let mut s = String ::from ("Hello World!" ); let wordIndex = first_word (&s); println! ("{}" ,wordIndex); }fn first_word (s: &String ) -> &str { let bytes = s.as_bytes (); for (i,&item) in bytes.iter ().enumerate () { if item == b' ' { return &s[..i]; } } &s[..] }
字符串字面值是切片,字符串字面值被直接存储在二进制程序中
将字符串切片作为参数传递,即: fn first_word(s: &str) ->
&str {} 1、使用字符串切片,直接调用该函数
2、使用String,可以创建一个完整的String切片来调用该函数
因为这样就可以同时接收String和&str类型的参数了,定义函数时使用字符串切片来代替字符串引用会是我们的API更加通用,而且不会损失任何功能
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 fn main (){ let my_string = String ::from ("Hello World" ); let wordIndex = first_word (&my_string[..]); let my_string_literal = "Hello World" ; let wordIndex = first_word (my_string_literal); }fn first_word (s: &str ) -> &str { let bytes = s.as_bytes (); for (i,&item) in bytes.iter ().enumerate () { if item == b' ' { return &s[..i]; } } &s[..] }
其他类型的切片:
1 2 3 4 fn main (){ let a = [1 ,2 ,3 ,4 ,5 ]; let slice = &a[1 ..3 ]; }
标准类
字符串
Rust中字符串使用UTF-8编码
Rust的核心语言层面,只有一个字符串类型:字符串切片str(&str)
字符串切片:对存储在其他地方、UTF-8编码的字符串的引用
字符串字面值:存储在二进制文件中,也是字符串切片
String类
UTF-8编码
创建
1 2 3 4 5 6 7 8 9 fn main (){ let mut s0 String ::new (); let mut s = String ::from ("Hello" ); s.push_str (", World" ); println! ("{}" ,s); }
&str转换成String
1 2 3 4 5 6 7 8 fn main (){ let data = "Hello, world" ; let s = data.to_string (); let s1 = "Hello, world" .to_string (); let s2 = data.to_owned (); let s3 = data.into (); }
更新
push_str():把一个字符串切片附加到String
1 2 3 4 5 6 7 8 9 fn main (){ let mut s = String ::from ("foo" ); s.push_str ("bar" ); println! ("{}" ,s); }
push()方法:把单个字符附加到String
1 2 3 4 fn main (){ let mut s = String ::from ("lo" ); s.push ('l' ); }
+:连接字符串 使用了类似这个签名的方法:fn add(self, &str) ->
String{...} 解引用强制转化,把字符串引用转化成字符串切片
1 2 3 4 5 6 7 8 9 fn main (){ let s1 = String ::from ("Hello " ); let s2 = String ::from ("World" ); let s3 = s1 + &s2; println! ("{}" ,s3); println! ("{}" ,s2); }
format!:连接多个字符串(更灵活)
不会取得任何参数的所有权,这些参数后续都可以继续使用
1 2 3 4 5 6 7 8 9 10 11 fn main (){ let s1 = String ::from ("tic" ); let s2 = String ::from ("tac" ); let s3 = String ::from ("toe" ); let s = format! ("{}-{}-{}" ,s1,s2,s3); println! ("{}" ,s); }
返回字符串长度
1 2 3 4 5 6 7 fn main (){ let s = String ::from ("Hello" ); let s2 = String ::from ("你好" ); println! ("{}" ,s.len ()); println! ("{}" ,s2.len ()); }
访问
String类不能按索引的形式进行访问 String是对Vec的包装
Rust有三种看待字符串的方式:
字节、标量值、字形簇(最接近所谓的字母)
Rust不允许String进行索引的最后一个原因:
索引操作应消耗一个常量时间O(1),而String无法保证,需要遍历所有内容来确定有多少个合法的字符
切割String
可以使用[]和一个范围来创建字符串的切片
1 2 3 4 fn main (){ let hello = "Hello" ; let s = &hello[0 ..4 ]; }
遍历
对于标量值:chars()方法
1 2 3 4 5 6 7 8 fn main (){ let s = "Hello" ; for b in s.chars (){ println! ("{}" ,b); } }
对于字节:bytes()方法
1 2 3 4 5 6 7 fn main (){ let s = "Hello" ; for b in s.bytes (){ println! ("{}" ,b); } }
对于字形簇:很复杂,标准库未提供
注意
Rust选择将正确处理String数据作为所有Rust程序的默认行为:程序员必须在处理UTF-8数据之前投入更多的精力
但却可以防止在开发后期处理涉及非ASCII字符的错误
代替
1 2 3 4 fn main (){ let input = String ::from ("I think cars are cool" ); let string = input.replace ("cars" . "balloons" ); }
转化为小写字母
to_lowercase() trait
返回值是String类型
转化为大写字母
to_uppercase() trait
&str(字符串切片)
去除空格
1 2 3 4 5 6 7 8 9 fn main (){ let input = " Hello! " ; let string0 = input.trim ().to_string (); let string1 = input.trim_start ().to_string (); let string2 = input.trim_end ().to_string (); }
连接
1 2 3 4 5 6 fn main (){ let input = "Hello" ; let input1 = "world" ; let string = format! ("{}{}" ,input,input1); }
代替
1 2 3 4 fn main (){ let input = "I think cars are cool" ; let string = input.replace ("cars" . "balloons" ).to_string (); }
转化为小写字母
to_lowercase() trait
返回值是String类型
Vector
Vec 可存储相同类型的值,在堆上连续存放
创建
1 2 3 4 5 6 fn main (){ let v : Vec <i32 > = Vec ::new (); let v2 = vec! [1 ,2 ,3 ]; }
添加
1 2 3 4 fn main (){ let mut v = Vec ::new (); v.push (1 ); }
读取元素
1 2 3 4 5 6 7 8 9 10 11 12 fn main (){ let v = vec! [1 ,2 ,3 ,4 ,5 ]; let third1 : &i32 = &v[2 ] println! ("The third element is {}" ,third); match v.get (2 ){ Some (thied2) => println! ("The third element is {}" ,third); None => println! ("There is no third element" ); } }
所有权和借用规则
不能再同一作用域内同时拥有可变和不可变引用
1 2 3 4 5 fn main (){ let mut v = vec! [1 ,2 ,3 ,4 ,5 ]; ler first = &v[0 ]; v.push (6 }
遍历元素
1 2 3 4 5 6 7 8 9 10 11 fn main (){ let mut v = vec! [1 ,2 ,3 ,4 ,5 ]; for item in &mut v{ *item += 50 ; } for item in &v{ println! ("{}" ,item); } }
存放不同数据类型
1、使用enum,因为enum的变体可以附加不同类型的数据,而enum变体定义在同一个enum类型下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 enum SpreadSheetCell { Int (i32 ), Float (f64 ), Text (String ), }fn main () { let row : Vec <SpreadSheetCell> = vec! [ SpreadSheetCell::Int (3 ), SpreadSheetCell::Text (String ::from ("blue" )), SpreadSheetCell::Float (10.12 ), ]; }
2、使用Box
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 use std::any::Any;fn main () { let mut vec : Vec <Box <dyn Any>> = Vec ::new (); vec.push (Box ::new (42 )); vec.push (Box ::new (3.14 )); vec.push (Box ::new ("Hello, Rust!" .to_string ())); for item in vec { if let Some (i) = item.downcast_ref::<i32 >() { println! ("Integer: {}" , i); } else if let Some (f) = item.downcast_ref::<f64 >() { println! ("Float: {}" , f); } else if let Some (s) = item.downcast_ref::<String >() { println! ("Text: {}" , s); } } }
方法
from_slice
函数定义:
1 pub fn from_slice (slice: &[T]) -> Self
接受一个切片,返回一个包含切片元素副本的新集合示例,这在需要从切片中构建集合时非常有用,因为它允许你在不修改原始数据的情况下创建一个新的集合
1 2 let slice = &[1 ,2 ,3 ];let vec : Vec <i32 > = Vec ::from_slice (slice);
extend_from_slice
定义:
1 pub fn extend_from_slice (&mut self , other: &[T])
接受一个切片,将切片中的元素追加到集合的末尾
1 2 3 let mut vec = Vec ::new ();let slice = &[1 ,2 ,3 ]; vec.extend_from_slice (slice);
copy_from_slice
定义:
1 pub fn copy_from_slice (src: &[T]) -> usize
接受一个切片作为参数,并将切片中的内容复制到调用者所代表的可变位置
1 2 3 let mut array = [0 ;5 ];let src = [1 ,2 ,3 ,4 ,5 ];let copy_count = array.copy_from_slice (&src);
HashMap
创建
创建空的HashMap:new()函数
1 2 3 4 5 use std::collections::HashMap;fn main (){ let mut scores : HashMap<String ,i32 > = HashMap::new (); }
使用collect方法:
1 2 3 4 5 6 7 8 use std::collections::HashMap;fn main (){ let teams = vec! [String ::from ("Blue" ),String ::from ("Yellow" )]; let initial_scores = vec! [10 ,50 ]; let scores : HashMap<_,_> = teams.iter ().zip (initial_scores.iter ()).collect (); }
添加
insert()方法
1 2 3 4 5 6 7 use std::collections::HashMap;fn main (){ let mut scores = HashMap::new (); scores.insert (String ::from ("Blue" ),10 ); scores.insert (String ::from ("Yellow" ),10 ); }
所有权
1、对于实现了Copy trait的类型(例如i32),值会被复制到HashMap中
2、对于拥有所有权的值(例如String),值会被移动,所有权会转移给HashMap
3、如果将值的引用插入到HashMap,值本身不会移动
访问
get方法(返回Option<&T>):
1 2 3 4 5 6 7 8 9 10 11 12 13 use std::collections::HashMap;fn main (){ let mut scores = HashMap::new (); scores.insert (String ::from ("Blue" ),10 ); scores.insert (String ::from ("Yellow" ),50 ); let team_name = String ::from ("Blue" ); let score = scores.get (&team_name); match score{ Some (s) => println! ("{}" ,s); None => println! ("team not exist" ); } }
遍历
1 2 3 4 5 6 7 8 9 10 11 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); } }
更新
1、HashMap大小可变 2、每个k对应一个v 3、更新数据
k已经存在,对应一个v: 替换现有的v 保留现有的v,忽略新的v
合并现存的v和新的v k不存在: 添加一对k,v
entry方法:检查指定的k是否对应一个v 参数为k,返回enum
Entry:代表值是否存在 or_insert方法: 如果k存在,返回到对应的v
的一个可变引用
如果k不存在,将方法参数作为k的新值插入进去,返回到这个值的可变引用
1 2 3 4 5 6 7 8 9 10 11 12 use std::collections::HashMap;fn main (){ let mut scores = HashMap::new (); scores.insert (String ::from ("Blue" ),10 ); scores.entry (String ::from ("Yellow" )).or_insert (50 ); scores.entry (String ::from ("Blue" )).or_insert (50 ); println! ("{:?}" ,scores); }
字符串单词计数器:
1 2 3 4 5 6 7 8 9 10 11 use std::collections::HashMap;fn main (){ let text = "hello wolrd 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); }
Hash函数
默认情况下,HashMap使用加密功能强大的Hash函数,可以抵抗拒绝服务(DOS)攻击
不是可用的最快的Hash算法,但具有更好的安全性
可以指定不同的Hasher来切换到另一个函数 hasher是实现BuildHasher
trait的类型
计算长度
len() trait
计算value的和
values().sum::()
枚举
定义枚举
IP地址:IPv4,IPv6
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 enum IpAddrKind { V4, V6, }struct IpAddr { kind: IpAddrKind, address: String , }fn main (){ let four = IpAddrKind::V4; let six = IpAddrKind::V6; let home = IpAddr{ kind: IpAddrKind::V4, address: String ::from ("127.0.0.1" ); } }
将数据附加到枚举的变体中
优点: 不需要使用额外的struct
每个变体可以拥有不同的类型以及关联的数据量
1 2 3 4 5 6 7 8 9 10 enum IpAddr { V4 (u8 ,u8 ,u8 ,u8 ), V6 (String ), Move {x: i32 , y: i32 }, }fn main (){ let home = IpAddrKind::V4 (127 ,0 ,0 ,1 ); let loopback = IpAddrKind::V6 (String ::from ("::1" )); }
1 2 3 4 5 6 7 8 9 struct Ipv4Addr { }struct Ipv6Addr { }enum IpAddr { V4 (Ipv4Addr), V6 (Ipv6Addr), }
1 2 3 4 5 6 7 8 9 10 11 12 enum Message { Quit, Move {x: i32 ,y: i32 }, Write (String ), ChangeColor (i32 ,i32 ,i32 ); }fn main (){ let q = Message::Quit; let m = Message::Move{x:12 ,y:24 }; let w = Message::Write (String ::from ("H" )); let c = Message::ChangeColor (0 ,255 ,255 ); }
定义方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 enum Message { Quit, Move {x: i32 ,y: i32 }, Write (String ), ChangeColor (i32 ,i32 ,i32 ); }impl Message { fn call (&self ){} }fn main (){ let q = Message::Quit; let m = Message::Move{x:12 ,y:24 }; let w = Message::Write (String ::from ("H" )); let c = Message::ChangeColor (0 ,255 ,255 ); m.call (); }
打印枚举
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #[derive(Debug)] enum Message { Quit, Echo (String ), Move {x: i32 , y: i32 }, ChangeColor (i32 , i32 , i32 ), }impl Message { fn call (&self ){ println! ("{:?}" ,self ); } }fn main (){ let messages : [Message;4 ] = [ Message::Move {x:10 , y:30 }, Message::Echo (String ::from ("Hello" )), Message::ChangeColor (200 ,255 ,255 ), Message::Quit, ] for message : &Message in &messages{ message.call (); } }
Option枚举
定义于标准库中,描述了某个值可能存在或不存在的情况
在预导入模块中,可直接使用
在rust中,没有null,但是有类似null概念的枚举也就是option
1 2 3 4 5 6 7 8 enum Option <T>{ Some (T), None , }
1 2 3 4 5 6 fn main { let some_number = Some (5 ); let some_string = Some ("A String" ); let absent_number : Option <i32 > = None ; }
使用Option比null的好处:
Option和T是不同的类型,如果要使用,必须将Option转换成T
1 2 3 4 5 6 7 8 9 10 11 12 fn main (){ let five = Some (5 ); let six = plus_one (five); let none = plus_one (none); }fn plus_one (x: Option <i32 >) -> Option <i32 >{ match x{ None => None , Some (i) => Some (i+1 ), } }
错误处理
概述
Rust的可靠性:大部分情况下,编译时提示错误并处理
错误的分类: 可恢复:例如文件未找到,可再次尝试
不可恢复:bug,例如访问的索引超出范围
Rust没有类似异常的机制: 可恢复错误:Result<T,E>
不可恢复错误:panic! 宏
不可恢复错误
当panic!宏执行: 1、程序会打印一个错误信息
2、展开(unwind)、清理调用栈(stack) 3、退出程序
为应对panic,展开或中止(abort)调用栈 默认情况下,当panic发生:
1、程序展开调用栈(工作量大):Rust沿着调用栈往回走,清理每个遇到的函数中的数据
2、或立即中止调用栈:不进行清理,直接停止程序,内存需要OS进行清理
想让二进制文件更小,把设置从“展开”改为“中止”:
在Cargo.toml中的profile部分设置,panic = 'abort' 例子:
1 2 [profile.release] panic = 'abort'
panic!可能出现在:我们写的代码中或我们所依赖的代码中
可通过调用panic!的函数的回溯信息来定位引起问题的代码。
通过设置环境变量RUST_BACKTRACE可得到回溯信息
可恢复的错误
Result枚举
enum Result<T, E> { Ok(T), Err(E), }
T:操作成功情况下,Ok变体里返回的数据的类型
E:操作失败情况下,Err变体里返回的错误的类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 use std::fs::File;use std::io::ErrorKind;fn main (){ let f = File::open ("hello.txt" ); let f = match f{ Ok (file) => file, Err (error) => match error.kind (){ ErrorKind::NotFound => match File::create ("hello.txt" ){ Ok (file) => file, Err (error) => panic! ("Error creating file: {:?}" , error), }, other_error => panic! ("Error opening the file: {:?}" ,other_error), } }; }
使用unwrap_or_else方法改良: 如果是Ok返回里面的变体
如果是Err调用后面的匿名函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 use std::fs::File;use std::io::ErrorKind;fn main (){ let f = File::open ("hello.txt" ).unwrap_or_else (|error|{ if error.kind () == ErrorKind::NotFound{ File::create ("hello.txt" ).unwrap_or_else (|error|{ panic! ("Problem creating the file: {:?}" , error); }) }else { panic! ("Problem opening the file: {:?}" , error); } }); }
unwrap
match表达式的一个快捷方法: 如果Result结果是Ok,返回Ok里面的值
如果Result结果是Err,调用panic!宏
1 2 3 4 5 6 7 8 9 10 11 12 use std::fs::File;fn main (){ let f = File::open ("hello.txt" ).unwrap (); }
expect
和unwrap类似,但可指定错误信息
1 2 3 4 5 6 7 8 9 10 11 12 use std::fs::File;fn main (){ let f = File::open ("hello.txt" ).expect ("无法打开文件hello.txt" ); }
传播错误
将错误返回给调用者
常规方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 use std::fs::File;use std::io::{self ,Read};fn read_username_from_file () -> Result <String , io::Error>{ let f = File::open ("hello.txt" ); let mut f = match f{ Ok (file) => file, Err (error) => return Err (error), }; let mut s = String ::new (); match f.read_to_string (&mut s){ Ok (_) => Ok (s), Err (error) => Err (error), } }fn main (){ let result = read_username_from_file (); }
?运算符:传播错误的一种快捷方式
1 2 3 4 5 6 7 8 9 use std::fs::File;use std::io::{self ,Read};fn read_username_from_file ()-> Result <String ,io::Error> { let mut f = File::open ("hello.txt" )?; let mut s = String ::new (); f.read_to_string (&mut s)?; Ok (s); }
如果Result是Ok:Ok中的值就是表达式的结果,然后继续执行程序
如果Result是Err:Err就是整个函数的返回值,就像使用了return
?与from函数 Trait std::convert::From 上的from函数,用于错误之间的转换
被?所应用的错误,会隐式的被from函数处理,即它所接收的错误类型会被转化为当前函数返回类型所定义的错误类型
使用链式继续优化:
1 2 3 4 5 6 7 8 use std::fs::File;use std::io::{self ,Read};fn read_username_from_file () -> Result <String ,io::Error> { let mut s = String ::new (); File::open ("hello.txt" )?.read_to_string (&mut s)?; Ok (s); }
?运算符只能用于返回Result的函数
main函数的默认返回类型是(),可以修改成返回Result类型:
1 2 3 4 5 6 7 8 9 use std::error::Error;use std::fs::File;fn main () -> Result <(), Box <dyn Error>> { let f = File::open ("hello.txt" )?; Ok (()) }
使用panic!的时机
总体原则:
在定义一个可能失败的函数时,优先考虑返回Result,否则就panic!
编写示例、原型代码、测试代码可用unwrap、expect
可以确定Result是Ok,可以使用unwrap
错误处理的指导性建议: 当代码最终可能处于损坏状态时,最好使用panic!
损坏状态:某些假设、保证、约定或不可变性被打破
(例如非法的值、矛盾的值或空缺的值被传入代码,以及下列中的一条:这种损坏状态并不是预期能够偶尔发生的事情;在此之后,代码如果处于这种损坏状态就无法运行;在使用的类型中没有一个好的方法来将这些信息进行编码)
场景建议: 1、调用你的代码,传入无意义的参数值:panic!
2、调用外部不可控代码,返回非法状态,你无法修复:panic!
3、如果失败是可预期的:Result!
4、当你的代码对值进行操作,首先应该验证这些值:panic!
为验证创建自定义类型:
创建新的类型,把验证逻辑放在构造示例的函数里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 pub struct Guess { value: i32 , }impl Guess { pub fn new (value: i32 ) -> Guess { if value < 1 || value > 100 { panic! ("Guess value must be between 1 and 100, got {}" , value); } Guess {value}; } pub fn value (&self ) -> i32 { self .value } }
错误处理代码
part1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 impl ParsePosNonzeroError { fn from_creation (err: CreationError) -> ParsePosNonzeroError { ParsePosNonzeroError::Creation (err) } fn from_parseint (err: ParseIntError) -> ParsePosNonzeroError { ParsePosNonzeroError::ParseInt (err) } }fn parse_pos_nonzero (s: &str ) -> Result <PositiveNonzeroInteger, ParsePosNonzeroError> { let x : i64 = s.parse ().map_err (ParsePosNonzeroError::from_parseint)?; PositiveNonzeroInteger::new (x).map_err (ParsePosNonzeroError::from_creation) }
方法
ok_or
接受一个参数,该参数是一个默认的错误值
1 2 let option_value : Option <i32 > = None ;let result = option_value.ok_or ("Default Error" );
ok_or_else
接受一个闭包作为参数
1 2 let option_value : Option <i32 > = None ;let result = option_value.ok_or_else (|| "Defalut Error" .to_string ());
ok_or方法更适合提供静态的错误值,而ok_or_else方法更适合提供需要计算的错误值。
泛型
提取函数
作用:消除重复代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 fn largerst (list: &[i32 ]) -> i32 { let mut largest = list[0 ]; for &item in list { if item > largerst { largest = item; } } largest }fn main () { let number_list = [34 ,50 ,25 ,100 ,65 ]; let result = largest (&number_list); }
简介
提高代码复用能力
泛型是具体类型或其他属性的抽象代替:
1、编写的代码不是最终的代码,而是一种模板,里面有一些“占位符”
2、编译器在编译时将“占位符”替换成具体的类型 例如:fn largest(list:
&[T]) -> T {...}
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 fn largerst <T>(list: &[T]) -> T { let mut largest = list[0 ]; for &item in list { if item > largerst { largest = item; } } largest }
struct定义中的泛型可以使用多个泛型的类型参数,但太多类型参数说明你的代码需要重组为多个更小的单元:
1 2 3 4 struct Point <T1, T2> { x: T1, y: T2, }
enum定义中的泛型,可以让枚举的变体持有泛型数据类型:
1 2 3 4 5 6 7 8 enum Option <T> { Some (T), None , }enum Result <T,E> { Ok (T), Err (E), }
为struct或enum实现方法的时候, 可在定义中使用泛型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct Point <T> { x: T, y: T, }impl <T> Point<T> { fn x1 (&self ) -> &T { &self .x } }impl Point <i32 > { fn x2 (&self ) -> &i32 { &self .x } }
struct里的泛型类型参数可以和方法的泛型类型参数不同:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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, } } }
泛型代码的性能:
1、使用泛型的代码和使用具体类型的代码运行速度是一样的
2、单态化:在编译时将泛型替换为具体类型的过程
Trait
简介
Trait告诉编译器某种类型具有哪些并且可以与其他类型共享的功能,抽象的定义共享行为,与其他语言的接口有点类似,但有些区别。
Trait bounds: 泛型类型参数指定为实现了特定行为的类型
定义
把方法签名放在一起,来定义实现某种目的所需的一组行为。
1、只有方法签名,没有具体实现
2、trait可以有多个方法,每个方法签名占一行,以:结尾
3、实现该trait的类型必须提供具体的方法实现
示例:
1 2 3 4 pub trait Summary { fn summarize (&self ) -> String ; fn summarize1 (&self ) -> String ; }
实现trait:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 pub trait Summary { fn summarize (&self ) -> String ; }pub struct NewsArticle { pub headline: String , pub location: String , pub author: String , pub content: String , }impl Summary for NewsArticle { fn summarize (&self ) -> String { format! ("{}, by {} ({})" , self .headline, self .author, 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) } }
实现trait的约束:
1、可以在某个类型上实现某个trait的前提条件是:这个类型或这个trait是在本地crate里定义的
2、无法为外部类型来实现外部的trait:
这个限制是程序属性的一部分(也就是一致性),更具体地说是孤儿原则,之所以这样命名是因为夫类型不存在。此规则确保其他人的代码不能破坏你的代码。
如果没有这个规则,两个crate可以为同一类型实现一个trait,rust就不知道应该使用哪个实现
默认实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 pub trait Summary { fn summarize (&self ) -> String { String ::from ("Read more ..." ) } }pub struct NewsArticle { pub headline: String , pub location: String , pub author: String , pub content: String , }impl Summary for NewsArticle { }pub struct Tweet { pub username: String , pub content: String , pub reply: bool , pub retweet: bool , }impl Summary for Tweet { }
把trait作为函数参数
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 use std::fmt::Display;pub trait Summary { fn summarize (&self ) -> String ; }pub struct NewsArticle { pub headline: String , pub location: String , pub author: String , pub content: String , }impl Summary for NewsArticle { fn summarize (&self ) -> String { format! ("{}, by {} ({})" , self .headline, self .author, 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) } }pub fn notify (item: impl Summary ) { println! ("Breaking news! {}" , item.summarize ()); }pub fn notify <T: Summary>(item: T) { println! ("Breaking news! {}" ,item.summarize ()); }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 ()); }
使用where关键字优化代码:
1 2 3 4 5 6 7 8 9 10 11 pub fn notiyf <T: Summary + Display, U: Clone + Debug >(a: T,b: U) -> String { format! ("Breaking news! {}" , a.summarize ()); }pub fn notify2 <T,U>(a:T, b:U) -> String where T: Summary + Display, U: Clone + Debug , { format! ("Breaking news! {}" ,a.summarize ()); }
把trait当作返回类型
1 2 3 pub fn notify (s: &str ) -> impl Summary { }
例子
修复上面的largest函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 fn largest <T: PartialOrd + Copy > (list: &[T]) -> T { let mut largest = list[0 ]; for &item in list.iter () { if item > largest { largest = item; } } largest }fn largest <T: PartialOrd + Copy > (list: &[T]) -> T { let mut largest = list[0 ].clone (); for item in list.iter () { if *item > largest { largest = item.clone (); } } largest }fn largest <T: PartialOrd + Copy > (list: &[T]) -> &T { let mut largest = &list[0 ]; for item in list.iter () { if item > &largest { largest = item; } } largest }
可以有条件地为实现了特定Trait的类型来实现方法:(即只有这个类型拥有了指定的Trait,才能使用这个方法)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 struct Pair <T> { x: T, y: T, }impl <T> Pair<T> { fn new (x: T, y: Y) -> Self { Self {x, y} } }impl <T: Display + PartialOrd > Pair<T> { fn cmp_diplay (&self ) { if self .x > self .y { println! ("The largest member is x = {}" , self .x); }else { println! ("The largest member is y = {}" , self .y); } } }
也可以为实现了其他Trait的任意类型有条件地实现某个Trait,就是说某个类型如果实现了某个Trait,那么就为它实现另外一个Trait。
为满足Trait
Bound的所有类型上实现Trait叫做覆盖实现,就上面说的那句话的操作就是覆盖实现,
生命周期
简介
1、Rust的每个引用都有自己的生命周期 2、生命周期:引用保持有效的作用域
3、大多数情况:生命周期是隐式的、可被推断的
4、当引用的生命周期可能以不同的方式互相关联时:手动标注生命周期
生命周期的主要目标:避免悬垂引用
借用检查器:比较作用域判断所有的借用是否合法。(被借用的数据生命周期不小于借用数据的生命周期)
输入生命周期:函数/方法的参数 输出生命周期:函数/方法的返回值
生命周期标注语法
无法比较时,需要显式注明生命周期:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 fn main () { let string1 = String ::from ("abcd" ); let string2 = "xyz" ; let result = longest (string1.as_str (),string2); println! ("The result is {}" , result); }fn longest <'a >(x : &'a str , y: &'a str ) -> &a' str { if x.len () > y.len () { x } else { y } }
生命周期的标注:描述了多个引用的生命周期间的关系,但不影响生命周期。
参数名:以'开头,通常全小写且非常短,很多人使用 'a 例子: &i32
&'a i32 &'a mut i32 单个生命周期标注本身没有意义
深入理解生命周期
指定生命周期参数的方式依赖于函数所做的事情:
1 2 3 4 5 6 7 8 9 10 fn main () { let string1 = String ::from ("abcd" ); let string2 = "xyz" ; let result = longest (string1.as_str (),string2); println! ("The result is {}" , result); }fn longest <'a >(x : &'a str , y: &str ) -> &a' str { x }
从函数返回引用时,返回类型的生命周期参数需要与其中一个参数的生命周期匹配,如果返回的引用没有指向任何参数,那么它只能引用函数内创建的值,这就是悬垂引用,该值在函数结束时就走出了作用域:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 fn main () { let string1 = String ::from ("abcd" ); let string2 = "xyz" ; let result = longest (string1.as_str (),string2); println! ("The result is {}" , result); }fn longest <'a >(x : &'a str , y: &str ) -> &a' str { let result = String ::from ("abc" ); result.as_str (); }fn longest <'a >(x: &'a str , y: &str ) -> String { let result = String ::from ("abc" ); result }
Struct定义中的生命周期标注
Struct里可以包括:自持有的类型;引用(需要在每个引用上添加生命周期标注)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct ImportantExcept <'a > { part: &'a str , }fn main () { let novel = String ::from ("Call me Ishmael. Some years ago ..." ); let first_sentence = novel.split ('.' ).next ().expect ("Could not found a '.'" ); let i = ImportantExcept { part: first_sentence, }; }
生命周期省略规则
在Rust引用分析中所编入的模式称为生命周期省略规则,这些规则无需开发者来遵循,它们是一些特殊情况,由编译器来考虑,如果你的代码符合这些特殊情况,那么就无需显式标注生命周期。
编译器使用3个规则在没有显式标注生命周期的情况下,来确定引用的生命周期,如果编译器应用完3个规则之后,仍然有无法确定生命周期的引用,那就会报错。
这些规则适用于fn定义和impl块
规则: 1、每个引用类型的参数都有自己的生命周期
2、如果只有1个输入生命周期参数,那么该生命周期被赋给所有输出生命周期参数
3、如果有多个输入生命周期参数,但其中一个是&self或&mut
self(只适用于方法),那么self的生命周期会被赋给所有的输出生命周期参数
如果不遵循上面三个规则,就需要自己标注生命周期
方法定义中的生命周期标注
1、在struct上使用生命周期实现方法,语法和泛型参数的语法一样
2、在哪生命和使用生命周期参数,依赖于生命周期参数是否和字段、方法的参数和返回值有关
3、struct字段的生命周期名在impl后生命,在struct名后使用
4、生命周期省略规则经常使得方法中的生命周期标注不是必须的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct ImportantExcept <'a > { part: &'a str , }impl <'a > ImportantExcept<'a > { fn level (&self ) -> i32 { 3 } fn annouce_and_return_part (&self , announcement: &str ) -> &str { println! ("Attention please: {}" . announcement); self .part } }
静态生命周期
'static 是一个特殊的生命周期,即整个程序的持续时间。
例如:所有的字符串字面值都拥有'static生命周期
1 let s : &'static str = "Hello, world!" ;
为引用指定'static生命周期前要三思:是否需要引用在程序整个生命周期内都存活
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 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! ("Anncouncement! {}" , ann); if x.len () > y.len () { x } else { y } }
测试
简介
测试实际上就是一个函数,用来验证非测试代码的功能是否和预期一致
测试函数体通常执行的3个操作(3a): 1、准备数据/状态
2、运行被测试的代码 3、断言(Assert)结果
如何使用:
测试函数需要使用test属性(attribute)进行标注,attribute就是一段Rust代码的元数据,在函数上加#[test],可把函数变成测试函数
运行测试: 1、使用cargo test 命令运行所有测试函数,rust会构建一个test
runner 可执行文件,它会运行标注了test的函数,并报告其运行是否成功。
2、当使用cargo创建library项目时,会生成一个test
module,里面就有一个test函数。 3、你可以添加任意数量的test module 或
函数
测试失败: 1、测试函数panic就表示失败 2、每个测试运行在一个新线程
3、当主线程看见某个测试线程挂掉了,那个测试标记为失败了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 pub fn add (left: usize , right: usize ) -> usize { left + right }#[cfg(test)] mod tests { use super::*; #[test] fn it_works () { let result = add (2 , 2 ); assert_eq! (result, 4 ); } #[test] fn another () { panic! ("Make this test fail" ); } }
断言(assert)
使用assert!宏检查测试结果,用来确定某个状态是否为true,如果返回false,调用panic,测试失败
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #[derive(Debug)] pub struct Rectangle { length: u32 , width: u32 , }impl Rectangle { pub fn can_hold (&self , other: &Rectangle) -> bool { self .length > other.length && self .width > other.width } }#[cfg(test)] mod tests { use super::*; #[test] fn larger_can_hold_smaller () { let larger = Rectangle { length: 8 , width: 7 }; let smaller = Rectangle { length: 5 , width: 1 }; assert! (larger.can_hold (&smaller)); } fn smaller_cannot_hold_larger () { let larger = Rectangle { length: 8 , width: 7 }; let smaller = Rectangle { length: 5 , width: 1 }; assert! (!smaller.can_hold (&larger)); } }
使用assert_eq!(相等)和assert_ne!(不等)测试相等性
断言失败:自动打印出两个参数的值,使用debug格式打印参数,要求参数实现了PartialEq和Debug
Traits(所有 基本类型和标准库里的大部分类型都实现了)
自定义错误信息
可以想assert!、assert_eq!、
assert_ne!添加可选的自定义信息,这些自定义信息和失败消息都会打印出来。
assert!:第一个参数必填,自定义消息作为第二个参数
assert_eq!和assert_nq!:前两个参数必填,自定义消息作为第三个参数
自定义消息参数会被传递给format!宏,可以使用{}占位符
should_panic
验证错误处理的情况:
测试除了验证代码的返回值是否正确,还需验证代码是否如预期地处理了发生错误的情况。
可验证代码在特定情况下是否发生了panic
需要在代码中添加should_panic属性: 函数发生panic:测试通过
函数没有发生panic:测试失败
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 pub struct Guess { value: i32 , }impl Guess { pub fn new (value: i32 ) -> Guess { if value < 1 || value > 100 { panic! ("Guess value must be betwwen 1 and 100, got {}" , value); } Guess { value, } } }#[cfg(test)] mod tests { use super::*; #[test] #[should_panic] fn greater_than_100 () { Guess::new (200 ); } }
让should_panic更精确,为should_panic属性添加一个可选的expected参数,将检查失败消息中是否包含所指定的文字
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 pub struct Guess { value: i32 , }impl Guess { pub fn new (value: i32 ) -> Guess { if value < 1 { panic! ("Guess value must be greater than or equal to 1, got {}" , value); } else if value > 100 { panic! ("Guess value must be less than or equal to 100, got {}." , value); } Guess { value, } } }#[cfg(test)] mod tests { use super::*; #[test] #[should_panic(expected = "Guess value must be less than or equal to 100" )] fn greater_than_100 () { Guess::new (200 ); } }
在测试中使用Result
无需panic,可使用Result<T, E>作为返回类型编写测试:
返回Ok:测试通过 返回Err:测试失败
1 2 3 4 5 6 7 8 9 10 11 #[cfg(test)] mod tests { #[test] fn it_works () -> Result <(), String >{ if 2 + 2 == 4 { Ok (()) } else { Err (String ::from ("two plus does not equal four" )) } } }
注意:不要在使用Result<T, E>编写的测试上标注#should_panic
控制测试运行
改变cargo test的行为:添加命令行参数
如果不添加命令行参数,会执行默认行为: 1、并行运行所有测试
2、捕获(不显示)所有输出,使读取与测试结果相关的输出更容易
命令行参数: 1、针对cargo test的参数,紧跟在cargo test后
2、针对测试可执行程序:放在 --之后
cargo test --help(显示可以放在cargo test后面的参数) 和 cargo test
-- --help(显示可以放在cargo test --后面的参数)
并行运行测试
默认使用多个线程并行运行
确保测试之间: 1、不会相互依赖
2、不依赖于某个共享状态(环境、工作目录、环境变量等等)
--test-threads 参数
1、传递给二进制文件
2、不想以并行方式运行测试,或想对线程数进行细粒度控制 3、可以使用
--test-threads 参数,后边跟着线程的数量
例如: cargo test -- --test-threads=1
显式函数输出
默认,如测试通过,rust的test库会捕获所有打印到标准输出的内容
例如:如果被测试段代码中用到了println!:
1、如果测试通过:不会在终端看到println!打印的内容
2、如果测试失败:会看到println!打印的内容和失败信息
如果想在成功的测试中看到打印的内容:cargo test -- --show-output
按名称运行测试
选择运行的测试:将测试的名称(一个或多个)作为cargo test的参数
运行单个测试:指定测试名 例子:cargo test 测试函数名称
运行多个测试:指定测试名的一部分(模块名也可以)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 hundred () { assert_eq! (102 , add_two (100 )); } }
运行多个测试: 1、cargo test add:
运行add_two_and_two和add_three_and_two两个测试 2、cargo test tests:
运行tests这个模块的测试
忽略测试
可以忽略运行比较耗时的测试
ignore属性
1 2 3 4 5 6 7 8 9 10 11 12 13 #[cfg(test)] mod tests { #[test] fn it_works () { assert_eq! (2 + 2 , 4 ); } #[test] #[ignore] fn expensive_test () { assert_eq! (5 ,1 +1 +1 +1 +1 ); } }
想要运行忽略的测试: cargo test -- --ignored
测试的分类
单元测试和集成测试
单元测试: 1、小、专注 2、一次对一个模块进行隔离的测试
3、可测试private接口
集成测试: 1、在库外部。和其他外部代码一样使用你的代码
2、只能使用public接口 3、可能在每个测试中使用到多个模块
单元测试
单元测试用#[cfg(test)]标注:只有运行cargo test才编译和运行代码
集成测试则不需要#[cfg(test)],集成测试在不同的目录
rust允许测试私有函数
集成测试
在rust中,集成测试完全位于被测试库的外部
目的:是测试被测试库的多个部分是否能正确地一起工作
创建集成测试:创建tests目录,tests目录下的每个测试文件都是单独的一个crate(需要将被测试库导入)
无需标注#[cfg(test)],tests目录被特殊对待,只有cargo
test,才会编译tests目录下的文件
运行指定的集成测试: 1、运行一个特定的集成测试: cargo test 函数名
2、运行某个测试文件内的所有测试:cargo test --test 文件名
如果想要在tests目录下创建其他辅助函数而不用进行测试运行,则在tests目录下重新创建一个文件夹,然后把辅助函数放在这个文件夹中。(文件命名可以为mod.rs)
(使用该模块要导入,在代码中用mod 文件夹名即可导入)
针对binary crate的集成测试: 如果项目是binary
crate,只有含有main.rs没有lib.rs,不能在tests目录下创建集成测试,无法把main.rs的函数导入作用域
只有library crate才能暴露函数给其他crate用,binary
crate意味着独立运行
文件处理
文件读取
用String类存储:
1 2 3 4 5 use std::fs;fn main () { let contents = fs::read_to_string ("hello.txt" ).expect ("文件读取失败!" ); }
闭包
简介
闭包:可以捕获其所在环境的匿名函数
特性: 1、是匿名函数 2、保存为变量、作为参数
3、可在一个地方创建闭包,然后在另一个上下文中调用闭包来完成运算
4、可从其定义的作用域捕获值
例子
名称:生成自定义运动计划的程序
算法的逻辑并不是重点,重点是算法中的计算过程所耗费的时间
目标:仅在必要时调用该算法,只调用一次
原本代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 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 simulated_expensive_calculation (intensity: u32 ) -> u32 { println! ("正在计算……" ); thread::sleep (Duration::from_secs (2 )); intensity }fn generate_workout (intensity: u32 , random_number: u32 ) { if intensity < 25 { println! ( "Today, do {} pushups!" , simulated_expensive_calculation (intensity) ); println! ( "Next, do {} situps!" , simulated_expensive_calculation (intensity) ); } else { if random_number == 3 { println! ("Take a break today! Remember to stay hydrated!" ); } else { println! ( "Today, run for {} minutes!" , simulated_expensive_calculation (intensity) ); } } }
第一步改进generate_workout函数:
(用变量存储函数的返回值,但是不需要的情况又造成浪费)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 fn generate_workout (intensity: u32 , random_number: u32 ) { let result = simulated_expensive_calculation (intensity); if intensity < 25 { println! ( "Today, do {} pushups!" , result ); println! ( "Next, do {} situps!" , result ); } else { if random_number == 3 { println! ("Take a break today! Remember to stay hydrated!" ); } else { println! ( "Today, run for {} minutes!" , result ); } } }
用闭包改进: (可以用闭包的特性继续改进)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 fn generate_workout (intensity: u32 , random_number: u32 ) { let expensive_closure = |num: u32 | { println! ("calculating 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) ); } } }
闭包类型推断和标注
闭包的类型推断: 1、闭包不要求标注参数和返回值的类型
2、闭包通常很短小,只在狭小的上下文中工作,编译器通常能推断出类型
3、可以手动添加类型标注
注意:闭包的定义最终只会为参数/返回值推断出唯一具体的类型
1 2 3 4 5 6 7 fn main () { let example_closure = |x| x; }
存储闭包
使用泛型参数和fn trait来存储闭包
记忆化/延迟计算:
创建一个struct,它持有闭包及其调用结果,只会在需要结果时才执行该闭包,可缓存结果
如何让struct持有闭包:
struct的定义需要知道所有字段的类型:需要指明闭包的类型,每个闭包实例都有自己唯一的匿名类型,即使两个闭包签名完全一样,所以需要使用泛型和trait
bound
fn traits由标准库提供 所有的闭包都至少实现了以下trait之一:Fn FnMut
FnOnce
改进上面的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 struct Cacher <T>where T: Fn (u32 ) -> u32 , { calculation: T, value: Option <u32 >, }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 generate_workout (intensity: u32 , random_number: u32 ) { let mut expensive_closure = Cacher::new (|num|{ println! ("calculating 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实现的限制:
1、Cacher实例假定针对不同的参数arg,value方法总会得到同样的值,可以使用HashMap,key:arg参数,value:执行闭包的结果
2、只能接收一个u32类型的参数和u32类型的返回值
使用闭包捕获上下文
闭包可以捕获它们所在的环境:闭包可以访问定义它的作用域内的变量,而普通函数则不能。但会产生内存开销。
1 2 3 4 5 6 7 8 9 fn main () { let x = 4 ; let equal_to_x = |z| z == x; let y = 4 ; assert! (equal_to_x (y)); }
闭包从所在环境捕获值得方式: 与函数获得参数的三种方式一样:
1、取得所有权:FnOnce 2、可变借用:FnMut 3、不可变借用:Fn
创建闭包时,通过闭包对环境值的使用,rust能推断出具体使用哪个trait:
所有闭包都实现了FnOnce 没有移动捕获变量的实现了FnMut
无需可变访问捕获变量的闭包实现了Fn
move关键字:
在参数列表前使用move关键字,可以强制闭包取得它所使用的环境值得所有权。当将闭包传递给新线程以移动数据使其归新线程所有时,此技术最为有用。
1 2 3 4 5 6 7 8 fn main () { let x = vec! [1 ,2 ,3 ]; let equal_to_x = move |z| z == x; let y = vec! [1 ,2 ,3 ]; assert! (equal_to_x (y)); }
最佳实践: 当指定Fn trait bound
之一时,首先用Fn,基于闭包体里的情况,如果需要FnOnce或FnMut,编译器会再告诉你
迭代器
简介
迭代器模式:对一系列执行某些任务
迭代器负责:遍历每个项;确定序列(遍历)何时完成
rust的迭代器:
懒惰的:除非调用消费迭代器的方法,否则迭代器本省没有任何效果
iterator trait和next方法
iterator trait
所有迭代器都实现了iterator trait
iterator trait定义与标准库,定义大致如下:
1 2 3 4 pub trait Iterator { type Item ; fn next (&mut self ) -> Option <Self ::Item>; }
type Item和 Self::Item定义了与此该trait关联的类型: 实现Iterator
trait需要你定义一个Item类型,它用于next方法的放回类型
next
每次返回迭代器中的一项,返回结果包裹在Some里,迭代结束,返回None
1 2 3 4 5 6 7 8 9 10 11 12 #[cfg(test)] mod tests { #[test] fn iterator_demonstration () { let v1 = vec! [1 ,2 ,3 ]; 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 )); } }
几个迭代方法
iter方法:在不可变引用上创建迭代器
into_iter方法:创建的迭代器会获得所有权 iter_mut方法:迭代可变的引用
消耗/产生迭代器
消耗迭代器的方法:
调用next方法的叫做“消耗型适配器”,因为调用它会把迭代器耗尽
例如:sum方法,取得迭代器的所有权,通过反复调用next,遍历所有元素,每次迭代,把当前元素添加到一个总和里,迭代结束,返回总和
1 2 3 4 5 6 7 8 9 10 #[cfg(test)] mod tests { #[test] fn iterator_sum () { let v1 = vec! [1 , 2 , 3 ]; let v1_iter = v1.iter (); let total : i32 = v1_iter.sum (); assert_eq! (total,6 ); } }
产生其他迭代器的方法: 定义在iterator
trait上的另外一些方法叫做”迭代器适配器“:把迭代器转换为不同种类的迭代器
可以通过链式调用使用多个迭代器适配器来执行复杂的操作,且可读性较高
例如:map,接收一个闭包,闭包作用于每个元素
1 2 3 4 5 6 7 8 9 #[cfg(test)] mod tests { #[test] fn iterator_sum () { let v1 = vec! [1 , 2 , 3 ]; let v2 : Vec <i32 > = v1.iter ().map (|x|{x+1 }).collect (); assert_eq! (v2, vec! [2 ,3 ,4 ]); } }
map方法是产生,接收一个闭包
collect方法:消耗性适配器,把结果收集到一个集合类型中,返回时Vec类型
使用闭包捕获环境
filter方法: 接收一个闭包
这个闭包在遍历迭代器的每个元素时,返回bool类型
如果闭包返回true,当前元素将会包含在filter产生的迭代器中
如果闭包返回false,当前元素将不会包含在filter产生的迭代器中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #[derive(PartialEq, Debug)] struct Shoe { size: u32 , style: String , }fn shoe_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 shoes = vec! [ Shoe { size: 10 , style: String ::from ("sneaker" ), }, Shoe { size: 13 , style: String ::from ("sandal" ), }, Shoe { size: 10 , style: String ::from ("boot" ), } ]; let in_my_size = shoe_in_my_size (shoes, 10 ); assert_eq! ( in_my_size, vec! [ Shoe { size: 10 , style: String ::from ("sneaker" ), }, Shoe { size: 10 , style: String ::from ("boot" ), } ] ); }
创建自定义迭代器
实现next方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 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 } } }#[test] fn call_next_directly () { let mut counter = Counter::new (); assert_eq! (counter.next (), Some (1 )); assert_eq! (counter.next (), Some (2 )); assert_eq! (counter.next (), Some (3 )); assert_eq! (counter.next (), Some (4 )); assert_eq! (counter.next (), Some (5 )); assert_eq! (counter.next (), None ); }fn using_other_iterator_trait_methods () { let sum : u32 = Counter::new () .zip (Counter::new ().skip (1 )) .map (|(a,b)| a*b) .filter (|x| x%3 == 0 ) .sum (); assert_eq! (18 , sum); }
循环和迭代器
迭代器更快
零开销抽象:使用抽象时不会引入额外的运行时开销
放心多多使用迭代器
迭代器方法
join
作用:将一个可迭代对象的元素连接成一个字符串的方法,它接受一个分隔符作为参数,并将可迭代对象中的每个元素以该分隔符连接起来,最终返回一个包含所有元素的字符串
函数签名:
1 2 3 fn join <Separator>(&self , sep: Separator) -> String where Separator: Display + Clone ,
示例:
1 2 3 4 5 fn main () { let words = vec! ["hello" , "world" , "rust" ]; let result = words.join (", " ); println! ("{}" , result); }
product
计算从1到num之间所有整数的成绩,并将结果存储在变量v中
1 2 3 4 fn main () { let num = 5 ; let v = (1 ..=num).product (); }
智能指针
简介
指针:一个变量在内存中包含的是一个地址(指向其他数据)
rust中最常见的指针就是引用&
智能指针: 行为和指针相似 有额外的元数据和功能
引用计数智能指针类型:
1、通过记录所有者的数量,使一份数据被多个所有者同时持有
2、并在没有任何所有者时自动清理数据
智能指针的例子: 1、String和Vec
2、都拥有一片内存区域,且允许用户对其操作 3、还拥有元数据
4、提供额外的功能或保障(String保障其数据是合法的UTD-8编码)
智能指针的实现:
1、通常使用struct实现,并实现了Deref和Drop这两个trait 2、Deref
trait:允许智能指针struct的实例项引用一样使用 3、Drop
trait:允许你自定义当智能指针实例走出作用域时的代码
标准库中常见的智能指针: Box:在heap内存上分配值
Rc:启用多重所有权的引用计数类型
Ret和RefMut:通过RefCell访问;在运行时而不是编译时强制借用规则的类型
内部可变模式: 不可变类型暴露出可修改其内部值的API
引用循环: 它们如何泄露内存,以及如何防止其发生
Box
Rc
refcell
同一数据的所有者
一个
多个
一个
可变性、借用检查
可变、不可变借用
不可变借用
可变、不可变借用
Box和Rc都在编译时检查,RefCell在运行时检查
Box
最简单的智能指针: 1、允许在heap上存储数据
2、stack上是指向heap数据的指针 3、没有性能开销和没有其他额外的功能
常用场景:
1、在编译时,某类型的大小无法确定。但使用该类型时,上下文却需要知道它的确切大小
2、当你有大量数据,想移交所有权,但需要确保在操作时数据不会被复制
3、使用某个值时,只关心它是否实现了特定的trait,而不关心它的具体类型
存储数据的简单例子:
1 2 3 4 fn main () { let b = Box ::new (5 ); println! ("b = {}" , b); }
使用Box赋能递归类型: 1、在编译时,rust需要知道一个类型所占空间的大小
2、而递归类型的大小无法在编译时确定 3、当Box类型的大小确定
关于Cons List: 1、来自Lisp语言的一种数据结构
2、两个元素:当前项的值,下一个元素
3、最后一个成员只包含Nil值,没有下一个元素
Cons List并不是rust的常用集合,而是Vec
使用Box来获得确定大小的递归类型:
Box是一个指针,rust知道它需要多少空间,因为指针的大小不会基于它指向的数据的大小变化而变化(间接存储)
1 2 3 4 5 6 7 8 9 10 11 12 13 use crate::List::{Cons, Nil};enum List { Cons (i32 , Box <List>), Nil, }fn main () { let list = Cons (1 , Box ::new (Cons (2 , Box ::new (Cons (3 , Box ::new (Nil)))))); }
Box解引用:
1 2 3 4 5 6 7 fn main () { let x = 5 ; let y = Box ::new (x); assert_eq! (5 ,x); assert_ne! (5 ,*y); }
Deref Trait
1、实现Deref Trait使我们可以自定义解引用运算符*的行为
2、通过实现Deref,智能指针可像常规引用一样来处理
定义自己的智能指针:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 (x); assert_eq! (5 ,*y); }
函数和方法的隐式解引用转化:
1、隐式解引用转换是为函数和方法提供的一种便捷特性 2、假设T实现了Deref
trait:Deref Coercion 可以把T的引用转化为T经过Deref操作后生成的引用
3、当某类型的引用传递给函数或方法时,但它的类型与定义的参数类型不匹配:
Deref Coercion就会自动发生
编译器会对deref进行一系列调用,来吧它转为所需的参数类型,且在编译时完成,没有额外开销
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 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 m = MyBox::new (String ::from ("Rust" )); hello (&m); }fn hello (name: &str ) { println! ("Hello, {}" ,name); }
解引用与可变性: 1、可使用DerefMut trait重载可变引用的*运算符
2、在类型和triat在下列三种情况发生时,rust会执行deref coercion: 当T:
Deref<Target=U>,允许&T转换为&U 当T:
DerefMut<Target=U>,允许&mut T转换为&mut U 当T:
Deref<Target=U>,允许&mut T转换为&U
Drop Trait
实现Drop Trait,可以让我们自定义当值将要离开作用域时发生的动作:
例如:文件、网络资源释放等 任何类型都可以实现Drop Trait
Drop Trait只要求你实现drop方法: 参数:对self的可变引用
Drop trait在预导入模块中,不用引入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 struct CustomSmartPointer { data: String , }impl Drop for CustomSmartPointer { fn drop (&mut self ) { println! ("Dropping CustomSmartPointer with data: {}" , self .data); } }fn main () { let c = CustomSmartPointer { data: String ::from ("My stuff" ), }; let d = CustomSmartPointer { data: String ::from ("other stuff" ), }; println! ("CustomSmartPointers created." ); }
可以使用std::mem::drop来提前drop值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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! ("CustomSmartPointers created." ); }
Rc引用计数智能指针
只有当数据为零引用时,这个智能指针才会被清理
使用场景:
1、需要在heap上分配数据,这些数据被程序的多个部分读取(只读),但在编译时无法确定哪个部分使用完这些数据
2、Rc智能用于单线程场景
Rc不在预导入模块 Rc::clone(&a)函数:增加引用计数
Rc::strong_count(&a):获得引用计数(强引用),还有Rc::weak_count函数
例子: 两个List共享另一个List 的所有权:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 enum List { Cons (i32 , Rc<List>), Nil, }use crate::List::{Cons, Nil};use std::rc::Rc;fn main () { let a = Rc::new (Cons (5 , Rc::new (Cons (10 , Rc::new (Nil))))); println! ("count after creating a = {}" , Rc::strong_count (&a)); let b = Cons (3 , Rc::clone (&a)); println! ("count after creating b = {}" , Rc::strong_count (&a)); { let c = Cons (4 , Rc::clone (&a)); println! ("count after creating c = {}" , Rc::strong_count (&a)); } println! ("count after c gose out of scope = {}" , Rc::strong_count (&a)); }
Rc通过不可变 引用,使你可以在程序不同部分之间共享只读数据
RefCell和内部可变性
内部可变性: 1、是rust的设计模式之一
2、它允许你在只持有不可变引用的前提下对数据进行修改:
数据结构中使用了unsafe代码来绕过rust正常的可变性和借用规则
与Rc不同,RefCell类型代表了其持有数据的唯一所有权,但二者都只能适用于单线程场景
Box和RefCell的区别:
1、Box在编译阶段强制代码遵守借用规则,否则出现错误
2、RefCell只在运行时检查借用规则,否则触发panic
内部可变性:可变的借用一个不可变的值
没看懂,后面有需要再看: BV1hp4y1k7SV 的P91
循环引用导致内存泄漏
使用Rc和RefCell接可能创造出循环引用,从而发生内存泄漏
没看懂,后面有需要再看: BV1hp4y1k7SV 的P92
无畏并发
简介
concurrent:程序的不同部分之间独立地执行(并发)
parallel:程序的不同部分同时执行(并行)
本章统称为并发
rust无畏并发:允许编写没有细微bug的代码,并再不引入新bug的情况下易于重构
多线程运行代码
多线程导致的问题: 竞争状态:线程以不一致的顺序访问数据或资源
死锁:两个线程彼此等待对方使用完所持有的资源,线程无法继续
只有在某些情况下发生的bug,很难可靠地复制现象和修复
实现线程的方式: 1、通过调用OS的API来创建线程:1:1模型:
需要较小的运行时 2、语言自己实现的线程(绿色线程):M:N模型:
需要更大的运行时
rust标准库只提供1:1模型
通过thread::spawn函数可以创建新线程:
参数:一个闭包(在新线程里运行的代码)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 use std::thread;use std::time::Duration;fn main () { thread::spawn (|| { for i in 1 ..10 { println! ("number {} from the spawned thread!" , i); thread::sleep (Duration::from_secs (2 )); } }); for i in 1 ..5 { println! ("number {} from the main thread!" , i); thread::sleep (Duration::from_secs (2 )); } }
通过join Handle等待所有线程的完成:
1、thread::spawn函数的返回值类型是JoinHandle
2、JoinHandle持有值得所有权:调用其join方法,可以等待对应的其他线程的完成
3、join方法:调用handle的join方法会阻止当前运行线程的执行,直到handle所表示的这些线程终结
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 use std::thread;use std::time::Duration;fn main () { let handle = thread::spawn (|| { for i in 1 ..10 { println! ("number {} from the spawned thread!" , i); thread::sleep (Duration::from_secs (2 )); } }); for i in 1 ..5 { println! ("number {} from the main thread!" , i); thread::sleep (Duration::from_secs (2 )); } handle.join ().unwrap (); }
使用move闭包:
1、move闭包通常和thread::spawn函数一起使用,它允许使用其他线程的数据
2、创建线程时,把值的所有权从一个线程转移到另一个线程
1 2 3 4 5 6 7 8 9 10 use std::thread;fn main () { let v = vec! [1 , 2 , 3 ]; let handle = thread::spawn (move || { println! ("Here's a vector: {:?}" ,v); }); handle.join ().unwrap (); }
消息传递跨线程传递数据
Go语言的名言:不要用共享内存来通信,要用通信来共享内存
流行且能保证安全并发的技术:消息传递
线程通过彼此发送信息来进行通信
Channel: 1、发送端、接收端
2、如果两端中有一端被关闭,那Channel就被关闭
使用mpsc::channel函数创建Channel: mpsc表示multiple producer,single
consumer(多个生产者,一个消费者)
返回一个tuple,里面的元素分别是发送端、接收端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 use std::sync::mpsc;use std::thread;fn main () { let (tx, rx) = mpsc::channel (); thread::spawn (move || { let val = String ::from ("hi" ); tx.send (val).unwrap (); }); let received = rx.recv ().unwrap (); println! ("Got: {}" , received); }
发送端send方法: 参数:想要发送的数据
返回:Result<T,E>,如果有问题,返回一个错误
接收端方法: 1、recv方法:阻止当前线程执行,直到Channel中有值被送来
一旦有值收到,就返回Result<T, E> 当发送端关闭,就会收到一个错误
2、try_recv方法,不会阻塞: 立即返回Result<T, E>
通常会使用循环调用检查try_recv的结果
当使用Channel发送数据值,该数据的所有权已经发送出去了
发送多个值,看到接受者在等待:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 use std::collections::btree_map::Values;use std::sync::mpsc;use std::thread;use std::time::Duration;fn main () { let (tx,rx) = mpsc::channel (); thread::spawn (move || { let vals = vec! [ String ::from ("hi" ), String ::from ("from" ), String ::from ("the" ), String ::from ("thread" ), ]; for val in vals { tx.send (val).unwrap (); thread::sleep (Duration::from_secs (1 )); } }); for received in rx { println! ("Got: {}" , received); } }
通过克隆创建多个发布者:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 use std::collections::btree_map::Values;use std::sync::mpsc;use std::thread;use std::time::Duration;fn main () { let (tx,rx) = mpsc::channel (); let tx1 = mpsc::Sender::clone (&tx); thread::spawn (move || { let vals = vec! [ String ::from ("1: hi" ), String ::from ("1: from" ), String ::from ("1: the" ), String ::from ("1: thread" ), ]; for val in vals { tx1.send (val).unwrap (); thread::sleep (Duration::from_secs (1 )); } }); thread::spawn (move || { let vals = vec! [ String ::from ("hi" ), String ::from ("from" ), String ::from ("the" ), String ::from ("thread" ), ]; for val in vals { tx.send (val).unwrap (); thread::sleep (Duration::from_secs (1 )); } }); for received in rx { println! ("Got: {}" , received); } }
共享状态的并发
Channel类似单所有权:一旦将值的所有权转移至Channel,就无法使用它
共享内存并发类似多所有权:多个线程可以同时访问同一块内存
使用Mutex来每次只允许一个线程访问数据 1、Mutex是mutual
exclusion(互斥锁)的简写
2、在同一时刻,Mutex只允许一个线程来访问某些数据 3、想要访问数据:
线程必须首先获取互斥锁,lock数据结构是mutex的一部分,它能跟踪谁对数据有用独占访问权
mutex通常被描述为:通过锁定系统来保护它所持有的数据
mutex的两条规则: 1、在使用数据之前,必须尝试获取锁
2、使用完mutex所保护的数据,必须对数据进行解锁,以便其他线程可以获取锁
Mutex的API: 1、通过Mutex::new来创建(智能指针)
2、访问数据前,通过lock方法来获取锁 会阻塞当前线程 lock可能会失败
返回的是MutexGuard(智能指针)
1 2 3 4 5 6 7 8 9 10 11 12 13 use std::sync::Mutex;fn main () { let m = Mutex::new (5 ); { let mut num = m.lock ().unwrap (); *num = 6 ; } print! ("m = {:?}" , m); }
多线程多重所有权: (错误的代码)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 use std::sync::Mutex;use std::thread;fn main () { let counter = Mutex::new (0 ); let mut handles = vec! []; for _ in 0 ..10 { let handle = thread::spawn (move || { let mut num = counter.lock ().unwrap (); *num += 1 ; }); handles.push (handle); } for handle in handles { handle.join ().unwrap (); } println! ("Counter: {}" , *counter.lock ().unwrap ()); }
多线程的多重所有权: Arc和Rc类似,但是它可以用于并发情景
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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 (); } let temp = *counter.lock ().unwrap (); println! ("Result: {}" , temp); }
也就是说,要共享的数据,就用Mutex创建就可以了,然后再多个线程锁住它、使用它、解锁它
Send 和 Sync trait
Send:允许线程间转移所有权 只有Rc没有实现Send
Sync:允许从多线程访问 实现Sync类型可以安全的被多个线程引用
Mutex实现了Sync
手动来实现Send和Sync是不安全的
模式匹配
简介
1、模式是rust中一种特殊语法,用于匹配复杂和简单类型的结构
2、将模式与匹配表达式和其他构造结合使用,可以更地控制程序的控制流
3、模式将由以下元素(的一些组合)组成:字面值、结构的数组、enum、struct和tuple、变量、通配符、占位符
4、想要使用模式,需要将其与某个值进行比较:如果模式匹配,就可以在代码中使用这个值的相应部分
1 2 3 4 5 6 fn main () { let v = vec! ['a' ,'b' ,'c' ]; for (index, value) in v.iter ().enumerate () { println! ("{} is at index {}" ,value,index); } }
1 2 3 4 fn main () { let a = 5 ; let (x,y,z) = (1 ,2 ,3 ); }
模式的两种形式
不可辨驳的:能匹配任何可能传递的模式 例如:let x = 5;
可辩驳的:对某些可能的值,无法进行匹配的模式 例如:if let Some(x) =
a_value;
语法
或:符号“|”
表示范围:..=
加条件:if关键字
高级特性
unsafe rust
rust隐藏着第二个语言,它没有强制内存安全保证:Unsafe
rust和普通的rust一样,但是提供了额外的“超能力”
存在的原因: 1、静态分析是保守的:使用unsafe
rust:我知道自己在做什么,并承担相应风险
2、计算机硬件本身就是不安全的,rust需要能够进行底层系统编程
使用unsafe关键字来切换到unsafe
rust,开启一个块,里面放着unsafe代码
unsafe rust可执行的四个动作: 1、解引用原指针 2、调用unsafe函数或方法
3、访问或修改可变的静态变量 4、实现unsafe trait
注意: 1、unsafe并没有关闭借用检查或停用其他安全检查
2、任何内存安全相关的错误必须留在unsafe块里
3、尽可能隔离unsafe代码,最好将其封装在安全的抽象里,提供安全的API
原始指针: 可变的:\(*\) mut T
不可变的:\(*const\)
T。意味着解引用后不能直接对其进行赋值
与引用不同,原始指针:
1、允许通过同时具有不可变和可变指针或多个指向同一位置的可变指针来忽略借用规则
2、无法保证指向合理的内存 3、允许为null 4、不实现任何自动清理
5、放弃保证的安全,换取更好的性能/与其他语言或硬件接口的能力
1 2 3 4 5 6 7 8 9 10 11 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); } }
为什么要使用原始指针: 1、与C语言进行接口
2、构建借用检查器无法理解的安全抽象
unsafe函数或方法:在定义前加上了unsafe关键字:
1、调用前需要手动满足一些条件(主要靠看文档),因为rust无法对这些条件进行验证
2、需要在unsafe块里进行调用
1 2 3 4 5 6 7 unsafe fn dangerous () {}fn main () { unsafe { dangerous (); } }
创建unsafe代码的安全抽象:
1、函数包含unsafe代码并不意味着需要将整个函数标记为unsafe
2、将unsafe代码包裹在安全函数中是一个常见的抽象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 use std::slice;fn split_at_mut (slice: &mut [i32 ], mid: usize ) -> (&mut [i32 ], &mut [i32 ]) { let len = slice.len (); let ptr = slice.as_mut_ptr (); assert! (mid <= len); unsafe { (slice::from_raw_parts_mut (ptr, mid), slice::from_raw_parts_mut (ptr.offset (mid as isize ), len - mid)) } }fn main () { let mut v = vec! [1 , 2 , 3 , 4 , 5 , 6 ]; let (a, b) = split_at_mut (&mut v, 3 ); assert_eq! (a, &mut [1 , 2 , 3 ]); assert_eq! (b, &mut [4 , 5 , 6 ]); }
使用extern函数调用外部代码:
1、extern关键字:简化创建和使用外部函数接口的过程
2、外部函数接口:它允许一种编程语言定义函数,并让其他编程语言能调用这些函数
1 2 3 4 5 6 7 8 9 extern "C" { fn abs (input: i32 ) -> i32 ; }fn main () { unsafe { println! ("Absolute value of -3 according to C: {}" , abs (-3 )); } }
从其他语言调用rust函数:
1、可以使用extern创建接口,其他语言通过它们调用rust函数
2、在fn前添加extern关键字,并指定ABI
3、还需添加#[no_mangle]注解:避免rust在编译时改变它的名称
1 2 3 4 5 6 7 #[no_mangle] pub extern "C" fn call_from_c () { println! ("Just called a Rust function from C" ); }fn main () {}
访问或修改一个可变静态变量:
1、rust支持全局变量,但因为所有权机制可能产生某些问题,例如数据竞争
2、在rust里,全局变量叫做静态变量
1 2 3 4 5 6 7 static HELLO_WORLD: &str = "Hello, World!" ;fn main () { println! ("{}" , HELLO_WORLD); }
常量:允许使用它们的时候对数据进行复制 静态变量:
1、有固定的内存地址,使用它的值总会访问同样的数据
2、可以是可变的,访问和修改静态可变变量是不安全的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 static mut COUNTER: u32 = 0 ;fn add_to_count (inc: u32 ) { unsafe { COUNTER += inc; } }fn main () { add_to_count (3 ); unsafe { print! ("COUNTER: {}" , COUNTER); } }
实现不安全trait:
1、当某个trait中存在至少一个方法拥有编译器无法校验的不安全因素时,就称这个trait是不安全的
2、声明unsafe trait:在定义前加unsafe
1 2 3 4 5 6 7 8 9 unsafe trait Foo { }unsafe impl Foo for i32 { }fn main () {}
高级trait
在trait定义中使用关联类型来指定占位类型:
关联类型是trait中的类型占位符,它可以用于trait的方法签名中:
可以定义出包含某些类型的trait,而在实现前无需知道这些类型是什么
1 2 3 4 5 6 7 pub trait Iterator { type Item ; fn next (&mut self ) -> Option <Self ::Item>; }fn main () {}
关联类型与泛型的区别:
泛型
关联类型
每次实现triat时标注类型
无需标注类型
可以为一个类型多次实现某个trait
无法为单个类型多次实现某个trait
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 use std::path::Iter;pub trait Iterator { type Item ; fn next (&mut self ) -> Option <Self ::Item>; }pub trait Iterator2 <T> { fn next (&mut self ) -> Option <T>; }struct Counter {}impl Iterator for Counter { type Item = u32 ; fn next (&mut self ) -> Option <u32 > { None } }impl Iterator2 <u32 > for Counter { fn next (&mut self ) -> Option <u32 > { None } }impl Iterator2 <String > for Counter { fn next (&mut self ) -> Option <String > { None } }fn main () { }
默认泛型参数和运算符重载:
1、可以在使用泛型参数时为泛型指定一个默认的具体类型
2、语法:<PlaceholderType=ConcreteType>
3、这种技术常用于运算符重载
4、rust不允许创建自己的运算符及重载任意的运算符
5、但可以通过实现std::ops中列出的那些trait来重载一部分相应的预算符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 () { assert_eq! (Point { x: 1 , y: 0 } + Point { x: 2 , y: 3 }, Point { x: 3 , y: 3 }); }
指定默认类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 use std::ops::Add;struct Millimeter (u32 );struct Meters (u32 );impl Add <Meters> for Millimeter { type Output = Millimeter; fn add (self , other: Meters) -> Millimeter { Millimeter (self .0 + (other.0 * 1000 )) } } fn main () {}
调用同名方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 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!" ); } }impl Human { fn fly (&self ){ println! ("*waving arms furiously*" ); } }fn main () { let person = Human; person.fly (); Pilot::fly (&person); Wizard::fly (&person); Human::fly (&person); }
方法参数没有传入自身类型,使用完全限定语法:
1、可以在任何调用函数或方法的地方使用
2、允许忽略那些从其他上下文能推导出来的部分
3、当rust无法区分你期望调用哪个具体实现的时候,才需要使用这种语法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 () { println! ("Dog: {}" , Dog::baby_name ()); println! ("Animal: {}" , <Dog as Animal>::baby_name ()); }
完全限定语法:::function()
使用supertrait来要求trait附带其他trait的功能:
需要在一个trait中使用其他trait的功能: 1、需要被依赖的triat也被实现
2、那个被间接依赖的trait就是当前trait的supertrait
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 use std::fmt;trait OutlinePrint : fmt::Display { fn outline_print (&self ) { let output = self .to_string (); let len = output.chars ().count (); 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 fmt ::Display for Point { fn fmt (&self , f: &mut fmt::Formatter<'_ >) -> fmt::Result { write! (f, "({}, {})" , self .x, self .y) } } impl OutlinePrint for Point {}fn main () {}
使用newtype模式在外部类型上实现外部trait:
1、孤儿规则:只有当trait或类型定义在本地包时,才能为该类型实现这个trait
2、可以通过newtype模式来绕过这一规则: 利用tuple
struct创建一个新的类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 use std::fmt::{self , write};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模式可以:
1、用来静态地保证各种值之间不会混淆并表明值的单位
2、为类型的某些细节提供抽象能力
3、通过轻量级的封装来隐藏内部实现细节
使用类型别名创建类型同义词: 1、rust提供了类型别名的功能:
为现有类型生产另外的名称 并不是一个独立的类型 使用type关键字
2、主要用途:较少代码字符重复输入
1 2 3 4 5 6 7 8 type Kilometers = i32 ;fn main () { let x : i32 = 5 ; let y : Kilometers = 5 ; println! ("x = {}, y = {}" , x, y); }
1 2 3 4 5 type Thunk = Box <dyn Fn () + Send + 'static >;fn takes_long_type (f: Thunk) { }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 use std::io::Error;use std::fmt;type Result <T> = std::io::Result <T>;pub trait Write { fn Write (&self , buf: &[u8 ]) -> Result <usize >; fn flush (&self ) -> Result <()>; fn write_all (&self , buf: &[u8 ]) -> Result <()>; fn write_fmt (&self , fmt: fmt::Arguments) -> Result <()>; }fn main () {}
Never类型: 1、有一个名为!的特殊类型: 它没有任何值,空类型
也称Never类型,因为它在不返回的函数中充当返回类型
2、不返回值的函数也称作发散函数
(Never类型可以是continue,可以是panic)
1 2 3 4 5 6 7 8 9 10 fn main () { let guess = "" ; loop { let guess : u32 = match guess.trim ().parse () { Ok (num) => num, Err (_) => continue , }; } }
Sized trait: 为了处理动态大小的类型:rust提供了一个Sized
triat来确定一个类型的大小在编译时是否已知:
1、编译时可计算出大小的类型会自动实现这个trait
2、rust还会为每一个泛型函数隐式地添加Sized约束
1 2 fn generic <T>(t: T) {}fn generic <T: Sized >(t: T) {}
默认情况下,泛型函数只能用于编译时已经知道大小的类型,可以通过特殊语法解除这一限制
即?Sized trait约束:
1 2 3 fn generic <T>(t: T) {}fn generic <T: Sized >(t: T) {}fn generic <T: ?Sized >(t: &T) {}
高级函数和闭包
函数指针: 1、可以将函数传递给其他函数
2、函数在传递过程中会被强制转换成fn类型 3、fn类型就是“函数指针”
1 2 3 4 5 6 7 8 9 10 11 12 13 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 answer = do_twice (add_one, 5 ); println! ("{}" ,answer); }
函数指针与闭包的不同:
1、fn是一个类型,不是一个trait:可以直接指定fn为参数类型,不用声明一个以Fn
trait为约束的泛型参数 2、函数指针实现了全部3中闭包trait:
总是可以把函数指针作为参数传递给一个接受闭包的函数
所以,倾向于搭配闭包trait的泛型来编写函数,可以同时接收闭包和普通函数
3、某些情景,只想接收fn而不接收闭包:
与外部不支持闭包的代码交互:C函数
1 2 3 4 5 6 7 fn main () { let list_of_numbers = vec! [1 ,2 ,3 ]; let list_of_strings : Vec <String > = list_of_numbers.iter ().map (|i| i.to_string ()).collect (); let list_of_numbers = vec! [1 ,2 ,3 ]; let list_of_strings : Vec <String > = list_of_numbers.iter ().map (ToString ::to_string).collect (); }
1 2 3 4 5 6 7 8 9 10 11 12 fn main () { enum Status { Value (u32 ), Stop, } let v = Status::Value (3 ); let list_of_statuses :Vec <Status> = (0u32 ..20 ) .map (Status::Value) .collect (); }
返回闭包:
闭包使用trait进行表达,无法在函数中直接返回一个闭包,可以将一个实现了该trait的具体类型作为返回值
1 2 3 4 5 6 7 fn returns_closure () -> Box <dyn Fn (i32 ) -> i32 > { Box ::new (|x| x + 1 ) }fn main () {}
类型转换
as
1 2 3 4 fn average (values: &[f64 ]) -> f64 { let total = values.iter ().sum::<f64 >(); (total / values.len () as f64 ) as f64 }
as_mut
1 2 3 4 5 6 7 fn num_sq <T: AsMut <u32 >>(arg: &mut T) -> u32 { let num = *arg.as_mut (); let squared = num * num; *arg.as_mut () = squared; squared }
as_bytes()
将字符串转换为一个字符数组(&[u8])
1 2 3 4 fn main () { let str = "Hello" ; let str_bytes = str .as_bytes (); }
as_ptr()
将目标转换为指针
例子:将C字符串转换为*const c_char
1 2 3 4 5 6 7 extern crate libc;use std::ffi:CString;use libc::c_char;fn main () { let a = CString::new ("Hello" ); let b :*const c_char = a.as_ptr (); }
as_mut_ptr()
将目标转换为可变指针
文件系统
结构体
PathBuf
用于操作路径的结构体,它提供了方便的方法来构建、操作和转换文件系统路径
定义:
1 2 3 4 5 6 pub struct PathBuf { inner: OsString, }pub struct OsString { inner: Buf, }
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 use std::path::PathBuf;fn main () { let mut path = PathBuf::new (); path.push ("dir" ); path.push ("file.txt" ); println! ("Path: {}" , path.display ()); path.set_extension ("dat" ); println! ("Path: {}" , path.display ()); if let Some (parent) = path.parent () { println! ("Parent directory: {}" , parent.display ()); } else { println! ("No parent directory found" ); } let path_str = path.to_str ().unwrap (); println! ("Path as string: {}" ,path_str); }
Path
定义:
1 2 3 4 5 6 pub struct Path { inner: OsStr, }pub struct OsStr { inner: Slice, }
创建:
1 let path = Path::new ("hello.txt" );
输入输出
一些重命名
1 pub type Result <T> = result::Result <T, Error>;
优化相关
编译器相关
core::intrinsics::unlikely
是一个用于指示编译器优化的内置函数(intrinsc)。它用于标记条件分支的分支预测,提示编译器某个分支的执行效率较低,以便编译器在生成机器代码时进行优化,使得执行效率高的分支更可能处于CPU流水线的“预取阶段”
当某一个执行效率明显高于另一个时,使用unlikely可以帮助编译器生成更优化的代码
1 2 3 4 5 if unsafe { core::intrinsics::unlikely (condition) } { } else { }
这是一个unsafe函数。
属性
debug
为结构体自动生成dubug
trait的实现,可以使得结构体使用"println!("{:?}",
locked_inode)等宏来打印调试信息
内存管理
弱引用
定义:弱引用不会阻止被引用对象被回收,如果被引用对象只有弱引用指向它,则可以自由地释放这个对象,即使还有弱引用指向它,因为弱引用==不会增加引用计数==。
功能:解决循环引用导致的内存泄漏问题,由于两个对象互相引用造成了循环引用,无法判断对象是否可以被释放
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 use std::sync::{Arc, Weak};use std::cell::RefCell;struct Node { next: RefCell<Option <Weak<Node>>>>, }fn main () { let node1 = Arc::new (Node { next: RefCell::new (Nonw), }); let node2 = Arc::new (Node { next: RefCell::new (None ), }); *node1.next.borrow_mut () = Some (Arc::downgrade (&node2)); *node2.next.borrow_mut () = Some (Arc::downgrade (&node1)); }
注意事项:
确保不会在没有强引用的情况下使用弱引用指向的对象,否则可能会出现空指针错误
原子引用计数
功能:用于在多线程环境中共享数据的所有权,提供一种安全地在多线程之间共享数据所有权的方式
引用计数:当Arc的引用计数为0时,它会释放内部数据并销毁自身。
内部可变性:与Rc类似,Arc不能提供内部可变性,如果需要在多线程环境下修改数据,可以使用Mutex、RwLock等同步原语结合Arc使用
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 use std::sycn::Arc;use std::thread;fn main () { let data = Arc::new (vec! [1 ,2 ,3 ]); let mut handles = vec! []; for i in 0 ..3 { let data_clone = Arc::clone (&data); let handle = thread::spawn (move || { println! ("Thread {}: {:?}" ,i,data_clone); }) handles.push (handle); } for handle in handles { handle.join ().unwrap (); } }