Rust 学习笔记
最近突然对于Rust产生了兴趣因为它的吉祥物小螃蟹很可爱,遂开始阅读The Rust PL一书想要一探究竟。我发现Rust真的是一门相当阳间的语言,一些functional programming的特性和build system可以让使用者用起来十分舒爽。
虽然目前只看了
两十章,但我已经从代码中嗅到了了浓郁的Rust的香锈味。
基本看完了现在。
在一个较为清闲的周末早上,我决定把最后一章看完。
创建时间:2021-01-09 21:10:07
Prologue
这篇博文是的主要内容是给我自己看的,用以弥补我孱弱的记忆力,其中大多数是看The Rust PL过程中的笔记和代码,少部分是我自己的一些思考。正常人可以只看这个章节。
为什么我要学Rust
经过了一段时间的使用,我发现Rust娘的脾气如下:
- 哪里不对点哪里
- Rust不像一个喜欢无理取闹的女朋友一样,觉得你做错了还憋着不说,硬是让你问:“我这里是不是做的不对?我那里是不是做错了?”;
- C这种女朋友即使你觉得自己哪哪都没错,她也不说你做错了,但是偏偏就是怎么也不work;
- 但是Rust不一样的地方在于,会明确的指出你哪里惹她不开心了
虽然她会经常经常不开心; - 即使Rust娘对你不爽了,她也不会弃你不顾,而是会贴心的给出各种help,陪你解决问题
实际上更多时候可能是谷哥解决的。
- 牛逼的各种属性
- Closure,虽然似乎对于生成closure的参数的生命周期有一定的限制
match
: 帮你不漏过每一个小角落?
- 语法相对自由,检查绝壁严格
Toolchain, Builder and Cargo
Rust的安装还是十分方便舒爽的,官网提供了非常方便的小脚本来装上它的一整套工具链,包括compilerrustc
和cargo
等等。类似cargo
,python-pip
这种依赖管理的方式真可谓是十分的阳间,不知道这种天才的idea是谁第一个发明的。Rust作为一个能够做System Programming的语言有这一套Cargo简直可以碾压其他这一领域的爷爷们了。而且还提供了黑魔法一般的Web Based Document,只需要运行cargo doc --open
即可打开浏览器查看该项目所有的dependencies(在toml
中指定的版本)的document,真可谓是十足的方便。
对于编译中摸不到头脑的奇奇怪怪的错误,它会告诉你一个错误代码,比如E0061
。然后跑一下rustc --explain E0061
,就可以更进一步的了解这货到底是啥玩意,Rust的开发者真是怕你不会用就差开个任意门到你脸上教你写Rust了。这种无微不至对于user的人文关怀真的让人倍感舒适。
Style
随便放一段第二章中完成的代码感受一下!
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
// 这里用到了rand库中更新版本的API,原书中`gen_range`是take两个参数的。
// rand = "0.8.1"
let secret_number = rand::thread_rng().gen_range(1..101);
println!("The secret number is: {}", secret_number);
loop {
println!("Please input your guess");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
// expect("Please type a number");
println!("You guessed: {}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
简单说一下
mut
来区分mutable和immutable的变量- 一个变量名可以被shadow:这个名字所对应的新的变量可以拥有一个不同的类型和值
match
这种functional programming中流行的玩意似乎真的是可以玩出花来,并且字里行间都有函数式编程的形状- 函数调用返回
Ok
或者Err
,感觉是比较麻烦的事情,但确实更加安全了 ::
还是相当多的- Rust是statically typed lang,但是可以看出来它的type inferencer似乎相当强大
Rust的设定
Copy
为了防止double free之类的错误,rust对于所有编译时大小未知的变量(以及没有implementCopy
trait的变量)在形如这样的assignment时都是只进行shallow copy的(书中的说法是move)。这样做的后果是在之后所有的s
都无法被正常使用。为了避免使用runtime gc的性能损耗,Rust在离开一个变量的scope(简单地说就是花括号包住的地方里)的时候就会彻底地释放掉它(在heap上清除)。因此如若s
和s2
都能够正常使用但是他们却指向了同一个heap中的地址,那么就会在离开这个scope的时候被double free从而产生安全隐患。因此可以使用clone
方法来创建deep copy。
let s = SomeType;
let s2 = s;
Ownership
Rust这个奇葩在传递参数的时候也会传递Ownership。Ownership简单地说就是这个变量属于哪个函数。这种传递是否为deep copy和上面一小节中assignment的规律是一样的。这样就会导致一些奇奇怪怪的问题:
fn main() {
let s = String::from("hello");
takes_ownership(s);
let x = 5;
makes_copy(x);
//若想编译通过,下面这个print不能要
//否则会有error: value borrowed here after move
// println!("{}", s);
println!("{}", x);
let s = String::from("hello2");
//
let s = takes_and_gives_back(s);
println!("{}", s);
}
fn takes_ownership(some_str: String) {
println!("{}", some_str);
}
fn makes_copy(some_int: i32) {
println!("{}", some_int);
}
fn takes_and_gives_back(some_str: String) -> String {
println!("{}", some_str);
//return
some_str
}
因为s
在传递给takes_ownership
时也会给出ownership,故而在函数return的时候ownership就么的了,然后这个String的memory便会被free掉。而与之相对的,x
能够在调用完以之为参数的函数后继续使用。
然而,以下代码可以获得正确的输出:
fn main() {
let s = "hello";
takes_ownership(s);
println!("{}", s);
}
fn takes_ownership(some_str: &str) {
println!("{}", some_str);
}
因为在这里s
并非是String
,而是str
,其是string literal,被hardcoded into text。
Call by reference
fn main() {
let s1 = String::from("hello");
let len = calculate_len(&s1);
println!("{}, {}", s1, len);
}
fn calculate_len (s: &String) -> usize {
s.len()
}
Rust似乎并不像C一样在每一次dereference的时候都需要使用到*
来dereference。Stackoverflow上的这个问题也部分的解释了我的疑惑:Rust在调用method的时候会自动地加上&
,&mut
或*
来match方法的signature。
Multiple borrows
如何做到在参数传递的时候不交出ownership呢?可以传递一个reference给函数。如上面的代码写的,s
在函数calculate_len
存储了一个指向s1
的指针。在Rust中,这种参数传递方式称为borrowing。
不过以这种方式传递的参数有一个缺点:数据是immutable的。可以使用另一种ref:
fn main() {
let mut s1 = String::from("hello");
let len = calculate_len(&mut s1);
println!("{}, {}", s1, len);
}
fn calculate_len (s: &mut String) -> usize {
s.push_str(" world");
s.len()
}
然而,如果两个mutable borrow发生在不同的scope里则是允许的
let mut s = String::from("hello");
{
let r1 = &mut s;
}
let r2 = &mut s;
Rust为了防止data race,禁止创建两个同样的mutable borrow。immutable borrow可以有无限多个,但是此时如果出现了mutable references的所有use在它的scope里都在一个mutable reference之前的话则可以compile)。感觉这些设定基本都是为了避免多并发时可能出现的一些bug。
Slice
和python类似,Rust里面的String也可以被slice。这种slice是为了避免变量被drop之后index还存在的尴尬事。
let s = String::from("hello");
let s1 = &s[0..2];
// let s1 = &s[..2];
将之前的功能重新用slice来实现。slice将作为&str
返回。
fn main() {
let s1 = String::from("hel lo");
let len = first_space(& s1);
//s1.clear();
//error because clear() will need a mutable reference to truncate the String
println!("{}, {}", s1, len);
}
fn first_space(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s
}
fn first_space(s: &str) -> &str
这样的函数原型的参数也可以是String
。
Slice这种机制也可以运用在其他类型的array中。
Struct
struct User {
username: String,
email: String,
active:bool,
sign_in_count: u64,
}
fn main() {
let user1 = User {
email: String::from("[email protected]"),
username: String::from("user1"),
active: false,
sign_in_count: 1
};
}
- 结构体可以像这样被定义和实例化。Rust也提供了额外的语法糖来简化一些参数:
fn init_user(email: String, username: String) -> User { User { //email: email, email, //username: username, username, active: false, sign_in_count: 1, } }
- Rust还允许从一个结构体创建另一个:
let user2 = User { username: String::from("user2"), ..user1 };
-
String会被deep copy吗?
-
Tuple也可以被作为Struct来使用:
struct Color(i32, i32, i32); let blk = Color(0, 0, 0);
- Struct中data fields的ownership归实例化的变量。
- Struct中可以有对于其他data的reference,但是这要求specify lifetime。
- 可以有不含任何data的struct(unit-like Struct)
Debug output
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rec1 = Rectangle {
width: 30,
height: 50,
};
println!("rec1 is {:#?}", rec1);
}
Rust对于Struct的格式化输出可以借助#[derive(Debug)]
来实现。这种用法有些类似Python中的装饰器。用官方的说法是这里derive了一个Debug
trait。具体是什么东西还得往后看看才明白。
Method
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, sub_rec: &Rectangle) -> bool {
self.width > sub_rec.width && self.height > sub_rec.height
}
fn new_square(edge: u32) -> Rectangle {
Rectangle {
width: edge,
height: edge,
}
}
}
fn main() {
let rec1 = Rectangle {
width: 30,
height: 50,
};
let rec2 = Rectangle {
width: 20,
height: 45,
};
let square = Rectangle::new_square(30);
println!("rec1 is {:#?}, area is {}", rec1, rec1.area());
println!("rec1 can hold re2? {}", rec1.can_hold(&rec2));
}
可以implement一个area方法。和Python中的方法类似,这里Struct的方法的第一个参数也是&self
。因为area
被实现在impl Rectangle {}
内部,所以&self
的类型不需要被指定。Ownership同样也可以在这里被方法拿走。
和C/C++不同的是,Rust并没有用->
来调用对象(当其为指针时)的方法或者取对象的成员。Rust会自动添加&
, &mut
, *
来match函数的signature。个人感觉这是一个很方便的东西,而且看上去比C要清爽很多。这个feature被称为automatic referencing and dereferencing。
不过这样的话,感觉需要多多在函数定义的时候注意到底参数是什么类型的。可能只会给Object(a.k.a self)去自动加?其他的参数似乎并不会。
Associated Functions
不把self
作为参数之一的方法。如果我没有记错的话,这种在Python中被称作类方法?Rust中被称为associated function。通常被用来return一个新的对象。语法参考上面的代码。
此外Rust还支持对于同一对象的多个impl
代码块,这似乎是为了trait和泛型设计的。
Enum
Rust的enum可以按照这种方式来定义:
enum Message {
// unit
Quit,
// anonymous nested struct
Move {x: i32, y: i32},
// String included
Write(String),
// 3 * i32 included
ChangeColor(i32, i32, i32),
}
impl Message {
fn call(&self) {
//
}
}
可以看出其支持在enum里面的不同variants有着不同的nested data types。这倒是一个十分方便的feature。
Option
为了避免C/C++中NULL
引起的一些问题,例如null pointer dereference之类的,Rust引入了Option枚举类。
enum Option<T> {
Some(T),
None,
}
这有些类似于functional programming中monad的思想。这样便允许了可以为空的fields。
那么如何把Option里面的东西取出来呢?那就要用到牛逼的match了
fn main() {
let x = Some(3);
let x = plus_one(x);
println!("x is {:?}", x);
let y = None;
let y = plus_one(y);
println!("y is {:?}", y);
}
fn plus_one(num: Option<i32>) -> Option<i32> {
match num {
Some(v) => Some(v+1),
None => None,
}
}
PS:在match里面,_
可以去match任意值。也可以用if let
来match一种case,这个时候Rust不会check是否有其他的case没有被match到。
if let Some(3) = num {
println!("Lucky!");
}
Rust包
- 通过
cargo new --lib [libname]
来创建一个Rust的lib - 在
src/lib.rs
下面可以通过mod
关键字创建多个module - module之间可以嵌套,可以包含enum, struct, function,这些都可以是public的
- 所有的
fn
,mod
,enum
,struct
都是默认private的,父类无法直接访问到子类但是反之则可 - 可以使用相对或者绝对“路径”来访问module里面的内容
impl
里面的东西可以access被impl的对象的private fielduse
基本等同于Python中的import
,也可以使用use A as a
pub use
可以被用来提升命名空间?ref- 也有
use [namespace]::*
的用法,和Python类似 - nested path
Rust提供了一个语法糖:
// original use std::cmp::Ordering; use std::io; use std::io::Write; // nested use std::{cmp::Ordering, io}; use std::io::{self, Write};
分割到不同文件
Rust通过把module里面的sub module放在root下的其他文件/文件夹中来完成module的嵌套。Rust的文件名即是其所在module的名字。
以下为在单个文件里面的module结构:
//#[cfg(test)]
mod back_of_house {
// #[derive(Debug)]
pub struct Breakfast {
pub toast: String,
seasonal_fruit: String,
}
impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
use crate::front_of_house::hosting;
// or use relative path
// use self::front_of_house::hosting;
pub fn eat_at_restaurant() {
// abosulute path
crate::front_of_house::hosting::add_to_waitlist();
// relative path
front_of_house::hosting::add_to_waitlist();
// effective after use
hosting::add_to_waitlist();
let mut meal = back_of_house::Breakfast::summer("Rye");
meal.toast = String::from("Wheat");
println!("{} toast pls!", meal.toast);
}
其中,front_of_house
可以通过这种重构实现:
src/lib.rs
rootmod front_of_house;
src/front_of_house.rs
pub mod hosting { pub fn add_to_waitlist() {} }
或者可以对hosting
进行更进一步的拆分:
src/front_of_house.rs
pub mod hosting;
src/front_of_house/hosting.rs
pub fn add_to_waitlist() {}
Rust Collections
Rust提供了一堆方便的数据结构。
Vector
这里的Vector其实基本上类似于可变长度的数组,定义与Pie language里面的vector比较类似。
可以通过vec!
宏或者Vec::new()
初始化。
因为vector内部的实现问题,mutable vector在长度变化的时候有可能会被挪动到别的内存(在vector变长的时候原本malloc的内存可能装不下),因此vector也遵循同一scope内不能有mutable和immutable reference的规则,即使immutable reference指向vector初始的几个元素。
还可以用引用和get()
方法来拿到vector中的元素。其处理的方式会有些许不同:
fn main() {
let mut v = Vec::new();
v.push(5);
v.push(6);
// panic if out of bound
let third = &v[2];
// will get an Option<T>
match v.get(1) {
Some(val) => println!("get {}", val),
None => println!("not even get!"),
}
}
Iterate a vector
和Python的语法类似,和C的语义类似,可以用指针来迭代vector:
for i in &mut v {
*i += 10;
}
但是如若像这样使用迭代器则会报错:
for mut i in v {
i += 10;
}
for j in v {
println!("{}", j);
}
报错:
error[E0382]: use of moved value: `v`
--> src/main.rs:12:14
|
2 | let mut v = Vec::new();
| ----- move occurs because `v` has type `Vec<i32>`, which does not implement the `Copy` trait
...
8 | for mut i in v {
| -
| |
| `v` moved due to this implicit call to `.into_iter()`
| help: consider borrowing to avoid moving into the for loop: `&v`
...
12 | for j in v {
| ^ value used here after move
|
note: this function consumes the receiver `self` by taking ownership of it, which moves `v`
这是因为在把给了迭代器的时候传递了ownership,导致v在后续的scope内失效了。看来如无必要迭代器还是得borrow。
如若想要把不同类型的元素存储在同一个vector里面,可以用它来存储一个enum类型,这个enum类型里面包括不同类型。
String
和之前的代码中出现的api类似,可以通过::new()
, ::from()
, .to_string()
等方法创建一个String
。
String
的api还包括push_str()
, push()
, +
运算符的重载。要说明的是这些运算并不会take ownership。
还有format!()
宏可以使用:
let s1 = String::from("tic");
let s = format!("{}-tok", s1);
s
也是一个String
。
Rust中String其实是Vec<u8
。但是其支持utf-8编码,因此Rust并不能像C一样直接index一个String。因此可以使用slice的方式去拿sub elements,但如若卡在了char boundary上面的话会报错。
可以用.chars()
或者.bytes()
方法来生成一个可迭代的对象,前者会划分为一个个unicode,而后者则是u8
。
Hashmap
看如下代码,HashMap
不像vector一样有用来初始化的宏,但是它可以从两个vector来初始化。
use std::collections::HashMap;
fn main() {
let mut scoreboard = HashMap::new();
scoreboard.insert(String::from("A"), 20);
scoreboard.insert(String::from("B"), 30);
println!("{:?}", scoreboard);
let teams = vec![String::from("A"), String::from("B")];
let scores = vec![20, 30];
// must specify the type here
let mut scoreboard: HashMap<_, _> = teams.into_iter().zip(scores.into_iter()).collect();
println!("{:?}", scoreboard);
// get an Option<T> here
let team = String::from("A");
let s = scoreboard.get(&team);
// euivalent to
let s = scoreboard.get("A");
println!("{:?}", s);
for (k, v) in &scoreboard {
println!("{}: {}", k, v);
}
// insert if the Key has no value
scoreboard.entry(String::from("A")).or_insert(50);
scoreboard.entry(String::from("C")).or_insert(50);
// update the data
let score = scoreboard.entry(String::from("A")).or_insert(0);
*score += 100;
println!("{:?}", scoreboard);
}
需要注意的是,HashMap
的key值不能重复,如果重复的话则会update现有的key对应的value。那么便可以用entry().or_insert
来检查是否存在,如若不存在则插入。
这里entry()
是一个蛮神奇的存在,其会返回一个Entry
。如果key存在则会返回一个mutable reference,如果不存在则会创建一个mutable reference并返回。
也可以通过entry()
来获得对于HashMap
里面一个slot的mutable reference,从而进一步更新其中的数据。
Error
和其他语言不同的是,Rust并没有Exception handling这种机制。其将错误分为recoverable和unrecoverable。前者通过Result<T, E>
来处理,而后者则会在调用panic!()
宏的时候出现。
这个宏可以主动调用来使程序panic,还可以在运行时设置RUST_BACKTRACE=1
来在程序panic的时候backtrace。
Result的定义如下:
enum Result<T, E> {
Ok(T),
Err(E),
}
用match
来对result处理是Rust的常规操作。骚操作呢?也不是没有:unwrap_or_else()
,后面会讲到,先卖个关子。如果懒得用match的话可以直接unwrap()
。这样如果没有错误便会直接返回Ok
里面夹着的东西,反之则panic。也可以用expect()
来在其参数里面填上其他的描述来实现更友好的错误输出,except()
除了接收一个参数外和unwrap()
一毛一样。
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("./src/hello.txt");
let f = match f {
Ok(file) => file,
Err(err) => match err.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(f) => f,
Err(err) => panic!("create file failed {:?}", err),
},
other => {
panic!("unexpected error: {:?}", other)
}
},
};
println!("open file successfully! {:?}", f);
}
错误传递
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(e) => return Err(e),
};
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
这段简单的代码可以返回一个Result
。而Rust提供了一个更简单的操作符, ?
???。用这个操作符,函数可以写成:
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
?
所做的事情几乎和之前写的match一毛一样,区别在于它会调用standard library里面的From
trait来把错误的类型转换为函数定义的return type中的错误类型。(难道matcch
在return的时候没有做类型转换吗?)但是这里还有一处改变,f
被定义为了mutable。这里令我十分无法理解,为什么在前面的code中immutable f
可以正常运行????read_to_string
的定义是fn read_to_string(&mut self, buf: &mut String) -> Result<usize>
,很显然需要一个mutable self。但是这tm居然能运行???我十分不解这是为什么。
破案了,因为上一个f被shadow了。。。我没发现f被定义了两次!!!
不过上面的任务有一个更简单的写法:
fs::read_to_string("hello.txt")
这里应该是使用了类似于类方法的东西,并不需要我们创建一个File就可以调用类的方法了。但是这个signature是这样的:pub fn read_to_string<P: AsRef<Path>>(path: P) -> io::Result<String>
,可见返回值并不是像之前看到的Result<T, E>
一样。因为这里面不知道为啥没法直接看到它的定义,但是猜测是封装了后者。
Generic Types, Traits, and Lifetimes
泛型以及其相关内容。本章会讲述形如这样的代码到底说了什么:
use std::fmt::Display;
fn longest_with<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
where
T: Display,
{
println!("announcement {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}
Generic Types
Struct
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
impl Point<f32> {
fn distance(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
struct PowerfulPoint<T, U> {
x: T,
y: U,
}
impl<T, U> PowerfulPoint<T, U> {
fn mixup<V, W>(self, other: PowerfulPoint<V, W>) -> PowerfulPoint<T, W> {
PowerfulPoint {
x: self.x,
y: other.y,
}
}
}
这里实现了结构体内的泛型。枚举类的泛型类似。有几点需要说明:
- 如果要实现泛型结构体的函数,
impl
后面要加上泛型参数<T>
- 那么也可以实现非泛型结构体的函数:为某一特定类型实现
- 实现的函数也可以具有不同与泛型结构体的泛型参数,如
PowerfulPoint
的实现 - 注意
x()
在这里的返回是一个引用。如果不是引用则会报错:cannot move out of
self.xwhich is behind a shared reference
- 其实应该是有办法的,如果
T
implement了Copy
这个trait的话。但是Rust现在不知道有没有这个trait,所以我们姑且先返回引用
Function
将这个函数变成泛型的版本:
fn largest(list: &[i32]) -> &i32 {
let mut largest = &list[0];
for n in list {
if n > largest {
largest = n;
}
}
largest
}
然而简单地加上泛型参数并不能使之编译:
fn largest<T>(list: &[T]) -> &T {
let mut largest = &list[0];
for n in list {
if n > largest {
largest = n;
}
}
largest
}
这是因为并非所有的type都可以去比大小。这个T
必须实现了偏序的trait。
改写成如下形式能够编译通过:
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &n in list {
if n > largest {
largest = n;
}
}
largest
}
这里的语法细节将在下面进一步讨论。
编译
Rust的泛型是通过在编译时把不同使用到的泛型都编译出来实现的,所以性能开销很小。比如foo<T>()
如果被传入了i32
和u8
,那么其在编译时便会被转变成两个函数:foo_i32()
和foo_u8()
,从而减少泛型引起的性能开销。
Trait
简单地说trait就是让编译器知道一些类型有着一些共有的功能。Rust说这货和接口比较像。
Define a trait
这里我们定义一个叫做Summary
的trait。注意函数的Signature以;
结尾。一个trait里面可以有多个函数,而每个类型若想实现一个trait必须自己拥有每一个trait里面的函数的实现。
// src/lib.rs
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct News {
pub headline: String,
}
impl Summary for News {
fn summarize(&self) -> String {
format!("{}", self.headline)
}
}
pub struct Tweet {
pub username: String,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}", self.username)
}
}
// src/main.rs
mod lib;
use lib::Summary;
fn main() {
println!("aaa");
let n = lib::News{
headline: String::from("headline_A"),
};
println!("{}", n.summarize());
}
Rust的trait遵循coherence property (orphan rule),即trait的implementation要么在trait里,要么在类里(类或者trait的crate是local的)。反之,假设在一个奇奇怪怪的地方(e.g.自己的lib)定义了比如Vec<T>
实现了Display
,但是这俩玩意都是来自stdlib里面的东西,对于自己的lib而言都是extern的,那么这样是不行的。这样做是为了防止trait被重复实现。除此之外也有安全的考虑:如果这些extern的trait和类可以被重新实现,那么即意味着任何在项目中使用的crate都可以劫持这些实现。
若要使用刚刚写的类和trait,可以切换到main.rs
下面去先mod lib
,然后use lib::Summary
。注意这里lib
其实可以是任何模块名(文件名),只要和src目录下的rs源文件名相同即可。而若要调用summarize()
这个来自Summary
trait的函数,则必须use它。
如同接口中所能做的一样,也可以将trait实现:
pub trait Summary {
fn summarize(&self) -> String {
String::from("Unimplemented Summmary")
}
}
pub struct News {
pub headline: String,
}
impl Summary for News {
}
这里在调用news.summarize()
时则会直接找trait定义里面的实现。但是必须impl Summary for News
才可能进行这个调用,即使impl
里面为空。这也有些类似类里面(虚)函数的重载。
mod lib;
use lib::Summary;
// also works here if fn notify(item: &Summary)
fn notify(item: &impl Summary) {
println!("Notification: {}", item.summarize());
}
fn main() {
println!("aaa");
let n = lib::News{
headline: String::from("headline_A"),
};
notify(&n);
}
- 定义在trait里面的函数可以调用这个trait中的其他函数,即使后者没有在trait里面实现
- 可以定义函数其参数必须实现一个trait从而不指定具体的类型
- 上面代码函数signature中
impl
其实是语法糖 - 原型应为:
fn notify<T: Summary>(item: &T)
- 可以要求参数同时实现多个trait吗?可以,使用
&(impl Trait1 + Trait2)
- 上面代码函数signature中
- 也可以要求返回值它实现了trait
- 当函数泛型有比较多的trait的约束时,可以用
where
关键字使其更清晰fn some_fn<T, U>(t: &T, u: &U) -> impl Summary where T: Summary + Display, U: Summary { lib::News{ headline: String::from("headline_A"), } }
目前
some_fn
只能return一种类型。即:它没法returnNews
或者News2
,即使两者都实现了Summary
。这与编译的实现有关。后面会讲到如何让它可以return不同类型的变量。我猜测这里是因为如果return了多种变量的话,编译器不知道在后续接收这个对象的代码中以何种方式调用对象的方法,因为似乎Rust中的trait并非是像虚函数那样实现的。
一开始的largest
函数也可以不需要Copy
或者Clone
这样的trait。这里它可以只返回一个slice:
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for n in list {
if *n > *largest {
largest = &n;
}
}
largest
}
Conditionally Implemented Method
我们也可以完成这个目标:当类中的泛型实现了某(些)trait时,为这个类实现一个方法。语法如下:
impl<T: Display + PartialOrd> Some_type<T> {
fn some_fn(&self) {
// do something
}
}
这里,当且仅当Some_type<T>
中的T
类型实现了Display
和PartialOrd
两个trait时,才为其实现some_fn
方法。
Lifetime
Rust和其他语言最不一样的地方应该属lifetime了。这里的lifetime指的是变量的lifetime。由于默认情况下所有变量在离开自己的scope之后便会被销毁,有些时候可能需要一些变量live longer。
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("{}", r);
}
在这段代码中,尝试编译会报错:
error[E0597]: `x` does not live long enough
--> src/main.rs:5:13
|
5 | r = &x;
| ^^ borrowed value does not live long enough
6 | }
| - `x` dropped here while still borrowed
7 | println!("{}", r);
| - borrow later used here
若想要实现和引用相关的函数,有时候也需要显示标注lifetime:
fn main() {
let s1 = String::from("str1");
{
let s2 = "s2";
let result = longest(&s1, s2);
println!("{}", result);
}
}
// fn longest (s1: &str, s2: &str) -> &str
// rasie error: expected named lifetime parameter
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() {
s1
}
else {
s2
}
}
这里因为longest
的参数s1
和s2
可能拥有不同的lifetime,导致return之后编译器不会玩了(不知道什么时候需要销毁变量)。因此我们这里需要显示的标注他们的lifetime。这里标注的意义是,s1
和s2
overlap的部分(或者说较小的那个?)即是return value的lifetime。标注之后,即使是如下的调用也是ok的:
fn main() {
let s1 = String::from("str1");
{
let s2 = "s2";
let result = longest(&s1, s2);
println!("{}", result);
}
}
因此,这样的调用会报错:
fn main() {
let s1 = String::from("str1");
let result;
{
let s2 = String::from("s2");
result = longest(&s1, &s2);
}
println!("{}", result);
}
lifetime类似于泛型参数,一般以'
+ 小写字母表示。类似类型,lifetime是不可以被更改的。
对于结构体和方法也可以标记lifetime:
struct Strstr<'a> {
part: &'a str,
}
impl<'a> Strstr<'a> {
fn level(&self) -> i32 {
42
}
}
实际上我们在真正写代码的时候并不需要那么多的annotations,这是因为Rust编译器引入了一些规则来帮助自动标记lifetime
- 所有的参数的lifetime都会被标记成不同的
- 如果只有一个参数,那么其返回的lifetime会被标记成和参数相同的
- 如果方法有
self
之类的参数,那么返回的lifetime和self
的相同
'static
除了上述的标记之外,还有一种特殊的lifetime标记:'static
。其意味着可以活到程序的终结。所有的string literal都具有这个lifetime。
Test
Rust提供了完善的测试功能。对于library,在创建的时候便会在lib.rs
里面生成test module。类似:
#[cfg(test)]
mod tests {
#[test]
fn exploration() {
assert_eq!(2 + 2, 4);
assert!(true);
}
#[test]
fn another() {
panic!("Fail!")
}
}
在需要作为测试函数的函数头上标记#[test]
,然后运行cargo test
即可自动去做test。而在module上面标记
#[cfg(test)]
可以实现对于一个模块的unit test。
除此之外,Rust还可以做panic的捕获,以及输出更加详细的测试信息。
Functional Rust
终于要快进到魔法Rust了。来这里体验一把Rust强大的函数式特性。
Closure
Rust里面的函数可以是一等公民,这即意味着函数可以作为参数和返回值。使用closure的范例如下:
use std::thread;
use std::time::Duration;
fn generate_workout(intensity: u32, random_number: u32) {
let expensive_closure = |num| {
println!("calulating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
if intensity < 25 {
println!("Do {} pushups!", expensive_closure(intensity));
println!("Do {} situps!", expensive_closure(intensity));
} else {
if random_number == 3 {
println!("Take a break");
} else {
println!("Run for {} mins!", expensive_closure(intensity));
}
}
}
这里expensive_closure
就是一个closure。其中|num|
表明了匿名函数的参数,而函数最后返回的也是num
。这个closure在之后可以被调用。
同样的,也可以对其标注类型:
let expensive_closure = |num: u32| -> u32 {
println!("calulating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
Memoization
可以看到最外层的if
内调用了expensive_closure
两次,而这其实是低效的。我们可以通过memoization来实现lazy evaluation。
为了实现memoization,构造一个Cacher
struct。它存储一个closure,上一次递交给它的参数和结果。注意这里用到了Fn
trait: where T: Fn(u32) -> u32
。这里Cacher
的实现并不完美。
struct Cacher<T> where T: Fn(u32) -> u32 {
calc: T,
val: Option<u32>,
arg: Option<u32>,
}
impl<T> Cacher<T> where T: Fn(u32) -> u32 {
fn new(calc: T) -> Cacher<T> {
Cacher {
calc,
val: None,
arg: None,
}
}
fn value(&mut self, argument: u32) -> u32 {
match self.val {
Some(val) => val,
None => {
let v = (self.calc)(argument);
self.val = Some(v);
v
}
}
}
}
Capturing the Environment
Rust中的closure中可以使用到当前scope里面有效的变量,比如这样:
fn main() {
let x = 4;
let eqx = |y| y == x;
eqx(4);
}
既然要和其所在的scope(Environment)产生物质交换,那么它便和函数一样有着Ownership的问题。Rust提供了三种对应的traits:
FnOnce
: take ownership,只能被调用一次(因为只能take一次ownership)FnMut
: 传递mutable borrows,因此可以改变environmentFn
: borrow不可变的值
fn main() {
let x = vec![1, 2, 3];
let eqx = move |z: Vec<i32>| z == x;
let y = vec![1, 2, 3];
let z = vec![1, 2, 3];
eqx(y);
eqx(z);
eqx(y);
println!("{:?}", x);
}
形如这样的代码会编译报一堆错:
x
的ownership被递交给了eqx
,在后面无法继续被使用y
的ownership也在使用的时候被递交给了eqx
- 可以继续使用z作为参数
Iterator
Rust内的Iterator trait定义如下:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
这里的type Item
是associated type,具体似乎后续会讲。简单地说Rust迭代器需要实现一个next
函数,而对于iter()
而言,其返回一个Iterator
。个人感觉似乎可以用dependent type中的理论来理解这个Item
,即表示Item
是一个类型。
这里便可以引入很多函数式编程中常用的函数,如map
,filter
等。用法如下:
fn main() {
let v1 = vec![1, 2, 3, 4, 5];
let it = v1.iter();
let v2: Vec<_> = it.map(|x| x + 1).collect();
let it = v1.iter();
let v2: Vec<_> = it.filter(|x| **x > 3).collect();
}
简单地说,map
把一个函数作为参数,然后apply这个函数;filter
会filter调所有返回值是false
的元素。它们都是再次产生迭代器的东西。
注意:
- 需要用
collect()
来完成真正的求值。因为Iterator
是lazy的 - 为什么
filter
里面需要两次dereference?`
实现Iterator trait
对原书中的例子修改之后的一个简单例子如下:
struct Counter {
count: i32,
}
impl Counter {
fn new(count: i32) -> Self {
Counter {
count: count,
}
}
}
impl Iterator for Counter {
type Item = i32;
fn next(&mut self) -> Option<Self::Item> {
self.count += 1;
Some(self.count)
}
}
fn main() {
let mut counter = Counter::new(10);
for i in (0..10) {
println!("{}", counter.next().unwrap());
}
}
从某种意义上讲,我们似乎可以通过这种方式来制造出一个stream。
Smart Pointer
杀马特指针
一些智能指针能够自动统计引用的数量,并且在引用数为0的时候自动清理掉。
Box<T>
Box
会帮你把数据分配在堆上。这样可以避免转移ownership时的拷贝。Box的语法简单如斯:
fn main() {
let b = Box::new(5);
println!("b = {}", b);
}
有没有什么是只有Box
才能做到的呢?有。有时候我们不知道需要在stack上面静态地分配多大的空间。cons list是函数式中常用到的recursive type,如果按照这种定义的话是会编译失败的:
enum List {
Cons(i32, List),
Nil,
}
甚至Rust还会贴心的提醒你:
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^ recursive type has infinite size
2 | Cons(i32, List),
| ---- recursive without indirection
|
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable
|
2 | Cons(i32, Box<List>),
| ^^^^ ^
因此可以以这种姿势来构造cons list:
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let ls = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
Deref
and Drop
Traits
Smart Pointer在Rust中之所以smart,是因为其对于一些特定的traits的实现,导致其在引用或者离开scope的时候可以有很多骚操作的空间。这样智能指针的行为可以类似于通常的引用。
Deref
Deref
trait可以customize在使用*
时的操作,这有些类似于重载了解引用这个操作。
首先,可以观察到Box
在dereference的时候,行为和通常的引用是基本一致的:
fn main() {
let x = 5;
let y = &x;
let y_box = Box::new(x);
assert_eq!(*y, 5);
assert_eq!(*y_box, 5);
}
Box
除去在heap上分配内存的部分,实现大抵是这样的:
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(val: T) -> MyBox<T> {
MyBox(val)
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
fn main() {
let x = 5;
let y_mybox = MyBox::new(x);
assert_eq!(*y_mybox, 5);
assert_eq!(*(y_mybox.deref()), 5);
}
实际上对于MyBox
的dereference会被自动变成形如*(y_mybox.deref())
的样子。值得一提的是deref
并没有直接返回self.0
,而是对其的一个引用。不直接返回值的原因是不传递ownership。
实际上Rust为了写起来方便,还引入了一个机制:Deref Coercions(解引用强制多态)。
fn hello(name: &str) {
println!("hello, {}", name);
}
fn main() {
let m = MyBox::new(String::from("f***"));
hello(&m);
}
这段代码是能够正常运行的。中文书中的解释如下:
这里使用
&m
调用hello
函数,其为MyBox<String>
值的引用。因为示例 15-10 中在MyBox<T>
上实现了 Deref trait,Rust 可以通过deref
调用将&MyBox<String>
变为&String
。标准库中提供了String
上的Deref
实现,其会返回字符串 slice,这可以在Deref
的 API 文档中看到。Rust 再次调用deref
将&String
变为&str
,这就符合hello
函数的定义了。
如若么的这个机制,写出来的代码应该长这样:
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
同理,mutable引用的解引用操作也可以通过DerefMut
重载。除此之外Deref
也可以将mutable引用解引用为immutable。
Drop
可以手动的实现Drop
trait:
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
// why &mut here?
fn drop(&mut self) {
println!("Dropping... {}", self.data);
}
}
fn main() {
let c = CustomSmartPointer { data: String::from("first pointer")};
let d = CustomSmartPointer { data: String::from("second pointer")};
println!("created pointers");
}
输出如下:
created pointers Dropping… second pointer Dropping… first pointer
Drop
是reverse order的- Rust不允许
drop
被直接手动调用 - 可以通过
std::mem::drop
来手动drop销毁
Rc<T>
Rc
是reference count(er)的缩写,其允许多个变量拥有同一个指针的ownership。它的内部维护一个引用计数器,在被创建(clone)的时候加一,被drop的时候减一。使用如下:
enum List {
Cons(i32, Rc<List>),
Nil,
}
fn main() {
let mut a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
println!("count after creating a: {}", Rc::strong_count(&a));
// *a = Cons(10, Rc::new(Nil));
let b = Cons(3, Rc::clone(&a));
let c = Cons(2, Rc::clone(&a));
println!("count after creating c: {}", Rc::strong_count(&a));
println!("{:?}", b);
}
但是Rs
不允许指针指向mutable reference。
RefCell<T>
当它是某个struct的element时,即使这个struct在被实例化的时候并没有被实例化成mutable,这个element仍旧可以被变更。而使用这个指针的borrow则会在runtime进行borrow规则的检查。 其用法可以通过一个简单的例子得知:
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T:Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T> where T:Messenger {
pub fn new(messenger: &T, max: usize) -> LimitTracker<T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage = self.value as f64 / self.max as f64;
if percentage >= 1.0 {
self.messenger.send("Error: over quota");
} else if percentage >= 0.9 {
self.messenger.send("Urgent Warning: over 90% quota");
} else if percentage >= 0.75 {
self.messenger.send("Warning: over 75% quota");
}
}
}
#[cfg(test)]
mod tests{
use super::*;
use std::cell::RefCell;
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.borrow_mut().push(String::from(message));
// 这里会运行时panic
let b1 = self.sent_messages.borrow_mut();
let b2 = self.sent_messages.borrow_mut();
}
}
#[test]
fn sends_over_75() {
let mockMessenger = MockMessenger::new();
let mut LimitTracker = LimitTracker::new(&mockMessenger, 100);
LimitTracker.set_value(80);
assert_eq!(mockMessenger.sent_messages.borrow().len(), 1);
}
}
RefCell
的规则是运行时检查的。其中borrow_mut()
会返回一个mutable borrow,而复数个mutable borrow会引起运行时panic。
其中的数据可以修改也可以通过这个例子说明,注意这里创建的List
都是immutable的:
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let val = Rc::new(RefCell::new(5));
let a = Rc::new(Cons(Rc::clone(&val), Rc::new(Nil)));
let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));
println!("a = {:?}", a);
*val.borrow_mut() += 10;
println!("a = {:?}", a);
println!("b = {:?}", b);
println!("c = {:?}", c);
}
而这段代码则产生了循环指针:
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil,
}
impl List {
// why &RefCell in Option
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match self {
Cons(_, item) => Some(item),
Nil => None,
}
}
}
use crate::List::{Cons, Nil};
fn main() {
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
println!("a initial rc = {}", Rc::strong_count(&a));
println!("a next: {:?}", a.tail());
let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
println!("a rc after b = {}", Rc::strong_count(&a));
println!("b initial rc = {}", Rc::strong_count(&b));
println!("b next: {:?}", b.tail());
if let Some(link) = a.tail() {
*link.borrow_mut() = Rc::clone(&b);
}
println!("a rc after b = {}", Rc::strong_count(&a));
println!("b rc after b = {}", Rc::strong_count(&b));
// infinite output loop here
// println!("a next item = {:?}", a.tail());
// println!("a next item = {:?}", a);
}
最后如果当输出a
或者a.tail()
的时候,会进入RefCell
里面循环地dereference直到溢出。这种Reference cycle会导致Rust无法自动地清理掉smart pointer,从而导致内存泄漏。
为了避免这种reference cycle,我们可以使用Rc::downgrade
来把strong reference变成weak reference(Weak<T>
)。Rc
的清理是根据strong_count
来进行的,而weak_count
并不能阻止它被drop。
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![])
});
println!("leaf strong = {}, weak = {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf));
println!("leaf parent: {:?}", leaf.parent.borrow().upgrade());
{
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!("branch strong = {}, weak = {}", Rc::strong_count(&branch), Rc::weak_count(&branch));
println!("leaf strong = {}, weak = {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf));
}
println!("leaf parent: {:?}", leaf.parent.borrow().upgrade());
println!("leaf strong = {}, weak = {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf));
}
}
以上代码实现了树的Node
。通过这种方式,可以让子节点拥有指向父节点的能力而不至于创建reference cycle,因为指针是Weak
的。
这里代码还是有一点点奇怪,upgrade()
似乎并不会把Weak
变成Rc
而是直接return Rc
。
Project: minigrep
在书中的第12章介绍了一个小的project如何在Rust里面实现。
// main.rs
use std::env;
use std::process;
use minigrep::Config;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {}", err);
process::exit(1)
});
if let Err(e) = minigrep::run(config) {
eprintln!("Runtime error: {}", e);
process::exit(1);
}
}
//lib.rs
use std::error::Error;
use std::fs;
use std::env;
pub struct Config {
pub query: String,
pub filename: String,
pub case_sensitive: bool,
}
impl Config {
pub fn new(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("Not enough args");
}
let query = args[1].clone();
let filename = args[2].clone();
let case_sensitive = env::var("CASE_INSENSITIVE").is_err();
Ok(Config { query, filename, case_sensitive })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>>{
let contents = fs::read_to_string(config.filename)?;
let result = if config.case_sensitive {
search(&config.query, &contents)
} else {
search_case_insensitive(&config.query, &contents)
};
for line in result {
println!("{}", line);
}
Ok(())
}
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut result = Vec::new();
for line in contents.lines() {
if line.contains(query) {
result.push(line);
}
}
result
}
pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut result = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
result.push(line);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(
vec!["safe, fast, productive."],
search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents));
}
}
虽然我必须要吐槽一下test case里面官方吹自己的吹的真是毫不掩饰,不过这个小project还是蛮有趣的。具体的实现细节就不多介绍了,有一些有意思的点:
- closure在
unwrap_or_else
里面的应用 - 对于环境变量变量和cli参数的读取:
env::var()
和env::args()
- 让function返回error,
main
处理error - lifetime在函数中的标记
在学习了Rust的Functional features之后,这段代码可以进行一些改写:
impl Config {
pub fn new(mut args: env::Args) -> Result<Config, &'static str> {
args.next();
let query = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get query string"),
};
let filename = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get filename"),
};
let case_sensitive = env::var("CASE_INSENSITIVE").is_err();
Ok(Config { query, filename, case_sensitive })
}
}
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents
.lines()
.filter(|x| x.contains(query))
.collect()
}
fn main() {
let config = Config::new(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {}", err);
process::exit(1)
});
if let Err(e) = minigrep::run(config) {
eprintln!("Runtime error: {}", e);
process::exit(1);
}
}
我也尝试使用closure将Config::new
的逻辑进行简化:
let getarg = |argname| {
match args.next() {
Some(arg) => arg,
None => return Err("Didn't get string"),
}
};
let query = getarg("query");
但是这里return
并没有办法return到closure外面去。故而感觉这种改写可能只能用宏来实现。
Concurrency
Rust的目标是安全的多线程,通过ownership和type enforcement等特性可以实现这个目的。而Rust需求的runtime很小,故而实现的多线程比较底层。一个简单的例子如下:
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("number {}, spawn", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("number {}, main", i);
thread::sleep(Duration::from_millis(1));
}
handle.join();
}
在ownership的设计下,可以方便地通过move
在线程间传递变量的ownership:
use std::thread;
use std::time::Duration;
fn main() {
let v = vec![1, 2, 3];
let clos = || println!("vector: {:?}", v);
clos();
let handle = thread::spawn(move || {
println!("vector: {:?}", v);
});
handle.join().unwrap();
}
spawn()
里面的closure可以capture v
,从而在新的线程中使用它。然而如若把move
去掉将会出现编译错误。注意这里倘若直接使用一个closure是不会报错的,因为main
里面的closure总是按照确定的顺序被执行的。然而在多线程的背景下,新线程中的println!
被执行时并不能确保v
是否仍然有效(因为可能在离开了自己的scope之后被drop),故而需要传递ownership。
Channel
在拥有了多线程之后,必然产生了线程间通讯的需求。书中引用了一句Go名言:
Do not communicate by sharing memory; instead, share memory by communicating.
Rust的通讯机制叫做channel,其可以传输某一个类型的对象。
use std::thread;
use std::sync::mpsc;
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会拿走ownership。
在这个模型中,也可以有多个生产者:
use std::thread;
use std::sync::mpsc;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
let tx1 = tx.clone();
thread::spawn(move || {
let vals = vec![
String::from("Hello"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for val in vals {
tx1.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
// println!("val is {}", val);
});
thread::spawn(move || {
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
for recvied in rx {
println!("Got: {}", recvied);
}
}
Shared-state
除了之前提到的允许多个ownership的智能指针,Rust还封装了mutex:
use std::sync::Mutex;
fn main() {
let m = Mutex::new(5);
{
let mut n = m.lock().unwrap();
*n += 1;
}
println!("m = {:?}", m);
}
这里的lock()
实际上就是在阻塞式地获取mutex。注意到这段代码并没有进行unlock,这是因为Mutex<T>
中智能指针MutexGuard
的实现导致其在离开scope时的Drop
进行了unlock()
。
在多线程的场景下使用:
use std::sync::{Mutex, Arc};
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = counter.clone();
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", counter.lock().unwrap());
}
这里的代码使用了Arc
而非Rc
。A代表atomic,其可以理解为多线程场景下使用的Rc
,因为Arc
实现了Send
这个trait而Rc
则没有。将Mutex
用法Arc
包住即可使得Mutex
能够在不同的线程间复用。这里可能还有一个骚操作:let counter = counter.clone();
,其创建了counter
的副本并将这个副本move
到了closure里面。还有一点则是counter
在被创建时实际上是immutable的,但是我们let mut num
,从counter
中得到了mutable reference,这是因为Mutex
就像Cell
一样提供了interior mutability。顺带一提,counter
也可以在代码中改为(*counter)
除此之外,Mutex
也可能引起deadlock(死锁)。Rust并不能阻止逻辑错误。
Sync
and Send
Rust提供了Sync
和Send
两个trait来处理并发。实现了Send
则表示其ownership可以在不同线程之间传递,而实现了Sync
则可以在不同线程里被访问。
OOP 面向对象
Rust并没有严格按照OOP来进行语言设计,但是Rust能够实现OOP能够实现的所有功能。Rust中并没有继承这一概念,但是可以通过约束函数中的泛型参数(bounded parametric polymorphism)必须实现某个trait来达到类似的目的。
除此之外,像对于函数(方法)的封装,隐藏实现细节/数据结构等等OOP特征都在Rust中存在。
Trait 对象
在Rust中,trait是更加接近对象的存在,因为其可以实现多个不同类型的函数重用。在这层意义上其比struct 或者enum的impl
更贴近对象的概念。可以用这种方式来定义一个动态的trait object vector:
//lib.rs
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
pub struct Button {
pub width: u32,
pub height: u32,
pub label: String,
}
impl Draw for Button {
fn draw(&self) {
//do something
}
}
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
//main.rs
use rust_test::Draw;
struct SelectBox {
width: u32,
height: u32,
options: Vec<String>,
}
impl Draw for SelectBox {
fn draw(&self) {
// code
}
}
use rust_test::{Button, Screen};
fn main() {
let screen = Screen {
components: vec![
Box::new(SelectBox {
width: 75,
height: 10,
options: vec![
String::from("Yes"),
String::from("No"),
],
}),
Box::new(Button {
width: 50,
height: 10,
label: String::from("OK"),
})
],
};
}
上述代码的抽象逻辑是实现对于gui的统一管理,用户也可以自行添加gui的控件,只要其实现Draw
trait即可。注意到dyn
关键字的使用,大抵是运行时才可以确定类型的标记,这样在编译时便会添加额外的runtime代码来进行检查。
在Rust中对于trait object的创建必须使用引用或者智能指针(Box<T>
,猜测是因为需要确定大小来分配内存)。
这段代码和泛型的区别在于,components
这个向量里面可以存放不同类型的变量,而如若使用T
泛型参数,则只能存放同一种类型。
Object-safe
Rust只允许object-safe的trait在trait object中被使用。规则如下:
- 返回类型不能为
Self
- 无泛型参数
原因可以查看Rust PL中文版:
当使用 trait 对象时其具体类型被抹去了,故无从得知放入泛型参数类型的类型是什么。
OOP范例
这里Rust PL给出了两个同一任务的两个参考OOP范例实现,通过举了一个发布post的例子。post被创建时是草稿,必须经过审核才能发表。
// lib.rs
pub struct Post {
state: Option<Box<dyn State>>,
// state: Box<dyn State>,
content: String,
}
impl Post {
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
// state: Box::new(Draft {}),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
self.state.as_ref().unwrap().content(self)
}
pub fn request_review(&mut self) {
// `take()` takes ownership and set to None temporarily
if let Some(s) = self.state.take() {
self.state = Some(s.request_review());
}
// self.state = self.state.request_review()
}
pub fn approve(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.approve());
}
}
}
trait State {
// self: Box<Self> means the caller of this is holds a `Box` for Self type
// note self will take the ownership, invalidating the current state
fn request_review(self: Box<Self>) -> Box<dyn State>;
fn approve(self: Box<Self>) -> Box<dyn State>;
fn content<'a>(&self, post: &'a Post) -> &'a str {
""
}
}
struct Draft {}
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
struct PendingReview {}
impl State for PendingReview {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
Box::new( Published {})
}
}
struct Published {}
impl State for Published {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
fn content<'a>(&self, post: &'a Post) -> &'a str {
&post.content
}
}
// main.rs
use rust_test::Post;
fn main() {
let mut post = Post::new();
post.add_text("some text??");
assert_eq!("", post.content());
post.request_review();
assert_eq!("", post.content());
post.approve();
assert_eq!("some text??", post.content());
}
Post
中包含了一个trait object,State
State
实现了状态转换self.state.take()
拿走了ownership来invalidate之前的状态- 个人认为还可以通过将
State
设计成enum然后通过match
来实现状态转换。 - 使用
state: Box::new(Draft {})
和self.state = self.state.request_review()
会出错,猜测原因是不能拿走ownership-
cannot move out of
self.state
which is behind a mutable reference -
move occurs because
self.state
has typeBox<dyn State>
, which does not implement theCopy
trait
-
除了上述实现之外,书中还给了一个将struct本身设计为状态的实现,即在不同的struct之间的转换即为状态转换。
在此展示出来的实现的好处是可以随意的添加状态。实际上通过在State
trait中添加可以return self
的默认实现可以简化代码,但是这样会违背object-safe的规则,从而导致不能使用trait object。
Pattern & Matching
读到这章才发现,Rust原来一直在用pattern matching这个概念,它最骚的是根本没有告诉你就连赋值这种简单的东西都是通过pattern matching来实现的。突然有一种自己被PUA了的感觉?于是我就想知道这种方式的实现和其他PL(e.g. C)的实现有什么区别?结果反倒翻到了王垠大佬的Blog,进而发现这家伙居然还在我校退学过,然后爬了一整天他的Blog还被“骗”五刀:)。结果发现自己不但还是没明白究竟是怎么实现的,连学Rust的时间都被压缩了-_-。
在Rust中,match
,if let
,while let
,destructure甚至let
和函数的参数传递本身都用到了pattern matching。你还可以写出这样的代码:
fn main() {
let color: Option<&str> = None;
let is_thursday = false;
let age: Result<u8, _> = "34".parse();
if let Some(color) = color {
println!("using color {}", color);
} else if is_thursday {
println!("Today is thursday");
} else if let Ok(age) = age {
if age > 30 {
println!("old boy");
} else {
println!("young boy");
}
} else {
println!("Nothing happened");
}
}
其中混用了if let
和分支判断。我个人认为这样的代码组合方式不是特别有逻辑,从某种意义上讲还是比较容易引起confusion的。
Refutability
然而pattern matching在Rust中是分情况的。考虑如下代码和报错:
let Some(x) = Some(1);
错误:
error[E0005]: refutable pattern in local binding: `None` not covered
--> src/main.rs:2:9
|
2 | let Some(x) = Some(1);
| ^^^^^^^ pattern `None` not covered
|
::: /home/ya0guang/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/option.rs:165:5
|
165 | None,
| ---- not covered
|
= note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
= note: for more information, visit https://doc.rust-lang.org/book/ch18-02-refutability.html
= note: the matched value is of type `Option<i32>`
help: you might want to use `if let` to ignore the variant that isn't matched
这里Rustc告诉我们let
需要 irrefutable pattern,即不会被拒绝的模式,意味着变量的绑定不会失败。而像if let
则可以接受refutable pattern。
Matching Syntaxes
match
_
在matching中有着特殊的意义:
fn main() {
let s = Some(String::from("hello"));
if let Some(x) = s {
println!("found string");
}
println!("{:?}", s);
}
用Some(_x)
或者是Some(x)
去匹配时都会报错:borrow of partially moved value: s
。而若是使用Some(_)
去匹配则不会产生任何错误,因为其不绑定。顺带一提这里还可以使用&s
避免move。
- 在
match
语句内会创建新的scope,也可能shadow外面的变量 - 可以使用
|
来表明逻辑或去匹配多个模式 ..=
能够匹配范围,例如:1..=5
,只允许数字/char
_
作为通配符匹配想要“忽略”的部分,并且不产生绑定。其作为符号是语义是不同于其他变量名的!
Destructuring
这段代码写下来有些奇怪:
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7};
let Point {x: a, y: b} = p;
// in case: let Point {x: a, y: b} = p;
// var x, y will be created
assert_eq!(0, a);
assert_eq!(7, b);
match p {
Point {x, y: 0} => println!("On x axis at {}", x),
Point {x: 0, y} => println!("On y axis at {}", y),
Point {x, y} => println!("Not on axis at {}, {}", x, y),
}
}
其他的例子如下:
struct Point {
x: i32,
y: i32,
z: i32,
}
fn main() {
let origin = Point {x: 0, y: 0, z: 0};
match origin {
Point {x, ..} => println!("on x axis"),
};
}
- destructuring也可以作用于struct/tuple/enum
- 同理,可以作用于nested type
..
用于匹配懒得管的字段,可以把..
以不产生歧义的方式丢在中间
Match Guard
可以在被匹配的模式后面加上if
表达式来仅匹配表达式为真的情况,这样能够丰富语义:
fn main() {
let x = Some(10);
match x {
Some(x) if x <= 10 => println!("x is {}", x),
_ => println!("Nothing"),
}
}
match guard的优先级低于|
。
Rust还提供了一个在我看来像是语法糖一样的东西,@
,其可以在同一个模式中顺便测试和保存值的范围。
// from Rust PL book
enum Message {
Hello { id: i32 },
}
let msg = Message::Hello { id: 5 };
match msg {
Message::Hello { id: id_variable @ 3..=7 } => {
println!("Found an id in range: {}", id_variable)
},
Message::Hello { id: 10..=12 } => {
println!("Found an id in another range")
},
Message::Hello { id } => {
println!("Found some other id: {}", id)
},
}
高级特性
终于到语言特性的最后一章了兴奋的搓手手。
unsafe
由于计算机不安全的本质,Rust为了system level programming,提供了一些不安全的“超能力”。
裸指针解引用
fn main() {
let mut num = 42;
let r1 = &num as * const i32;
let r2 = &mut num as * mut i32;
unsafe {
println!("r1: {}", *r1);
println!("r2: {}", *r2);
}
let address = 0x12345usize;
let r = address as *const i32;
println!("addr: {:?}",r);
// seg fault
unsafe {
println!("deref a invalid pointer: {:?}", *r);
}
}
Rust的裸指针可以有两种形式(类型):* const T
和* mut T
。创建裸指针本身并不违法,但是解引用就需要超能力的帮助了。甚至我们也可以直接创建一个地址。
调用不安全的函数/方法
一个例子如下:
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!(len >= mid);
unsafe {
(
slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
// (&mut slice[..mid], &mut slice[mid..])
}
fn main() {
let mut v = vec![1, 2, 3, 4, 5, 6];
let r = &mut v;
let (a, b) = split_at_mut(r, 3);
assert_eq!(a, &mut [1, 2, 3]);
assert_eq!(b, &mut [4, 5, 6]);
}
如果不使用unsafe
中的内容而用注释中的代码的话,编译无法通过:rust不允许对同一个变量创建两个mutable references。因此这里需要使用std::slice
中提供的unsafe函数。
extern
可以让Rust使用外部的函数或是给其他语言export一个Rust写的函数:
extern "C" {
fn abs(input: i32) -> i32;
}
#[no_mangle]
pub extern "C" fn call_from_c() {
println!("Hello Rust");
}
fn main() {
unsafe {
println!("abs fo -3: {}", abs(-3));
}
}
其中,no_mangle
告诉编译器不要mangle函数名。
可变静态变量
static
变量在Rust中被存储在固定的内存中。其本不应该是可变的。但是可以通过这种技术:
static mut s: &str = "hello";
fn push(ins: &'static str) {
unsafe {
s = ins;
}
}
fn main() {
push("Rust");
unsafe { println!("unsafe string: {}", s);}
}
这种操作还是比较骚的。原书中使用的是u32
,但是我想试试能不能改变一个&str
的值。我还不知道应该如何把String
的值强行赋给这个&str
,不过感觉这样需要非常蛋痛地fight with compiler。
实现不安全的trait
unsafe trait Foo {
// do something
}
unsafe impl Foo for i32 {
// do something else
}
访问联合体中的字段
高级trait
Associated Type
可以在trait中使用associated type:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
// code
}
}
// generics signature
pub trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
当然也可以用泛型实现类似的功能。Associated type限制我们对于一个trait只能有一个实现,而泛型则允许我们对于多种类型进行不同的实现。
Default Generic Type Para & Overloading
可以给泛型参数设置默认值和重载运算符:
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: 2 } + Point { x: 2, y: 1},
Point { x: 3, y: 3}
)
}
// `Add` trait
trait Add<Rhs=Self> {
type Output;
fn add(self, rhs: Self) -> Self::Output;
}
消除歧义
如果在不同名称的trait内定义了同样名称的方法,而之后又为某个类型实现了这些traits,那么在调用的时候应该可以采用这种方式:Trait::method(&var)
。更加general的语法是这样的:<Type as Trait>::function()
。
Supertrait
当一个trait依赖另一个来实现功能时,后者是supertrait。
use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("* {} *", output);
println!("{}", "*".repeat(len + 4));
}
}
impl OutlinePrint for Point {}
impl fmt::Display for Point {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(fmt, "({}, {})", self.x, self.y)
}
}
高级类型
// nothing different from i32 for Kilometers
type Kilometers = i32;
fn main() {
let km: Kilometers = 42;
let m: i32 = 44;
// can still add km and m here
let x = km + m;
}
fn generic<T: ?Sized>(t: &T);
- 可以用
type Kilometers = i32;
类似的代码来创建一个类型的别名 - Rust有一个奇怪的
!
类型,它fn bar() -> !
表示函数永不返回panic!()
是!
类型continue
是!
类型!
可以被视作任何类型,所以match
可以使得所有分支的返回类型相同
- 出于静态编译/DST(dynamically sized type)的考虑,Rust有
Sized
trait- 它用来决定在编译时类型的大小是否知道
- 所有泛型参数必须实现
Sized
- 接上条,除非使用
<T: ?Sized>
时,可以在函数中使用&T
高级函数/Closure
可以像这样把函数作为参数传递给别的函数:
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, 10);
}
不得不说这种写法让人想起了Typed Racket。
f
在这里的类型是函数指针,注意fn
是类型,其实现了所有closure的trait,和closure的Fn
区分。因此理论上接收closure的地方都应该接收fn
。
还可以通过这样的骚操作来初始化数组:
enum Status {
Value(u32),
Stop,
}
fn main() {
let list: Vec<Status> = (0u32..20).map(Status::Value).collect();
}
当然也函数也可以返回一个closure:
// fn return_closure() -> dyn Fn(i32) -> i32 {
// |x| x + 1
// }
fn return_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
因为closure在编译时不知道大小(Sized
问题),必须用指针包起来。
宏
宏是一种元编程,简单地说就是可以让Rust帮你写Rust。
Declarative Macro
例如 vec!简化版是这样实现的:
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
这个规则其实有点类似于Racket的define-syntax
,也基本上是宏定义的写法。
( $( $x:expr ),* )
匹配模式$(),*
中,,
可以出现或者不出现,*
表示匹配数次=>
语义同match
$x:expr
匹配Rust表达式,将其赋给$x
#[macro_export]
使其能够被调用
Procedural Macro
更加类似于函数:接收代码作为输入并输出代码。
use proc_macro;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {}
Custom Derive
在rust_test
目录下创建rust_test_derive
lib 如下:
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn;
#[proc_macro_derive(HelloMarco)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
let ast = syn::parse(input).unwrap();
impl_hello_macro(&ast)
}
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let gen = quote! {
impl HelloMarco for #name {
fn hello_macro() {
println!("Hello!, my name is {}!", stringify!(#name));
}
}
};
gen.into()
}
并将这个项目的配置文件加入:
[lib]
proc-macro = true
[dependencies]
syn = "1"
quote = "1"
外面的rust_test
项目内代码如下:
// lib.rs
pub trait HelloMarco {
fn hello_macro();
}
// main.rs
use rust_test::HelloMarco;
use rust_test_derive::HelloMarco;
#[derive(HelloMarco)]
struct Pancakes;
fn main() {
Pancakes::hello_macro();
}
记得将rust_test_derive
加入配置:rust_test_derive = {path = "./rust_test_derive"}
。
简单地说这段代码的原理就是通过调用Rust的编译器生成AST然后在拿到AST中关于类名的元数据,再从元数据生成一段新的impl
后插入原来的代码。
- Rust并不能在运行时知道类名,但是可以通过宏实现
- Rust蛋疼的设定导致必须在一个crate里面完成宏定义
proc_macro
是编译器读取代码的APIsyn
是用来生成AST的包,而quote
生成代码
初次之外还有attribute-like macro和function-like macro。前者类似于Python中web server对于网页路径对应的解析函数的装饰器,而后者类似于macro_rules!
但是却依赖#[proc_macro]
。
Final Project: Multithreaded Web Server
这几行小代码可以实现一个tiny webserver。
use std::net::TcpListener;
use std::net::TcpStream;
use std::io::prelude::*;
use std::fs;
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
println!("connection established!");
handle_connection(stream);
}
}
fn handle_connection(mut stream: TcpStream) {
let mut buffer = [0; 1024];
stream.read(&mut buffer).unwrap();
let get = b"GET / HTTP/1.1\r\n";
let (status_line, filename) = if buffer.starts_with(get) {
("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
} else {
("HTTP/1.1 404 NOT FOUND\r\n\r\n", "404.html")
};
println!("Request from client: {}", String::from_utf8_lossy(&buffer));
let content = fs::read_to_string(filename).unwrap();
let response = format!("{} \r\nContent-Length: {}\r\n\r\n{}", status_line, content.len(), content);
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
但是如果在handle_connection
中的任务如若比较繁重则无法高效地服务于多用户。下面使用线程池来对这段代码改进。
// file: src/bin/main.rs
use std::net::TcpListener;
use std::net::TcpStream;
use std::io::prelude::*;
use std::fs;
use std::time::Duration;
use std::thread;
use server::ThreadPool;
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
let pool = ThreadPool::new(4);
//or stream in listener.incoming().take(2) {
for stream in listener.incoming() {
let stream = stream.unwrap();
println!("[Server:] connection established!");
pool.execute(|| {
handle_connection(stream);
});
}
}
fn handle_connection(mut stream: TcpStream) {
let mut buffer = [0; 1024];
stream.read(&mut buffer).unwrap();
let get = b"GET / HTTP/1.1\r\n";
let sleep = b"GET /sleep HTTP/1.1\r\n";
let (status_line, filename) = if buffer.starts_with(get) {
("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
} else if buffer.starts_with(sleep) {
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
} else {
("HTTP/1.1 404 NOT FOUND\r\n\r\n", "404.html")
};
println!("Request from client: {}", String::from_utf8_lossy(&buffer));
let content = fs::read_to_string(filename).unwrap();
let response = format!("{} \r\nContent-Length: {}\r\n\r\n{}", status_line, content.len(), content);
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
// file: src/lib.rs
use std::thread;
use std::sync::mpsc;
use std::sync::Arc;
use std::sync::Mutex;
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Message>,
}
type Job = Box<dyn FnOnce() + Send + 'static>;
enum Message {
NewJob(Job),
Terminate,
}
struct Worker {
id: usize,
job: Option<thread::JoinHandle<()>>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Message>>>) -> Worker {
let job = thread::spawn(move || loop {
let message = receiver.lock().unwrap().recv().unwrap();
match message {
Message::NewJob(job) => {
println!("Worker {} got a job; exec...", id);
job();
}
Message::Terminate => {
println!("Worker {} told me to stop work", id);
break;
}
}
});
Worker {
id,
job: Some(job),
}
}
}
impl ThreadPool {
/// Creates a new thread pool
///
/// # Panics
///
/// size cannot be zero or negetive
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for i in 0..size {
workers.push(Worker::new(i, Arc::clone(&receiver)))
}
ThreadPool {
workers,
sender,
}
}
pub fn execute<F>(&self, f: F)
where F: FnOnce() + Send + 'static {
let job = Box::new(f);
self.sender.send(Message::NewJob(job)).unwrap();
}
}
impl Drop for ThreadPool {
fn drop(&mut self) {
println!("Sending terminate msg to workers");
for _ in &self.workers {
self.sender.send(Message::Terminate).unwrap();
}
for worker in &mut self.workers {
println!("Shutting down worker {}", worker.id);
if let Some(thread) = worker.job.take() {
thread.join().unwrap();
}
}
}
}
- 这个程序实现了线程池模型的web server,思路和用C++的实现基本相同
- 提供了graceful clean,但是没有实现
Signal
机制下的结束 - 使用了高级特性,比如之前接触到的channel,
Arc
,Mutex
等等。
Epilogue
基本上本人对于Rust PL这本书的学习就告一段落了,然而对于编程语言的学习似乎还没有办法停止。Rust确实是一门非常酷炫的语言,它在简洁的同时做到了非常“啰嗦”,这真的是一种非常矛盾的特点,大概安全的代价就是啰嗦吧。为了避免敲键盘到手酸,此处安利一下VSCode里面的tabnine
插件,可以让你少敲很多代码的!
那么,代价是什么呢?
Other References
- 如何看待 Rust 的应用前景?里面说的关于Rust的各种features还有待我去体会到
- 用Rust重写Linux内核模块体验留用参考
- 王垠对于Rust的分析
- 注:我不认为他这篇分析是好的,因为大多数集中在语法的分析而忽略了语法背后的设计考量。
留下评论