2019 Rust学习
Contents
准备工作
rustup
rustup 用于安装 Rust , 并可管理 rust 不同的版本(stable, beta, nightly) 并更新它们.
rust 被安装在 $HOME/.cargo/bin
目录下.
cargo install
安装的程序和插件, 同样也在上面的目录中.
安装 nightly 版本
rustup toolchain install nightly
# 只运行
rustup run nightly rustc --version
# 切换默认为 nightly
rustup default nightly
安装
curl https://sh.rustup.rs -sSf | sh
#在当前窗口加载环境配置
source $HOME/.cargo/env
本地文档
rustup doc
删除
rustup self uninstall
自动补全
# bash
rustup completions bash > $(brew --prefix)/etc/bash_completion.d/rustup.bash-completion
# zsh
rustup completions zsh > ~/.zfunc/_rustup
在 ~/.zshrc 文件中, 在 compinit 之前添加以下语句
fpath+=~/.zfunc
更新
rustup update
rustup self update
安装其他版本
rustup 安装 rust 时, 默认是 stable 版本
# 安装 nightly 版本
rustup toolchain install nightly
# 运行 nightly 版本编译器
rustup run nightly rustc --version
# 切换默认版本为 nightly
rustup default nightly
替换 crate 源
vim ~/.zshrc
export RUSTUP_DIST_SERVER="https://mirrors.ustc.edu.cn/rust-static"
export RUSTUP_UPDATE_ROOT="https://mirrors.ustc.edu.cn/rust-static/rustup"
vim $HOME/.cargo/config
添加以下内容
[source.crates-io]
registry = "https://github.com/rust-lang/crates.io-index"
replace-with = 'ustc'
[source.ustc]
registry = "git://mirrors.ustc.edu.cn/crates.io-index"
cargo 常用命令
cargo new project-name
cargo run
cargo build --release
# 升级依赖. 成功后, 会更新 Cargo.lock 文件
cargo update
# 为你的项目及依赖生成文档
cargo doc --open
Cargo.lock 文件是你第一次执行
cargo build
后输出的文件.你的项目在其他人里编译时, 如果 cargo 发现有 Cargo.lock 文件, 则以该文件里的一切配置及版本为准来编译项目. 这方便重现你的项目.
Rust 项目的命名风格
- 小写字母 + 下划线
项目结构
在同一项目中编写自己的模块时, 可以这样子使用
hello.rs
:
pub fn say_hello(hello: &str) {
println!("\nhelo {}", hello);
}
main.rs
:
mod hello;
use hello::say_hello;
fn main() {
say_hello(" world");
}
src
├── hello.rs
└── main.rs
可见性
- 如果不显示使用pub声明,则函数或模块的可见性默认为私有的。
pub
,可以对外暴露公共接口,隐藏内部实现细节。pub(crate)
,对整个crate可见。pub(in Path)
,其中Path是模块路径,表示可以通过此Path路径来限定可见范围。pub(self)
,等价于pub(in self),表示只限当前模块可见。pub(super)
,等价于pub(in super),表示在当前模块和父模块中可见。
变量与绑定
Rust 中的表达式一般可以分为位置表达式( Place Expression
)和值表达式( ValueExpression
)。在其他语言中,一般叫作左值(LValue
)和右值(RValue
)
求值上下文
求值上下文也分为位置上下文(PlaceContext
)和值上下文(Value Context
)。下面几种表达式属于位置上下文
- 赋值或者复合赋值语句左侧的操作数。
- 一元引用表达式的独立操作数。
- 包含隐式借用(引用)的操作数。
- match判别式或let绑定右侧在使用ref模式匹配的时候也是位置上下文。
除了上述几种情况,其余表达式都属于值上下文. 值表达式不能出现在位置上下文中.
所有权与引用
当位置表达式
出现在值上下文
中时,该位置表达式将会把内存地址转移给另外一个位置表达式,这其实是所有权的转移.
在语义上,每个变量绑定实际上都拥有该存储单元的所有权,这种转移内存地址的行为就是所有权(OwnerShip
)的转移,在 Rust 中称为移动(Move
)语义,那种不转移的情况实际上是一种复制(Copy
)语义。Rust没有GC,所以完全依靠所有权来进行内存管理
变量的生命周期
从使用 let 声明创建变量绑定开始,到超出词法作用域的范围时结束
闭包
在一般情况下,闭包默认会按引用捕获变量。如果将此闭包返回,则引用也会跟着返回. 所以闭包最后通常会使用 move
来将所有权转移.
println!
宏
- nothing代表Display,比如
println!("{}",2)
- ?代表Debug,比如
println!("{:?}",2)
- o代表八进制,比如
println!("{:o}",2)
- x代表十六进制小写,比如
println!("{:x}",2)
- X代表十六进制大写,比如
println!("{:X}",2)
- p代表指针,比如
println!("{:p}",2)
- b代表二进制,比如
println!("{:b}",2)
- e代表指数小写,比如
println!("{:e}",2)
- E代表指数大写,比如
println!("{:E}",2)
Rust 概念
它是一门表达式语言, 一切皆表达式
默认情况下, 变量是不可变的.
immutable by default
- 要想使变量可变, 则要用
mut
来修饰.
- 要想使变量可变, 则要用
默认情况下, 引用也是不可变的.
references are immutable by default
- 要想使引用可变, 则要
&mut
来修饰
- 要想使引用可变, 则要
immutable
表示一旦绑定bind
就不可以再改变其值.Result
是枚举类型.Ok
或Err
.cargo
文件Cargo.toml
的版本写法rand = "0.3.14"
等同于rand = "^0.3.14"
表示任意兼容0.3.14
版本 API 的版本即可.crate
表示库trait
表示接口shadow
: 通常用于同一个变量名, 从一种类型切换到另一种类型.let x = 5; let x = x + 1;
shadow
与mut
的区别- 如果没有
let
然后重新声明同样名的变量会编译错误. 通过let
我们可以方便地从一个变量转变为immuable
let
可以重用原来的变量名, 但类型可以不同. 而mut
不可以.
- 如果没有
指定变量类型
let var_name: var_type
_
: 特殊变量, 表示所有.const
与immutable
的区别const
不允许用mut
修饰. 它是一直都不可变的.const
中, 类型必须显式指定const
可以在任何地方声明const
只允许const
表达式赋值, 而不是一个函数调用或其他在 runtime 时计算出来的值.
数据类型
scalar
表示一个单独的值. 有 4 种.
integer
length | signed | unsigned |
---|---|---|
8bit | i8 | u8 |
16bit | i16 | u16 |
32bit | i32 (默认,即使是 64 位平台) | u32 |
64bit | i64 | u64 |
128bit | i128 | u128 |
arch | isize | usize |
字面量
类型 | 示例 |
---|---|
十进制 | 98_222 |
十六进制 | 0xff |
八进制 | 0o77 |
二进制 | 0b1111_000 |
字节(仅限 u8 类型) | b’A’ |
溢出处理
Debug 模式: 会检测是否溢出. 然后产生 panic
退出.
Release 模式: 并不会检测溢出. 超出值范围的话, 会产生 two's complement wrapping
.
foating-point number
bit | type |
---|---|
32 | f32 |
64 | f64 (默认) |
bool
- 只有两个值
false
,true
- 大小为
1 byte
character
- 使用单引用. 双引用的话是 String 类型.
- 它是 4
bytes
大小.
compound : 这可以组成多个值为一个类型. 有两种
tule
- 可以由多种类型组合为一种.
- 它是固定长度. 一旦声明, 则不能改变大小.
- 声明:
let tup =(32, 6.4, 1)
. ` 要获取值 let (x, y, z) = tup;
: 这种称为解构destructuring
- 也可以通过下标直接访问
tup.0
,tup.1
. 第一个索引的下标为 0. - 元素不必是相同类型
array
- - 固定长度.
- 声明
let a = [1,2,3,4,5];
let a : [i32; 5] = [1,2,3,4,5];
let a = [3; 5]
. 表示元素全是 3, 一共 5 个.
String
在Rust中每个char类型的字符都代表一个有效的u32类型的整数.
Rust 中字符串是 UTF-8 编码序列。出于内存安全的考虑,在Rust中字符串分为以下几种类型
- str,表示固定长度的字符串
- String,表示可增长的字符串
- CStr,表示由C分配而被Rust借用的字符串,一般用于和C语言交互
- CString,表示由Rust分配且可以传递给C函数使用的C字符串,同样用于和C语言交互
- OsStr,表示和操作系统相关的字符串。这是为了兼容Windows系统
- OsString,表示OsStr的可变版本。与Rust字符串可以相互转换
- Path,表示路径,定义于
std::path
模块中。Path包装了OsStr - PathBuf,跟Path配对,是Path的可变版本。PathBuf包装了OsString
String 类型本质为一个成员变量是 Vec<u8>
类型的结构体,所以它是直接将字符内容存放于堆中的.
len
方法获取的是堆中字节序列的字节数,而非字符个数as_ptr
获取的是堆中字节序列的指针地址- 通过引用操作符
&
得到的地址为字符串变量在栈上指针的地址
Rust提供了bytes
和chars
两个方法来分别返回按字节和按字符迭代的迭代器。所以,在Rust中对字符串的操作大致分为两种方式:按字节处理和按字符处理
函数
- 风格
fn hello_world()
- 参数
fn hello_world(x:i32)
参数中的类型是必填的 - 函数体. 包含许多 statement 以及结尾可选的 expression.
statement
: 执行一些动作, 但没有返回值.- 比如函数声明
let
expression
: 它会给出一个值.- 调用函数
- 调用 macro
{}
定义作用域:let y = {let x = 3; x + 1}
- 注意
5+1
是 expression. 但5+1;
是 statement. expression
可以是statement
的一部分.
- 注意, Rust 是一门
expression-based
的语言. 这跟其他的有很大的不同. - 返回值
fn hello_world() -> i32 {}
- 函数体最后一个 expression 就是函数的返回值.
- 多值(通过 tuple).
fn hello_world() -> (i32, i32){}
- 注释:
// your comment
函数参数可以按值传递,也可以按引用传递。
当参数按值传递时,会转移所有权或者执行复制(Copy)语义。(按值传递的参数使用mut
关键字)
当参数按引用传递时,所有权不会发生变化,但是需要有生命周期参数。(按引用传递参数时的mut
用法: &mut
)
当符合生命周期参数省略规则时,编译器可以通过自动推断补齐函数参数的生命周期参数,否则,需要显式地为参数标明生命周期参数
控制流程
if expression
注意, 它是表达式来的.if bool-expression {} else if bool-expression2 {} else {}
- 用在
let
中 :let number = if condition {} else {};
- 这时, if 与 else 的
expression
结果, 都需要一致.
- 这时, if 与 else 的
- 循环
loop
- 无限循环.
loop {}
- 无限循环.
while
while condition {}
for
- 遍历数组
for e in arr.iter() {}
for number in (1..4).rev() {}
- 遍历数组
- 中断循环 :
break;
它是expression
.break x * 2;
表示中断, 并返回值x*2
- 继续循环 :
continue;
值语义和引用语义
为了更加精准地对这种情况进行描述,值语义(Value Semantic)和引用语义(ReferenceSemantic)被引入,定义如下。
- 值语义:按位复制以后,与原始对象无关。引用语义:也叫指针语义。一般是指将数据存储于堆内存中,通过栈内存的指针来管理堆内存的数据,并且引用语义禁止按位复制。按位复制就是指栈复制,也叫浅复制,它只复制栈上的数据。相对而言,深复制就是对栈上和堆上的数据一起复制
- 引用语义:也叫指针语义。一般是指将数据存储于堆内存中,通过栈内存的指针来管理堆内存的数据,并且引用语义禁止按位复制。
按位复制就是指栈复制
,也叫浅复制
,它只复制栈上的数据。相对而言,深复制
就是对栈上和堆上的数据一起复制
所有权机制
解引用操作会获得所有权
复制(Copy
)语义和移动(Move
)语义。
复制语义对应值语义,移动语义对应引用语义。
这样划分是因为引入了所有权机制,在所有权机制下同时保证内存安全和性能
枚举体enum
和结构体struct
是类似的,当成员均为复制语义类型时,不会自动实现Copy。
而对于元组类型(tuple
)来说,其本身实现了Copy,如果元素均为复制语义类型,则默认是按位复制的,否则会执行移动语义.
数组array
和Option类型与元组类型都遵循这样的规则:如果元素都是复制语义类型,也就是都实现了Copy,那么它们就可以按位复制,否则就转移所有权
字符串字面量,支持按位复制
闭包
闭包闭包会创建新的作用域,对于环境变量来说有以下三种捕获方式:
- 对于复制语义类型,以不可变引用(
&T
)来捕获。 - 对于移动语义类型,执行移动语义(
move
)转移所有权来捕获。 - 对于可变绑定,如果在闭包中包含对其进行修改的操作,则以可变引用(
&mut
)来捕获。
所有权借用
在所有权系统中,引用&x
也可称为x的借用(Borrowing
),通过&
操作符来完成所有权租借。既然是借用所有权,那么引用并不会造成绑定变量所有权的转移。但是借用所有权会让所有者(owner
)受到如下限制:
- 在不可变借用(
&
)期间,所有者不能修改资源,并且也不能再进行可变借用。 - 在可变借用期间(
&mut
),所有者不能访问资源,并且也不能再出借所有权。
引用在离开作用域之时,就是其归还所有权之时。使用借用,与直接使用拥有所有权的值一样自然,而且还不需要转移所有权。
借用规则为了保证内存安全,借用必须遵循以下三个规则。
- 规则一:借用的生命周期不能长于出借方(拥有所有权的对象)的生命周期。
- 规则二:可变借用(引用)不能有别名(Alias),因为可变借用具有独占性。
- 规则三:不可变借用(引用)不能再次出借为可变借用。
检查是复制语义还是移动语义
fn test_copy(hello: impl Copy) -> impl Copy {
hello
}
fn main() {
let x = Box::new("hello");
test_copy(x);
println!("{}", x);
}
Ownership
rules
- 在 Rust 中的每个值都有一个称为
owner
的变量 - 同一时间, 只允许一个
owner
- 当
owner
离开了作用域, 则这个 value 将被 drop 掉.- 即 Rust 在变量离开作用域时, 自动调用
drop
函数来释放资源.
- 即 Rust 在变量离开作用域时, 自动调用
stack and heap
- Stack : 所有保存在 stack 的数据必须是编译期已知的, 固定大小的.
- Heap : 编译期未知大小的或大小允许被改变的数据则存储在 heap.
变量与数据交互
move
类似其他类型的 shallow copy
, 在 Rust 中称为 move
.
当一个变量 move 到另一个变量时, 则旧的变量将变成无效了. 即 ownership move 了.
clone
类似其他类型的 deep copy
, 在 Rust 中是调用 clone()
方法. 开销比较大, 容易影响性能.
stack only
在 stack 上的数据, 都是 copy
. (即不会 move)
在 stack 中的数据, shadow copy
与 deep copy
是一样的.
Rust 不允许在某个类型拥有 Drop trait 的添加 Copy trait .
copy trait 的数据类型
- integer
- bool
- float point number
- character
- tuple 并且仅包含是
copy trait
的元素. 例如(i32, i32)
. 但(i32, String)
则不是.
ownership 与 function
从语义上说, 传递一个值给函数, 跟赋值给一个变量是同义的. 所以,传递一个变量给一个函数, 将会导致 copy
或 move
, 像赋值那样子.
返回值也同样会 transer ownership .
references and borrowing
可以通过 reference 一个对象作为参数, 而不是获取它的 ownership. (这称为 borrowing
, 即函数参数是 reference , &
)
注意,
&
即 reference , 默认情况下也是 immutable 的.要想
&
即 reference 变成 muutable, 则要let mut s = String.from("hello");
然后传递reference 时使用&mut s
即可(相应的函数参数也要指定为&mut String
mutable 的 reference 有一个很大的限制, 即在同一个作用域内, 只允许一个 mutable reference . 比如下面这样子就不允许:
let mut s = String.from("hello"); let r1 = &mut s; let r2 = &mut s;
也不允许 mutable reference 与 immutable reference 共存.
但可以允许多个 immutable reference 共存
fn get_length(s:&String) -> usize {
s.len()
}
下面这种则会获取 ownership . 这时 s 在调用完函数后会被 drop 掉.
fn get_length(s:String) -> usize {
s.len()
}
dangling reference
fn main() {
let s = no_dangle();
println!("{}", s);
}
// 这个方法是 borrow, ownership 在 dangle() 方法内, 返回后, Rust 会释放 s 的内存. 导致 dangle reference, 所以在编译期就禁止了.
fn dangle() -> &String {
let s = String::from("hello");
&s
}
// 这个方法是 move, 即将 ownership 移出去, 这样子, Rust 就不会释放 s 的内存了
fn no_dangle() -> String {
let s = String::from("hello");
s
}
reference rule
- 在给定的任意时间, 要么只有一个 mutable reference 要么多个 immutable reference . 但不能共存.
- reference 必须总是有效的.
slice type
slice 并不拥有 ownership
let s = String::from("hello");
let slice = &s[2..4];
返回 String slice
fn hello_world(s : &String) -> &str {
&s[..]
}
string 字面值
所有 string 字面值, 都是 slice 类型. 即
&str
. 也意味着它是 immutable 的. 因为 reference 默认是 immutable 的.
其他类型的 slice
let a = [1,2,3,4,5];
let slice = &a[1..3]; // slice 是 &[i32] 类型
struct
定义
struct User {
name : String,
email : String,
active : bool,
}
实例化
let user1 = User {
name : String::from("yang"),
email : String::from("xx@qq.com"),
active : true,
};
// mutable
let mut user1 = User {
name : String::from("yang"),
email : String::from("xx@qq.com"),
active : true,
};
注意, 整个实例要么是 immutable, 要么是 mutable, Rust 不允许 struct 中部分字段是 mutable, 部分是 immutable
使用函数来初始化可以简化
fn build_user(email : String, name : String) -> User {
User {
email,
name,
active: true,
}
}
即函数参数跟成员同名即可.
使用旧的 struct 实例来初始化新的
let user1 = User {
name : String::from("yang"),
email : String::from("xx@qq.com"),
active : true,
};
let user2 = User {
name : String::from("yang"),
email : String::from("xx@qq.com"),
..user1
};
这表示, user2 中除了 name, email 不同值之外, 其他成员字段值跟 user1一致.
tuple struct
类似 tuple, 但没有名字, 只有类型. 可以像 tuple 那样解构以及通过索引访问
struct Color(i32, i32, i32);
struct Point(i32, i32);
unit like struct
它没有任何成员. ()
通常用于实现一个 trait, 但没有任何数据要存储到这个类型上.
默认情况下, 自定义的 struct 类型并没有实现 std::fmt::Display
这个 trait . 所以当使用 println!()
来打印 struct 对象时会编译报错.
可修改为 debug print
#[derive(Debug)]
struct Are {
width: u32,
height: u32,
}
fn main() {
let are = Are {
width: 16,
height: 16,
};
println!("are -> {:?}", are);
}
即 println! 中的
{:?}
表示使用#[derive(Debug)]
的 trait 来显示.
method
method 不完全同于 function. Method 是定义在 struct (或 enum, 或 trait 对象) 的上下文中的.
Method 可以 move ownership (self), 或 borrow immutable (&self) 或 borrow mutable (&mut self)
Rust 在调用 method 的时候, 会自动将 struct 转换为 method 签名的类型.(如果兼容的话)
定义 method
#[derive(Debug)]
struct Are {
width: u32,
height: u32,
}
// struct 允许有多个 impl .
impl Are {
fn area(&self) -> u32 {
self.height * self.width
}
}
fn main() {
let are = Are {
width: 16,
height: 16,
};
println!("are -> {:?}", are.area());
}
Method 的第一个参数, 总是 self
(可以为 &self
或 &mut self
).
associated function
在 impl 语句块中的函数第一个参数不是 self
的函数, 即是 associated function. 它不是 method, 因为它不与实例关联.
String::from 就是一个 associcated function
enum 与 pattern matching
定义
enum IP {
V4,
V6,
}
或
enum Message {
Quit,
Move {x: u32, y: u32},
Write(String),
Color(i32, i32, i32),
}
类似 struct, 也可以为 enum 添加 impl .
Option enum.
Rust 中没有 null 这种特性.
match expression
.
fn value_in_message(message: Message) -> u8 {
match message {
Message::Quit => 1,
Message::Move => 2,
Message::Write => 3,
Message::Color => 4,
}
}
//绑定值
fn main() {
println!("{:?}", plus_one(Some(32)));
}
fn plus_one(x : Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
// 结合 if let. 它与 match 类似, 当 match 条件匹配时, 则执行某动作. 注意, if let xx 后面是 = 号, 而不是 ==
// 可以想象为 if let 是 match 语法糖, 它只执行 match 其中之一, 而忽略剩余其他的全部.
fn main() {
let som = plus_one(Some(32));
if let Some(33) = som {
println!("33");
}
}
fn plus_one(x : Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
match 必须匹配所有情况. 特殊的
_
表示剩余所有情况.因为 match 要匹配所有情况. 所以, 如果你只想处理其中之一的情况, 请记得用 if let 语法.
Project manage
- Package
- Crates
- Modules 以及 use
- Paths
package and crates
一个 crate 是一个 binary (main.rs
) 或 lib. (lib.rs
)
# binary
cargo new project
# lib
cargo new --lib libname
一个 package 是一个或多个 crate. 一个 package 包含一个 Cargo.toml
文件描述如何构建这些 crate. 一个 package 可以包含 0 或至多一个 lib crate.
module
# 创建库
cargo new --lib mathlib
# 添加模块. 修改 src/lib.rs 文件
mod hello {
fn say_world(s: &str) {
println!("hello {}", s);
}
mod sub {
fn say_world(s: &str) {
println!("hello in sub mod {}", s);
}
}
}
module tree
根据上面的情况, module tree 为
crate -> hello -> say_world
-> sub -> say_world
module 引用路径
- 绝对路径: 通过 crate 名字或以
crate
开头. 相对路径: 在当前模块中, 使用
self
或super
或直接当前模块的标识符pub mod hello { pub fn say_world(s: &str) { println!("hello {}", s); } mod sub { fn say_world(s: &str) { println!("hello in sub mod {}", s); } } } pub fn say_world(s: &str) { hello::say_world(s); }
默认情况下, rust 所有东西(function, method, struct, enum, module, constant)都是 private 的. 要想导出路径, 则使用 pub 关键字.
调用父模块的方法(每一次 super , 相当于目录的 ../
, 即上级目录)
pub mod hello {
pub fn say_world(s: &str) {
println!("hello {}", s);
}
mod sub {
fn say_world(s: &str) {
println!("hello in sub mod {}", s);
super::super::say_world_parent(s);
}
}
}
pub fn say_world_parent(s: &str) {
hello::say_world(s);
}
struct 与 enum 的 pub
注意, 在 struct 之前使用 pub
表示该 struct 是 pub 的, 但里面的字段仍然是 private
的. 但 enum 的话 如果 pub 了, 则所有都默认都是 pub 的.
use 与 as
use : 导入路径.
pub mod hello {
pub fn say_world(s: &str) {
println!("hello {}", s);
}
mod sub {
fn say_world(s: &str) {
println!("hello in sub mod {}", s);
super::super::say_world_parent(s);
}
}
}
use hello::say_world;
pub fn say_world_parent(s: &str) {
say_world(s);
}
其他用法
use std::{com::Ordering, io};
use std::io::{self, Write};
use std::io::*;
as : 别名
pub mod hello {
pub fn say_world(s: &str) {
println!("hello {}", s);
}
mod sub {
fn say_world(s: &str) {
println!("hello in sub mod {}", s);
super::super::say_world_parent(s);
}
}
}
use hello::say_world as h;
pub fn say_world_parent(s: &str) {
h(s);
}
将 lib 分离不同文件
src/lib.rs
src/hello.rs
只要在 lib.rs 文件中使用
mod hello;
即可导入到 lib.rs 合并了.
使用本地库
Cargo.toml
文件修改:
[dependencies]
rand = "0.3.14"
# 注意 这个命名, 要跟库项目中的 Cargo.toml 中的 name 一致.
mathlib = {path = "../mathlib"} # 这里指向库的根目录
使用
use mathlib;
fn main() {
mathlib::hello::say_world("fuck");
}
使用同一项目中其他目录的模块
结构
src
├── benches
│ └── my_benchmark.rs
├── ipdb.rs
├── kit
│ ├── ip_kit.rs
│ └── mod.rs
├── main.rs
└── parser
├── bid_request.rs
├── mod.rs
└── parser.rs
my_benchmark.rs
中调用 ipdb.rs
的函数
#[path = "../ipdb.rs"] mod ipdb;
fn say() {
ipdb::find();
}
常用集合 Collections
这些数据结构是保存在 heap 的.
vector
每个元素只能是同一种类型. 不过, 可以利用 enum 来持有其他类型.
// 创建
fn main() {
let v: Vec<i32> = Vec::new();
let vv = vec![1, 2, 3];
}
// 添加/删除元素
fn main() {
let mut v: Vec<i32> = Vec::new();
v.push(13);
println!("{:?}", v);
v.remove(0);
println!("{:?}", v);
}
//遍历
fn main() {
let v = vec![1, 2, 3, 4];
for i in &v {
println!("{}", i);
}
}
注意. 这里是用
&v
, 如果为v
, 则表示将 let v 的 ownership move 到 for 语句块了. 当结束时, v 就无效了, 即后面的代码不能再访问 v 了.
String
它是 UTF8 encode 的.
let mut s = String::new();
//修改
s.push_str("hello world");
//拼接 concat
fn main() {
let s1 = String::from("hello ");
let s2 = String::from("world");
let s3 = s1 + &s2;
println!("{}", s3);
}
实际调用的方法签名伪代码为
fn add(self, s:&str) -> String {}
所以, 第一个参数(即 s1)的 ownership 已经 move 了. 之后的代码中, s1 不再有效. 由于 s2 只是借用而非 move, 所以 s2 仍然是 valid 的.
//拼接 2
fn main() {
let s1 = String::from("hello ");
let s2 = String::from("world");
let s3 = format!("{}{}", s1, s2);
println!("s1 {}, s2 {}, s3 {}", s1, s2, s3);
}
//索引 String. Rust 的 String 不同于其他的编程语言可以直接根据下标来索引字符.
// Rust 的 String 内部是使用 Vec<u8> . 即 Vec<byte> 来保存的. 即字节 Vec
// String.len() 返回的是字节的长度, 而不是字符.
// Rust 中的 String 有三种表示. bytes, scalar values, grapheme clusters.
// Rust 不允许使用索引来引用 String 的另一个原因是, 索引操作预期是 O(1) 的, 但Rust 中的 String 并不能这样子, 只能从头到尾来决定字符是否有效.
// 允许使用索引的情况是返回值为 a byte, a character, a grapheme cluster 或 string slice.
"hello".chars()
"hello".bytes()
grapheme 比较复杂, 并没有在标准库中提供.
HashMap
类似 vector, Hashmap 保存在 heap. k 要同一类型. v 也要同一类型.
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("hello"), 10);
scores.insert(String::from("world"), 20);
println!("{:?}", scores);
}
//通过两个 vec 来构建 HashMap
use std::collections::HashMap;
fn main() {
let teams = vec![String::from("hello"), String::from("world")];
let scores = vec![10, 20];
let m: HashMap<_, _> = teams.iter().zip(scores.iter()).collect();
println!("{:?}", m);
}
//遍历
for (k, v) in &m {
}
ownership
对于实现了 Copy trait 的, 例如 i32
, 则值将 copy 到 HashMap, 而对于 owned value 的, 例如 String
, 则 ownership 将会 move 到 HashMap. 例如
use std::collections::HashMap;
fn main() {
let h = String::from("hello");
let w = String::from("world");
let mut map = HashMap::new();
map.insert(h, w);
println!("{:?}", map);
//println!("h {}, w {}", h, w); h, w 的 ownership 已经 move 到 map 了, 所以 insert 之后, 这两个变量已经 invalid 了.
}
不过, 如果我们插入 reference 的话, 则 ownership 不会 move .
但这种情况下, reference 的生命周期至少要与 hashMap 一样有效.
更新 hashmap
//直接覆盖
let h = String::from("hello");
let w = String::from("world");
let mut map = HashMap::new();
map.insert(h, w);
map.insert(String::from("hello"), String::from("new world"));
//只在 key 没有值的时候插入
map.entry(String::from("newKey")).or_insert(String::from("fuck"));
//基于旧值更新. or_insert 实际返回的是 &mut v, 这样子就可以修改这个值即可.
let v = map.entry(xx).or_insert(yy);
*v += 1;
hash function
默认情况下 HashMap 使用的是 siphash 函数. 但它不是性能最快的, 但权衡了安全性. 如果你发现它不能满足的你性能要求, 可以指定其他的 hash function .
错误处理
不可恢复的错误 panic
panic!("crash message...");
当使用该宏时, 会打印 panic 信息, 然后 unwind 以及程序清理栈, 最后退出.
Unwinding 要执行许多工作. 一个可选的方式是立即 abort . 这会导致程序直接退出, 而不进行清理. (例如内存的释放交由操作系统来处理, 而不是程序自身).
如果你的程序binary文件想尽可能小, 则可以切换 panic 为 abort 处理模式. 在 Cargo.toml 文件中添加
[profile.release]
panic = 'abort'
跟踪栈. (此时, debug symbols 必须要开启. --release
参数构建的 binanry 则没有)
RUST_BACKTRACE=1 cargo run
可恢复错误 Result
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => {
panic!("open file error {:?}", error);
}
};
}
//进一步处理不同的错误
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(err) => {
panic!("create file error {}", err);
}
},
_ => {
panic!("other error {}", error);
}
}
};
}
在 Error 中快速 panic
unwrap()
: 即如果 Result 是 Ok 则返回 Ok 持有的数据. 如果返回的是 Err 则调用panic!().
let f = File::open("hello.txt").unwrap();
expect(msg)
: 类似 unwrap, 但让我们选择 panic 输出的 message.
传播 error
即将错误处理交由 caller 调用者来决定.
fn read_line() -> Result<String, io::Error> {
}
快速传播 Error 操作符: ?
fn read_string() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
ok(s)
}
//简化调用
fn read_string() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
//再一步
use std::fs;
fn read_string() -> Result<String, io::Error> {
fs::read_to_string("hello.txt")
}
即在 Result
类型后添加 ?
. 如果返回 Ok, 则会继续执行. 否则 return 返回调用者.
?
与 match 的区别是: ?
会调用 From trait
的 from
函数, 该函数用于将 error 类型转换为另一个类型.
泛型, trait 以及生命周期
泛型函数
fn largest<T>(list:&[T]) -> T {
}
trait
定义共享行为
fn main() {
let p = Person{};
println!("{}", p.say_hello());
}
//定义一个 trait
pub trait TraitName {
fn say_hello(&self) -> String;
fn say_hello2(&self) -> String {
//添加默认实现
String::from("default value")
}
}
//为某类型实现 trait
struct Person {
}
impl TraitName for Person {
fn say_hello(&self) -> String {
String::from("hello world")
}
}
trait 作为参数
pub fn notify(item: impl TraitName) {
}
//或
pub fn notify<T:TraitName>(item: T) {
}
//多种 Trait 结合
pub fn notify(item: impl TraitName + Display) {
}
//where clause
fn notify<T, U>(t: T, u: U) -> i32
where T: Display + Clone,
U: Clone + Debug
{
}
trait 作为返回值
fn notify() -> impl TraitName {
}
reference lifetime
每个引用的 lifetime , 即是它们的作用域. 大多数时候, lifetime 是隐含及推断的.
它的主要目的是防止 dangling reference.
lifetime annotation : 它并不改变一个引用能存活多久. 它只是描述了在多个 reference 之间的 lifetime 关系. 语法如下
&i32 , 添加 lifetime annotation 则为 &'a i32, &'a mut 32
单个 reference 标注 lifetime 并没有多大意义. 因为 annotation 主要是告诉 Rust 在多个 reference 之间的 lifetime 关系.
默认lifetime rules
- 每个reference 参数都有它们自己的 lifetime
- 如果仅有一个输入参数, 则输出(返回参数)的 lifetime 跟输入的一致.
- 如果有多个输入参数, 但其中之一是
&self
或&mut self
, 由于是 method, 则self
的 lifetime 被应用于所有输出参数.
static lifetime
有个特殊的 lifetime anntion , 它就是 'static
. 所有 string 字面量都是这种 lifetime
生命周期参数
值的生命周期和词法作用域有关, 但是借用可以在各个函数间传递,必然会跨越多个词法作用域.
显式生命周期
生命周期参数必须以单引号开头,参数名通常都是小写字母,比如'a
。生命周期参数位于引用符号&
后面,并使用空格来分割生命周期参数和类型. 比如
&'a i32
&'a mut i32
标注生命周期参数并不能改变任何引用的生命周期长短,它只用于编译器的借用检查.
函数中使用的语法如下
fn foo<'a>(s: &'a str, t: &'a str) -> &'a str;
函数名后面的<'a>
为生命周期参数的声明,与泛型参数类似,必须先声明才能使用。函数或方法参数的生命周期叫作输入生命周期(input lifetime
),而返回值的生命周期被称为输出生命周期(output lifetime
)
函数签名的生命周期参数有这样的限制条件:输出(借用方)的生命周期长度必须不长于输入(出借方)的生命周期长度(此条件依然遵循借用规则一)
生命周期省略规则
- 每个输入位置上省略的生命周期都将成为一个不同的生命周期参数
- 如果只有一个输入生命周期的位置(不管是否忽略),则该生命周期都将分配给输出生命周期
- 如果存在多个输入生命周期的位置,但是其中包含着
&self
或&mut self
,则self
的生命周期都将分配给输出生命周期
它们组合在一起
fn longest_with<'a, T>(x: &'a str, y:&'a str, ann: T) -> &'a str where T: Display {
println!("ann {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}
自动化测试
pub mod hello {
pub fn say_world(s: &str) {
println!("hello {}", s);
}
mod sub {
fn say_world(s: &str) {
println!("hello in sub mod {}", s);
super::super::say_world_parent(s);
}
}
}
use hello::say_world as h;
pub fn say_world_parent(s: &str) {
h(s);
}
#[cfg(test)]
mod tests {
#[test]
fn testSay() {
super::hello::say_world("fuck the world");
assert_eq!(2 + 2, 5);
}
}
测试命令
# 执行所有测试代码
cargo test
# 控制并行数
cargo test -- --test-threads=1
# 只执行特定测试方法
cargo test 方法名
# 执行显式指定属性的测试方法
#[cfg(test)]
mod tests {
#[test]
fn testSay() {
super::hello::say_world("fuck the world");
assert_eq!(2 + 2, 5);
}
#[test]
#[ignore]
fn testSay2() {
super::hello::say_world("fuck the world2");
assert_eq!(2 + 2, 5);
}
}
这时
cargo test -- --ignored
则只会执行带有 `#[ignore]` 属性的测试方法. 而普通 cargo test 则不会执行.
性能测试
示例性能测试代码
#![feature(test)]
extern crate test;
use test::Bencher;
#[bench]
fn bench_xor_1000_ints(b: &mut Bencher) {
b.iter(|| {
(0..1000).fold(0, |old, new| old ^ new);
});
}
rustup install nightly
然后执行
cargo +nightly bench
I/O
读取命令行参数
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
println!("? => {:?}", args);
}
注意, args() 如果包含有无效的 unicode 则会导致 panic. 如果想包含这种数据的话, 则要用 args_os() .
传递参数来运行
cargo run hello world ,则输出如下
Compiling hello-rust v0.1.0 (/Users/emacsist/Documents/rust/hello-rust)
Finished dev [unoptimized + debuginfo] target(s) in 0.80s
Running `target/debug/hello-rust hello world`
["target/debug/hello-rust", "hello", "world"]
函数式语言特征: iterator 与 closure
Closure : anonymous functions that can capture their environment . 即, 它可以捕获定义他们地方所有作用域的值.
let clo = |num| {
println!("in closure ....");
num
};
//注意, let clo 表示 clo 包含匿名函数的定义, 则不是执行后的返回值.
//如果不指定参数类型, 则 Rust 以第一次调用时的为准.
fn trait
struct Cache<T> where T: Fn(u32) -> u32 {
exec: T,
value: Option<u32>,
}
闭包(Closure)通常是指词法闭包,是一个持有外部环境变量的函数。外部环境是指闭包定义时所在的词法作用域。外部环境变量,在函数式编程范式中也被称为自由变量,是指并不是在闭包内定义的变量。将自由变量和自身绑定的函数就是闭包
捕获 environment
有三种方式, 对应函数参数的三种方式(take ownership, borrowing mutably, borrowing immutably). 三个 trait 如下
FnOnce
: 将会 take ownership.FnMut
: borrowing mutablyFn
: borrowing immutably
可以显式指定 move :
let x = 32;
let clo = move |param| param == x
这时, x 的 ownership move 到了 closure 里了. 后面将不再有效
iterator
注意, iter().next() 返回的是 &item
所有 iterators 都是 lazy 的. 例如
let v1 = vec![1,2,3];
let v2 = v1.iter().map(|x| x + 1); // 这时 v2 是 Map 类型. 要调用 collect() 来消费 map 产生的 iter
use std::env;
fn main() {
let v1 = vec![1, 2, 3];
let v2: Vec<i32> = v1.iter().map(|x| x + 1).collect();
println!("{:?}", v2);
}
它与 for 的性能: 在 Rust 中, 不用担心这些开销, iterator 与 for 几乎一样的.
Cargo
默认的 profile 是 dev
: 它的默认优化级别是 0.
Cargo.toml 文件中
[profile.dev]
opt-level = 0
[profile.release]
opt-level = 3
文档化注释 /// doc
workspace :
tree .
.
├── Cargo.toml
├── hello
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── hello-lib
├── Cargo.toml
└── src
└── lib.rs
在 workspace 根目录执行命令
cargo build
Compiling hello v0.1.0 (/private/tmp/work/hello)
Compiling hello-lib v0.1.0 (/private/tmp/work/hello-lib)
Finished dev [unoptimized + debuginfo] target(s) in 1.82s
从 cargo.io 安装 binary: cargo install xxx
smart pointer
Box<T>
用于指向在 heap 的数据. 它没有性能开销, 除了保存数据在 heap 而不是 stack 外.
recursive type
在编译时, Rust 需要知道一种类型要占多少空间. 如果在编译时不知道大小的类型, 它就是一个 recursive type
, 即该值是它自身同一类型的另一个值的一部分. 但 Box 有大小, 所以可以用它来实现 recursive type.
例如
enum List {
Cons(i32, List),
Nil,
}
use List::{Cons,Nil};
let list = Cons(1, Cons(2, Cons(3, Nil)));
上面代码编译不了. 因为 Rust 编译时不知道 List 的大小.
借助 Box 就可以编译了
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons,Nil};
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
通过 Deref
trait , 让 smark pointer 与普通 reference 一样.
通过实现 Deref trait , 允许你自定义 dereference operator, *
.
struct MyBox<T>(T);
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
这时
let m = MyBox(13);
*m 等同于 *(m.deref())
Drop trait
当变量 owner 离开作用域时, Rust 会调用该变量的 drop()
(如果有的话)来释放资源.
但该方法, 不允许显式调用. 例如 o.drop();
. 但可以通过 std::mem::drop
来处理. drop(o);
Rc<T>
变量默认是单个 ownership 的. 如果想开启多个 ownership, 则可使用该类型.
注意, 它只能用于单线程环境.
只增加引用计数: Rc::clone(&ref);
如果调用 ref.clone()
这是deep copy 了
RefCell<T>
它不同用 Box<T>
(编译期, compile time , 检查 borrow 规则), 而 RefCell<T>
(运行期, runtime, 检查 borrow 规则)
它也只能用于单线程环境.
概括
Rc<T>
允许同一份数据有多个 owner. 而Box<T>
和RefCell<T>
是 single ownerBox<T>
允许 immutable 或 mutable borrow , 并且是在 compile time 检查.Rc<T>
只允许 immutable borrow , 并且在 compile time 检查.RefCell<T>
允许 immutable 或 mutable borrow, 但是在 runtime 检查.- 由于
RefCell<T>
允许 mutable borrow 并且在 runtime 才检查, 所以, 你可以更改在RefCell<T>
的值, 即使RefCell<T>
是 immutable 的也可以.
并发
在 Rust 的线程实现中, 与操作系统的是 1:1
模型.
use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(|| {
for i in 1..10 {
println!("hi {}, in thread", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi {}, in main", i);
thread::sleep(Duration::from_millis(1));
}
}
//等待线程完成. hand 是 JoinHandle 类型.
use std::thread;
use std::time::Duration;
fn main() {
let hand = thread::spawn(|| {
for i in 1..10 {
println!("hi {}, in thread", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi {}, in main", i);
thread::sleep(Duration::from_millis(1));
}
hand.join().unwrap();
}
move
这样, 可以让某线程从另一条线程数据中 move ownership.
use std::thread;
use std::time::Duration;
fn main() {
let v = vec![1, 2, 3];
let hand = thread::spawn(move || {
println!("{:?}", v);
});
hand.join().unwrap();
for i in 1..5 {
println!("hi {}, in main", i);
thread::sleep(Duration::from_millis(1));
}
}
线程之间消息传递
mpsc, multi producer, single consumer
use std::thread;
use std::time::Duration;
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let msg = String::from("hi");
tx.send(msg).unwrap();
});
let recMsg = rx.recv().unwrap();
println!("Got {}", recMsg);
}
//多个 tx
let tx1 = mpsc::Sender::clone(&tx);
channel 与 ownership
在 tx
发送消息时 send()
函数会 take ownership , 即参数的 ownership 会 move , 然后被传送到 rx.
Mutex
use std::sync::Mutex;
fn main() {
let m = Mutex::new(5);
{
let mut num = m.lock().unwrap();
*num = 6;
}
println!("{:?}", m);
}
Arc<T>
类似 Rc<T>
, 但是线程安全的.
use std::sync::{Mutex, Arc};
use std::thread;
use std::rc::Rc;
fn main() {
//你可能已经看到, counter 是 immutable 的. 但后面却修改了它.
// 这意味着 Mutex<T> 提供了内部修改的能力. 类似 Cell 家族.
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 1..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!("{:?}", *counter.lock().unwrap());
}
如果这里修改为 Rc , 则会导致编译错误.
OOP
//定义 Object
pub struct Person {
name: String,
age: u16,
}
//定义方法
impl Person {
pub fn getName(&self) -> &str {
}
}
// Trait
pub trait Run {
fn run(&self);
}
//为 Person 实现 Run trait
impl Run for Person {
fn run(&self) {
//code...
}
}
常见问题及解决
Blocking waiting for file lock on package cache
rm ~/.cargo/.package-cache
然后重新 cargo run