Rust
我觉得 Rust Book 第二版有些内容都缺了,
比如 Universal functional call syntax
安装
使用 rustup
编译
$ rustc xxx.rs
$ ./xxx
Cargo
用于处理项目依赖等
创建新项目
$ cargo new hello-cargo
编译
$ cargo build [--release]
编译, 默认是 debug 版本
$ cargo run
编译并且运行
$ cargo check
检查错误
$ cargo update
更新依赖
设置
所有项目配置, 包括项目依赖, 都在 Cargo.toml 中
增加依赖, 在 Cargo.toml 中加入
[dependencies]
rand = "0.3.14"
使用 SemVer
实际上是只要和 0.3.14 兼容即可, 如 0.3.x, x >= 14
语言基础
区分大小写. 语句需要分号结尾. 注释是 //.
使用 snake_case.
/// 是文档注释, 其中支持 markdown 语法.
变量常量声明
声明变量如
let x = 233;
不必须, 但是可以指定类型如
let x: u8 = 233;
rust 中变量默认是不可变的, 除非
let mut x = 233;
注意 _ 不能作为变量名
新变量可以用旧变量的名字 (shadowing), 新的声明会覆盖旧的.
在任何作用域中都可以声明常量, 必须指定类型
const CONST: u32 = 65_534;
还可以声明全局变量, 也是任何作用域中均可, 最好指定类型
static N: i32 = 5;
函数内部用 let 声明的变量都放到栈上, 因此需要是定长的.
表达式
表达式可以有副作用
let x = { println!(x); fac(10) };
语句块可以复杂如
let m = if x < y { x } else { y };
甚至
let x = {
let (mut a, mut b) = (1, 1);
loop {
b = a+b; a = b-a;
if b > MAX { break b; }
}
};
语句
if 语句
if x > y {
println!("x greater than y");
} else if x < y {
println!("x less than y");
} else {
println!("x equal to y");
}
loop 语句
loop {
// 相当于 while (1), 用 break 跳出
// break 后可以加一个值作为语句块的返回值
}
while 语句
while x != 0 {
x = x-1;
}
for 语句
for i in (1..10) {
s = s+i;
}
其中 (1..10) 是 std::ops::Range<{Integer}>
let a = [1, 2, 3, 4, 5];
for (i, v) in a.iter().enumerate() {
s = s+v;
}
函数
声明函数格式如
fn main() { println!("Hello world"); }
返回值和参数格式如
fn add(x: u32, y: u32) -> u32 { return x + y; }
可以省略 return
fn add(x: u32, y: u32) -> u32 { x + y }
fn ask_for_money(x: u32) -> String {
if x > 100 {
String::from("Too much!")
} else if x > 10 {
return String::from("Emmmm...");
} else {
String::from("Ok")
}
}
有些函数没有返回值 (diverging functions), 写成
fn diverge() -> ! { loop { } }
类型
i8, u8, ..., i32, ..., i64, u64, i128, u128;
isize, usize
整数默认是 i32.
字面量如 233, 547, b'A', 65_536, 0xDEADBEEF
整数溢出在 debug 情况下是 panic, release 情况下是 wrap
f32, f64
默认是 f64
bool
取值为 true false
char
特别注意 rust 中 char 是 unicode 而非 ANSI
所以可以 let ch: char = '你'.
对于 ANSI 可以处理如 let ansichar: u8 = b'A'
元组
声明如 let tup: (i32, u8) = (500, b'A');
可以解构如 let (x, y, z) = tup;
访问某元素如 let x = tup.0;
数组
声明如 let a: [i32; 5] = [1, 2, 3, 4, 5];
访问某元素如 a[0], 越界导致运行时的 panic
Rust 有类似 typedef 的类型别名, 使用 type alias = original, 如
type Num = i32;
let x: i32 = 5;
let y: Num = 5;
Ownership
除了 GC 以外另一种保证内存安全的方案,
检查都在编译时完成, 运行时没有开销.
基本思想
每个值在任何时刻都有一个且只有一个 owner 变量 (或者名字)
不同的时候 owner 变量可能变化.
当 owner 离开其生命周期 (e.g. 离开作用域) 的时候, 值被解构 (drop).
移动语义
如下代码片段中
let x = String::from("Hi"); // A
let y = x; // B
1. A 语句执行了创建一个 String 值, 以及把这个值绑定到变量 x 的工作.
2. A 执行完之后, x 是 "Hi" 的 owner, 可以通过 x 来访问这个值.
3. B 语句执行了将 x 拥有的值转交给 y 的工作,
执行之后 x 就不再拥有 "Hi" 了, 不能再使用没有任何值的 x
但如下代码片段中
let x = String::from("Hi"); // A
let y = x.clone(); // B
1. B 语句执行的是, 创建新的 String 值, 等于 "Hi"
(但是和 x 拥有的 "Hi" 是不同的对象) 然后将新的值绑定给 y
2. 之后既可以使用 x 也可以使用 y.
具体的原因是, String 实现了 Clone trait.
但如下代码片段中
let x = 233; // A
let y = x; // B
1. B 语句之后 x 和 y 都可以用, 而且显然的改变 x 不会影响 y
具体的原因是, 基础类型如 i32 实现了 Copy trait.
另外, 如果将某个 String 变量作为参数传递给了 fn take_own(s: String)
那么其值的 owner 就变成了函数的参数, 原来的变量就失去了值
引用
使用某个值但是不取得其 ownership (borrowing). 如
let x = String::from("Hi");
let y: &String = &x;
可以使用引用作为函数参数
fn strlen(s: &String) -> usize { s.len() }
引用默认是不可变的, 除非使用 &mut
fn change(s: &mut String) { s.push_str("changed"); }
防止数据竞争, 任何时候对于任何值, 有如下情况
1. 有单个写引用
2. 有零或多个读引用
slice
除了普通引用, 还有种引用是 slice.
slice 引用数组 / 字符串的一部分, 但是也不取得数据的 ownership.
因此 slice 只能在其引用形式下存在.
slice 只包含 <beg,len>; 不包含 cap (没有存储, 没有 ownership).
slice 声明类似
let s = String::from("abcdefgh");
let s1 = &s[0..4];
let s2 = &s[0..=4];
let s3 = &s[..4];
let s4 = &s[4..];
let s5 = &s[..];
字符串字面量, 以及上面的 s1 .. s5 的类型都是 &str
slice 包括的不仅仅是字符串, 还有数组, 如
let a = [1, 2, 3, 4, 5];
let slice: &[i32] = &a[1..3];
结构体
结构体的声明如
struct Person { name: String, age: u8, }
实例化不用 new
let tom: Person = Person { name: String::from("Tom"), age: 17 };
访问某个域通过点记号
println!("{}", tom.name);
实例化的时候可以有一些简写, 如变量名和域名相同
let (name, age) = (String::from("Sav"), 21);
let sav = Person { name, age }
可以复用另外实例的域
let tim = Person { name: String::from("Tim"), ..sav }
元组 struct 的各个域没有名字, 但是整个 struct 是有名的
struct ColorRGB(i32, i32, i32);
let red = ColorRGB(255, 0, 0);
println!("{}", red.0);
通常用来给一种 tuple 起名, 防止其和普通的 tuple 混淆
空 struct 可以用 struct Empty { } 或者 struct Empty();
在 struct 定义寸前加上 #[derive(Debug)], 让 struct 能被 pretty print
打印使用 println!("{:?}", some_instance);
方法
方法的第一个参数是 self, 表示调用方法的结构体实例.
方法在 impl 块定义, 一个结构体可以有多个 impl 块
impl Person {
fn age_of_next_year(&self) -> u8 { self.age + 1 }
}
调用如
println!("{}", tom.age_of_next_year());
第一个参数不是 self 的方法是静态方法, 如
impl Person {
fn new(name: String, age: u8) -> Self {
Person { name, age }
}
}
其中 Self 是实现方法的类型, 通常就是 self 的类型.
调用类似
Person::new(String::from("Tom"), 12)
Trait
类似 Java 的 interface.
定义如
trait Summary {
fn summarize(&self) -> String;
}
在定义中不要忘记写 self
结构体在单个 impl 块中实现某 trait 的所有接口
格式使用 impl TRAIT for STRUCT, 如
struct NewsArticle { ... }
impl Summary for NewsArticle {
fn summarize(&self) -> String {
// ...
}
}
trait 中的方法可以有默认实现,
这时实现 trait 的结构体就不一定要重写默认的实现
trait Summary {
fn summarize(&self) -> String {
return String::from("stupid summary");
}
}
函数的参数可以是 trait, 类似接口参数, 使用 impl 关键字
fn notify(x: impl Summary) { ... }
可以用泛型写成等价的
fn notify<T: Summary>(x: T, y: T) { ... }
以及
fn nofity(x: T, y: T) where T: Summary { .. }
注意 impl TYPE 只能在函数参数或者返回值处使用
并且一个函数可以要求参数实现多个 trait
pub fn notify(x: impl T) where T: Summary + Copy { ... }
甚至函数返回可以是 trait
pub fn return_trait() -> impl Summary { ... }
如果结构体实现不同的 trait 中同名函数, 直接调用会有歧义
struct Foo;
impl Bar for Foo {
fn f(a: i32) { println!("Bar::f({})", a); }
}
impl Baz for Foo {
fn f(a: i32) { println!("Baz::f({})", a); }
}
需要使用方法调用的一般语法
let f = Foo;
Bar::f(&f, 2);
Baz::f(&f, 3);
Derivable traits
使用简单的一行属性, 使得结构体实现某 trait, 如
#[derive(Debug, Copy, Clone)]
常用的 derivable traits 有
Debug
允许可读输出
PartialEq
允许 == 和 != 比较结构体.
结构体相同, 当且仅当所有字段相同
Eq
是一个 marker trait, 语义是对于这个类型的所有值, 它都等于自己
Eq 依赖于 PartialEq, 但是并不等价.
如对于浮点, NaN != NaN
PartialOrd
允许 < > <= >=
依赖于 PartialEq.
对于结构体, 从前到后比较每一字段.
对于 enum, 前面的更小.
Ord
全序, 相对于 PartialOrd 的偏序.
Clone
可以完成 (可能很昂贵的, 可能拷贝堆数据) 深拷贝
Copy
只复制栈上的值就可以完成拷贝, 如 i32
Hash
可以被散列, e.g. 作为 Map 的 K
Default
某个类型的默认值.
泛型
允许变量的类型是抽象的类型, 待使用时再确定.
Rust 的泛型实现 monomorphization 类似 C++ 的 template,
没有运行时开销.
泛型函数如
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
泛型结构体如
struct Point<T> { x: T, y: T, }
之后声明变量如
let p = Point { 1, 2 };
泛型结构体的 impl 块如
impl<T> Point<T> {
fn get_x(&self) -> &T { &self.x }
}
注意 impl 块中, 要写 impl<T> 而非 impl, 否则就是泛型特化的写法了如
impl Point<i32> {
// ...
}
在特化的 impl 中可以加入新方法, 表示这些方法只有对于部分的 T 才存在
impl Point<i32> {
fn only_work_with_i32() { ... }
}
即使在泛型的 impl 块中, 方法也可以有泛型参数
impl <T> Point<T> {
fn foobar<U>(&self, v: U) -> (T, U) { (self.x, v) }
}
还可以有泛型 enum, 如
enum Result<T, E> {
Ok(T),
Err(E),
}
面向对象特性
封装
struct 的字段, 函数, 除非标记为 pub, 否则总是私有的.
继承
Rust 没有继承, 而是使用 trait object 完成类似的功能.
trait 对象指向一个实现了该 trait 的值, 它是一个指针, 如 & 和 Box<>
任何实现了该 trait 的值都可以是一个 trait object.
如
let a: Vec<&dyn Animal> = vec![&Cat {color: Orange}, &Dog {male: True}]
trait 对象有运行时的开销, 而泛型没有.
trait 的好处在于, 它能完成 dynamic dispatch
(多态中根据类型决定调用的函数具体是哪个), 如
fn foobar(name: &str) -> &dyn Speaker {
match name {
"tom" => &Tom { age: 15 },
"jerry" => &Jerry(),
_ => &Tom { age: -1 },
}
}
这样的代码不可能被施加 monomorphization, 不可能用泛型实现
Lifetime
通常 lifetime 都是推导的, 但是无法推导的时候必须指定.
基本原理是, 任何引用都不应当比其引用的值有更长的生命周期,
否则就会出现野引用. 所有引用都有一个生命周期.
值也有生命周期, 通常就是其 owner 的作用域.
有些情况, 如函数返回一个引用, 结构体中有引用的时候,
可能自动推导无法确定此引用的生命周期,
那么需要手动增加泛型的生命周期注解.
生命周期注解的格式类似泛型, 如
fn longest<'a>(x: &'a str, y: &'a str): &'a str { ... }
说明返回值的生命周期和两个参数中最短的一个是相同的.
在结构体中, 类似
struct StructWithRef<'a> {
v: &'a i32,
}
其对应的 impl 块类似
impl<'a> StructWithRef<'a> {
// ...
}
Rust 能推导的生命周期, 如
fn ident(a: &str) -> &str { a }
基本规则如下
* 如果只有一个输入 lifetime 'a e.g. 只有一个参数,
那么所有输出的 lifetime 都是 'a
* 如果有一个输入是 &self 或者 &mut self,
那么所有输出的 lifetime 都是 self 的生命周期
一个特殊的 lifetime 是 'static, 其代表程序执行的全过程.
字面量串的 lifetime 是 'static
Enum
枚举在 Rust 中是一种类型, 其和其他类型一样可以作为函数参数, 返回值的类型.
基础的 enum 声明格式如
enum IpAddrKind { V4, V6, }
之后引用如
let four = IpAddrKind::V4;
枚举类型也可以有自己的域, 甚至变成结构体, 如
enum Message {
Quit,
Move { dx: i32, dy: i32 },
Write(String),
Panic(u8, String),
}
之后
let loopbackv4 = IpAddr(127, 0, 0, 1);
枚举也可以有 impl 块
impl Message {
// .. methods on &self or &mut self
}
很有用的一个枚举类型是 Option<T> { Some(T), None }
Option 用来代替其他语言的 null, 因为 null 容易出现错误
注意用 None 赋值时候需要指定类型
let absent_number: Option<i32> = None;
模式匹配结构
模式匹配通常用于处理枚举类型, 类似 C 的 switch-case 如
match x {
Some(val) => println!("Got value {}", val),
None => println!("No value found"),
}
其中每一项 PATTERN => EXPR 称为一个 arm,
每个 arm 都应该由逗号结尾.
第一个 arm 匹配所有 Some, 并且将 Some 包装的值绑定到 val 中
对应 switch-case 的 default 的是 _, 空语句是 ()
let y = match x {
1 => { println!("unit val"); 1 },
0 => { println!("zero val"); 0 },
_ => -1,
}
如果只考虑多种情况的一种, 那么
match { v => EXPR1, _ => EXPR2 }
可以简写为 if let PATTERN = EXPR { ... } [else { ... }], 如
if let Some(15) = opt_val {
println!("We got a Some(15)!");
} else {
println!("Looking for Some(15)... failed");
}
Rust 中大量使用模式匹配, 如
* match PATTERN => EXPR,
* if let PATTERN = EXPR { ... }
* while let PATTERN = EXPR { ... }
* for PATTERN in EXPR { ... }
* 甚至 let PATTERN = EXPR, 如
let (x, y) = (15, 12)
* 函数参数的形式是 PATTERN: EXPR 如
fn ptn_arg(&(x, y): &(i32, i32)) { ... }
利用 pattern matching 可以轻松地完成结构化的数据的解构, 如
* 解构元组: let (x, y) = (4, 5)
* 解构结构体: fn fnparam_ptn(&MyStruct {x, y}: &MyStruct) { ... }
解构可以嵌套
PATTERN 的形式可以很多样, 如
match xxx {
x1 | x2 => // ...
x1 ... x2 => // ...
Point {x: 0, y} => // ... 这里不能用 x
&Point {x, y} => // ... 如果数据是引用
Some(x) if x < 5 => // ...
(first, _, third, _, fifth) => // ...
HighDimPts { first, .., last } => // ... 使用 .. 必须是无歧义地
_ => // ...
}
PATTERN 有两种, refutable 和 irrefutable.
诸如 _, x, (x, y) 是 irrefutable 的, 不能放到 if let 之类里面
诸如 Some(x) 是 refutable 的, 不能直接放到 let 里面
match arm 必须是 refutable 的, 除非最后一个 arm
解构默认就会取得 ownership, 可以使用 ref 来避免
let so = Some(String::from("Some ownership"));
if let Some(ref bstr) = so { println!("one: {}", bstr); }
if let Some(ref mut bstr) = so { so.push_str("~~~"); }
而不能写成 if let Some(&bstr) ...
使用 @ 来完成检测条件的同时变量绑定
match i32tuple {
(x @ 3...7, y) => // we can use the variables x and y here
_ => // oth
}
函数式编程 // TODO
闭包
闭包可以完成 lazy evaluation! (把值引用变成函数调用)
定义格式
let myclosure = |num| {
println!("Calling myclosure({})", num);
num + 2
};
闭包可以不用手动指定参数和返回值的类型, 而是可以推断
不过要加也可以
let myclosure = |num: i32| -> i32 {
println!("Calling myclosure({})", num);
num + 2
};
Rust 每个闭包的类型是一个独有的匿名类型.
两个闭包即使签名相同, 类型也不会相同.
闭包可以捕获环境中的变量, 函数不能
closure 捕获环境变量的方式有三种
1. 取得所有权
2. 取得写 handle
3. 取得读 handle
分别对应三种 Trait:
FnOnce
FnMut
Fn
闭包默认都实现 FnOnce, 如果不获取 ownership 则实现 FnMut, 如果不修改变量则实现 Fn
iterator
可以 for x in xxx.iter() { ... }
(如果实现了 IntoIterator, 还可以 for x in xxx { ... })
Iterator 是一个 trait, 实现需要
1. 指定一个 type Item
2. 实现 fn next(&mut self) -> Option<Self::Item>
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
iterator 和 closure 是 zero-cost abstraction
智能指针
Rust 中, 引用被认为是一种指针, 也有不安全的底层指针.
智能指针相对于引用来说, 有额外的元数据以及功能, 如引用计数.
另外, 引用没有数据 ownership, 但是通常智能指针拥有数据 ownership.
事实上, String 和 Vec<T> 就都是智能指针.
Box<T>
指针存储在栈上, 但是指向的数据在堆上.
并且指针拥有数据的 ownership, 因此 Box<T> 离开作用域时, 会解构
1. 栈上的指针
2. 堆上的数据
这种功能由 Drop trait 实现
指针可以当成引用来使用, 如
let b = Box::new(5);
println!("{}", b);
这是由 Deref trait 来实现的
一个例子就是链表的实现
enum List {
Next(i32, Box<List>),
End
}
这里不能直接用 List, 因为这样的话就类似
struct List{
int val;
struct List next;
}
了, 出现无线循环.
也不能用 &List, 因为 Next 的生命周期将变得不清楚.
Rc<T>
数据可能有多个 owner, 它们都离开生命周期之后数据才能被解构
Rc<T> 不是线程安全的, 只能在单线程环境下使用.
例子还是链表的实现, 不过这次两个链表能有相同的后缀.
use std::rc::Rc;
enum RcList {
N(i32, Rc<RcList>),
E
}
之后构建链表的时候用到两个函数, 取得 ownership 的 new,
以及完成浅拷贝的 clone
use RcList::*;
let a = Rc::new(
N(1, Rc::new(
N(2, Rc::new(E)))));
let b = N(10, Rc::clone(&a));
let c = N(20, Rc::clone(&a));
RefCell<T>
Rust 中一个设计模式是 interior mutability, 基本含义是,
修改一个值, 即使有指向该值的不可变引用.
通常这个可以用来实现可变的字段, 即使整个值是不可变的.
如 Rc 中的计数就是可变的字段 -- 但是它不对外暴露
所以这个字段可变, 对于不可变的 &Rc 是完全没有问题的.
RefCell<T> 可以实现 interior mutability, 但只能在单线程情况下使用
方法有
let refcell = RefCell::new(x)
创建 RefCell
refcell.borrow_mut()
取得一个 x 的可变引用 &mut T
x.borrow()
取得一个 x 的不可变引用 &T
RefCell 并没有逃离 Rust 的 borrow checker, 只是让检查推迟到了
运行时而非编译时
Rc<RefCell<T>> 可以得到支持多 owner 的可变引用, 但很不安全,
容易产生循环引用, 造成引用计数到达不了 0 而内存泄漏
Weak<T>
引用计数可以分为两类, strong 和 weak,
由引用是否有值的 ownership 决定.
strong 计数表示, 有多少引用是拥有值的 ownership 的.
weak 计数表示, 有多少计数是单单一个 handle 的.
当 strong 计数到达 0 的时候, 值被解构.
换言之, 值的解构和 weak 计数无关.
典型例子就是循环链表, 如果 tail 到 head 的引用是 strong 引用,
那么这个链表中元素的 strong 计数永远不会到 0, 也就出现了内存泄漏.
但是链表暴露的应当只有 head,
所以 tail 到 head 的引用也只是一个 handle, 不应当是 strong 引用.
如上, tail 到 head 的引用就是 weak 引用, 用 std::rc::Weak<T> 表示
和其他的智能指针不同, Weak 不拥有其指向值的 ownership.
方法有
let wk = Weak::new(x)
创建
如果想要使用 wk 指向的值, 必须现将其转为 strong 引用, 防止野引用
match wk.upgrade() {
Some(rc) => println!("{:?}", *rc),
None => println!("the value wk points to is no longer here!")
}
当然, strong 引用也可以转为 weak 引用, 使用
Rc::downgrade(this: &Rc<T>) -> Weak<T>
Deref trait
自定解引用算子的行为. 要求实现
type Target;
fn deref(&self) -> &Self::Target;
最初 Rust 中引用不能直接访问数据, 必须使用 * 来解引用才行, 如
fn are_same<T: PartialEq + std::fmt::Debug>(x: T, y: T) {
println!("are_same {:?} {:?}: {}", x, y, x == y);
}
let x = 2;
let y = &x;
are_same(x, y); // error: {integer} expected, found &{integer}
are_same(x, *y); // ok
are_same(&x, y); // ok
实现了 Deref trait 的智能指针也能被解引用
let z = Box::new(x);
are_same(x, z); // error
are_same(x, *z); // ok. Box<i32> derefs to i32
are_same(&x, z); // error
这里 *z 实际上是 *(z.deref())
Rust 在一些情况下可以自动帮我们添加解引用算子和取指针算子, 简化语法
称为 dereference coercion, 如将
1. &T to &U where T: Deref<Target=U>
2. &mut T to &mut U
2. &mut T to &U
不这样做的话就会出现反面例子如 nginx 的 void ****conf_ctx
(在 nginx-1.14.0/src/core/ngx_cycle.h:40)
Drop trait
智能指针用来解构自己的同时解构自己指向的数据. 要求实现
fn drop(&mut self)
这个方法在指针离开作用域时自动调用, 而且不能手动调用
另外, 有时候可能需要提前调用 drop 函数, 如提前释放一个锁,
一种方法是使用 std::mem::drop, 如
drop(early_lock)
模块化代码 crate
Rust 中也有模块, 创建如
$ cargo new mymodule --lib
这里 --lib 表示创建一个库而非可执行的项目.
当然可执行的项目中也可以也有模块.
定义模块使用 mod 关键字
mod name {
// definitions
}
可以嵌套模块, 如
mod network {
fn foobar() { }
mod far {
fn foobar() { }
}
}
模块或者子模块的顶层源文件中可以导入其他文件的内容,
只需要一行 mod name; 如
// lib.rs
mod sub;
fn foobar() {
println!("This is mymodule::foobar()");
}
// sub.rs. 这里不需要 mod sub { ... }
fn foobar() {
println!("This is mymodule::sub::foobar()");
}
子模块如果希望有自己的子模块, 如 top::sub::sub2,
那么其应当被放到一个单独的目录下, 子模块的顶层文件是 mod.rs, 如
// lib.rs
mod sub;
fn foobar() {
println!("This is mymodule::foobar()");
}
// sub/mod.rs
fn foobar() {
println!("This is mymodule::sub::foobar()");
}
mod sub2;
// sub/sub2.rs
fn foobar() {
println!("This is mymodule::sub::sub2::foobar()");
}
简单地, 若子模块没有它自己的子模块, 则放到 sub.rs 不然就 sub/mod.rs
cargo 默认只检查 lib.rs,
如果有其他文件, 需要使用如上方式在 lib.rs 中提及
模块中所有符号都默认是 private, 只有加上 pub 才能被模块外代码使用, 如
// lib.rs
pub mod sub;
pub fn foobar() {
println!("This is mymodule::foobar()");
}
使用模块的符号
首先需要导入模块的 crate, 通常如
// main.rs, in the same directory as lib.rs
extern crate mymodule;
如果希望使用 mymodule::sub::sub2::foobar(), 可以直接使用
mymodule::sub::sub2::foobar();
也可以使用 use 语句
use mymodule::sub::sub2;
sub2::foobar();
或者
use mymodule::sub::sub2::foobar;
foobar();
以及
use mymodule::sub::sub2::foobar as fb;
fb();
use 语句可以导入多个符号, 如
use mymodule::sub::{sub2, foobar};
或者
use mymodule::sub::*;
use 开始的地方是当前模块,
所以可以直接使用兄弟姐妹的符号
use sibling::foobar;
而不是
use super::sibling::foobar();
子模块使用父模块的符号, 可以
// sub/sub2.rs
fn parent_foobar() {
use super::foobar;
foobar();
}
可以从顶层模块开始导入, 如
// sub/sub2.rs
fn root_foobar() {
use ::foobar;
foobar();
}
高级 crate // TODO
profile 包含构建的各种默认选项. 在 Cargo.toml 中设定, 如
[profile.dev]
opt-level = 0
[profile.release]
opt-level = 3
容器 // TODO
容器的数据都存储在堆上
Vector
let mut v1: Vec<i32> = Vec::new();
let mut v2 = vec![1, 2, 3];
v1.push(3);
let v12 = &v1[2]; // &i32
let v12_ = v1.get(2); // Option<&i32>
for i in &v1 {
println!("{}", i);
}
for i in &mut v1 {
*i += 20;
}
String
let s1 = String::from("Hello World");
let mut s2 = "hi!".to_string();
s2.push_str("bad");
s2.push('!');
let s3 = format!("{}, {}", s1, s2);
HashMap
use std::collections::HashMap;
let mut map = HashMap::new();
map.insert(String::from("Age"), 12);
let mut map2: HashMap<_, _> = vec![1, 2, 3].iter().zip(vec![5, 6, 7].iter()).collect();
insert 是要取得 ownership 的, 除非传入 ref
for (k, v) in &map {
println!("{}, {}", k, v);
}
match map.get("key") {
Some(val) => // deal with val,
None => // ...
}
如单词计数
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0); // 返回 &mut xxx
*count += 1;
}
错误处理 // TODO
错误分成可恢复和不可恢复的
不可恢复的使用 panic
panic!("message");
如果需要报错的 backtrace, 应当
$ RUST_BACKTRACE=1 cargo run
可恢复的错误通常使用 enum Result<T, E> { Ok(T), Err(E) } 结合 match 完成
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => {
panic!("There was a problem opening the file: {:?}", error)
},
};
这个可以简单地写成
let f = File::open("hello.txt").expect("error opening file");
如果当前函数上下文中不知道如何处理某错误, 可以向上传播
就是把当前函数的返回值改成 Result<T, E>
这个通常可以简写成
fn read_hello() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?;
// ...
}
也可以链接
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
测试 // TODO
测试函数是有 test 属性的
#[test]
fn xxx() { ... }
其中通常使用 assert!(), assert_eq!(), assert_ne!(), panic!()
如 assert_eq!(x1, x2, "x1 does not equal to {}", "x2");
另外可以用 should_panic 属性来让一个测试函数通过, 如果该函数 panic
使用 ignore 属性来让昂贵的测试默认不运行, 除非加上 --ignored
测试使用
$ cargo test # 并行运行所有测试
$ cargo test -- --test-threads=1 # 串行运行测试
$ cargo test -- --nocapture # 对于通过的测试也显示 stdout
$ cargo test NAME # 只运行 tests::*NAME* 函数 (名字中包含 NAME 的函数)
宏
Rust 中宏的处理是在 AST 构建完成之后进行的, 因此必须是 disciplined
宏展开之后, 是一个 AST 子树, 而且必须是
* 模式
* 语句
* 表达式
* items
* impl items
这样可以防止奇怪的优先级问题
宏事实上是一个语法拓展, 不属于 Rust 语法的一部分
使用宏的方法一般是
$name ! $arg
其中 $arg 是单个的 token tree (使用 "[]", "{}", "()" 组合起来的一系列 token)
因此可以写 println!(...) 也可以写 println!{...}
定义宏格式为
macro_rules! $name {
$rule0 ;
$rule1 ;
// ...
}
其中每个 rule 格式类似
($pattern) => { $expansion }
如
macro_rules! four { () => { 1 + 3 } ; }
甚至可以
macro_rules! four { (NORMAL) => { 1 + 3 } ;
(BAD) => { 2 + 3 } ;
(FUCKING CRAZY) => { 1 - 3 } ; }
fn main() { println!("{} {} {}", four!(NORMAL),
four![BAD], four!{FUCKING CRAZY});
// 4 5 -2
}
匹配宏的时候是按照从上到下第一个能匹配的来的,
如果匹配失败就会 panic 而非继续尝试下面的 rule
宏当然可以捕获参数, 单个捕获的格式是
$ident:kind
其中 kind 可以是 item (类似函数 / struct / module)
block ({ 之间的代码 }) 以及 stmt, pat, expr, ty, ident, path, tt
如
macro_rules! max3 {
($a:expr, $b:expr, $c:expr) => {
if $a > $b { if $a > $c { $a } else { $c } }
else { if $b > $c { $b } else { $c } }
}
}
参数包括重复的时候, 基本捕获的格式为
$( REP_ITEM ) SEP REP
SEP 为分割符, 类似 , ,, ::
REP 为重复标记, 仅为 * (0或多个) + (至少 1 个)
REP_ITEM 为标记每个重复项的格式
在 $expansion 中也使用 $( ITEM ) REP 来完成重复
可以实现简单的 vec! 如下
macro_rules! myvec {
($($a: expr),*) =>
}
甚至可以是
macro_rules! foo {
($(FUCK),+) => { println!("Stop FUCKING!"); } ;
($a:expr) => { println!("Got expr: {}", $a); } ;
}
fn main() { foo!(FUCK, FUCK); foo!(FUCK); foo!(254); foo!("bad"); }
并行
线程模型有 1:1 (语言线程对应一个内核线程), 如 pthread.
也有 m:n 在一系列内核线程上复用多个语言线程, 如 goroutine, 称为 green thread.
Rust 为了保证最小的 runtime, 使用 1:1 线程.
创建线程使用 thread::spawn(), 如
let handle = thread::spawn(|| {
for i in 1..10 {
println!("Hi number {} from spawned", i);
thread::sleep(Duration::from_millis(1));
}
});
这里 handle 可以用来执行 join 操作. 当 handle 被解构时, 子线程被自动 detach.
线程可以有返回值, 使用 join 来取得线程返回值, 或者等待线程执行完成
match handle.join() {
Ok(t) => println!("{:?}", t),
Err(_) => println!("thread join failed"),
}
线程之间的执行顺序是不确定的, 因此如果某个线程希望使用另外线程的数据,
一般需要先取得 ownership, 如
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("{:?}", v);
});
handle.join().unwrap();
Rust 的线程间通讯可以使用 channel 来完成, 类似 Golang
创建 channel 如
use std::sync::mpsc; // multiple producer, single consumer
let (tx, rx) = mpsc::channel();
有多个发送者时使用 mpsc::Sender::clone
let tx2 = mpsc::Sender::clone(&tx);
之后发送使用 tx.send()
match tx.send(val) {
Ok(()) => println!("sent val"),
Err(e) => println!("{}", e),
}
发送操作可以在不同的线程中执行, 就如不同的 goroutine 通过 chan 通信
接收使用 rx.recv(), 如
let recv_val = rx.recv().unwrap()
recv() 是阻塞的, 使用 try_recv() 来非阻塞的检查接受
match rx.try_recv() {
Ok(val) => println!("recved {}", val),
Err(e) => println!("{}", e),
}
rx 对象可以使用 for 遍历, 直到 chan 被关闭
for v in rx {
println!("rx got {}", v);
}
当所有 tx 被解构时, chan 自动被关闭
除了 channel, 也可以有线程共享的资源, 如全局变量.
这些资源需要被 Mutex<T> 保护.
如一个被多个线程使用的全局计数器实现为
let counter = Arc::new(Mutex::new(0));
Mutex 提供了 interior mutability 因此这里不需要 mut
Rust 强制要求使用被 Mutex 保护的资源前, 先用 lock() 取得锁.
lock() 实际返回的是一个智能指针, 可以像访问其他智能指针一样访问它
注意取得锁的先决条件时拥有这个锁的 ownership.
let counter = Arc::clone(&counter);
let h = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
上面使用了 Arc<T>. Arc 是 Rc 的多线程版本, 基本接口和 Rc 相同.
它使得 counter Mutex 能同时有多个 owner,
并且在引用计数到达 0 的时候被解构.
一个类比可以看成, Arc <=> Rc, Mutex <=> RefCell.
而且使用 Mutex 的时候要注意可能产生死锁.
Send trait 是一个 marker trait,
指定某类型值的 ownership 可以在不同线程之间发送 / 共享.
Sync trait 也是 marker trait,
指定不同的线程可以同时持有该类型的值的引用.
这两个 trait 是自动实现的, 手动实现是不安全的.
Supertrait
类似 trait 的继承, 通常用来要求实现某个 trait 的值也必须实现另外某个 trait.
如实现 Copy 必须实现 Clone 一样.
格式如
trait Speaker {
fn speak(&self);
}
impl Superspeaker {
fn superspeak(&self) {
for _ in 0..10 { self.speak(); }
}
}
Unsafe // TODO
用来避免编译时的静态检测. 通常完成对内存的高级操作.
对裸指针解引用
裸指针类型如
*const T
*mut T
裸指针不会被 borrow checker 检查,
但是不保证有效, 也不完成自动解构.
对裸指针的解引用是不安全的, 但是创建它们还是安全的
创建裸指针如
let mut v = 5;
let r1 = &v as *const i32;
println!("{:?}", r1);
unsafe { println!("{:?}", *r1); }
以及
let r2 = &mut v as *mut i32;
unsafe { *r2 = 15; }
println!("{:?}", r2);
unsafe { println!("{:?}", *r2); }
裸指针允许同时有 mut 和 const 指向统一块内存区域
调用不安全的函数
在一个安全的函数中用 unsafe 来包装不安全的代码或者调用
不加锁地修改全局变量
使用 static 可以创建全局变量
static GLOBVAR: &str = "Hello World";
实现 unsafe trait
最好应该把 unsafe 的 API 用 safe 的方式包装起来提供出去
常见操作
类型转换
let a = 5;
println("{}", a as f64);
字符串转数字
let v: i32 = match numstr.parse() {
Ok(v) => v,
Err(e) => panic!(e),
};
必须指定 parse() 的类型