最近有一些水文章的需求,而且意外的在github上看到一些巨有趣的项目,结合了树莓派、Rust、操作系统等诸多元素,多厨狂喜,就准备搞一把Rust尝个鲜。
其中一个项目叫做Writing an OS in Rust,有中文版本,但是翻译的不多。英文的我大概看了,深入浅出的在x86设备上编写操作系统,有很多操作系统相关基础的文字介绍。
另一个项目叫做Operating System development tutorials in Rust on the Raspberry Pi。从readme来看,是一个给ARM64位ARMv8-A架构的操作系统业余爱好者进行入门的一个类教程项目。项目用docker来抹平可能存在的工具链问题,推荐在Linux(据说macOS这种unix也可以)里练手。里面还提到了在宿主机上用QEMU模拟内核进行编码并编译,后面还会通过串口和UART在树莓派实操。
还有个更神的太素 TisuOS,这是在RISC-V上的教程型操作系统,国人创作,后期可以试试。
可能因为我最近极度崇拜稚晖君,因此,想以一个CS的身份向EE方向龟速考虑,日拱一卒吧……
1 Rust环境
安装官方推荐方法curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh,安装rust全家桶(包括包管理器及工具链管理器rustup和一些环境变量配置)。
宿主机macOS,IDE选用不要钱的vscode,插件装上Rust或rust-analyzer、codelldb(调试)后,基本环境组装完毕(再夸一句不要钱的vscode)。(如果lldb安装时下载不给力,可以手动下然后使用cmd+shift+p,搜索入Extensions: Install from VSIX离线安装*.vsix文件。)
配置debug参数,vscode里cmd+shift+P调出菜单,输入debug选择调试 打开launch.json配置调试参数,vscode自动新建该.vscode/lanuch.json文件并初始化配置(这时候就用到codelldb了)。
在vscode中启动调试时,vscode会自动安装调试组件:
rustup component add rust-analysis --toolchain stable-x86_64-apple-darwin <info: component 'rust-analysis' for target 'x86_64-apple-darwin' is up to date
rustup component add rust-src --toolchain stable-x86_64-apple-darwin <info: component 'rust-src' is up to date
rustup component add rls --toolchain stable-x86_64-apple-darwin <info: installing component 'rls'一个典型的配置类似:
{ // 使用 IntelliSense 了解相关属性。 // 悬停以查看现有属性的描述。 // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "lldb", "request": "launch", "name": "Launch", "args": [], "program": "${workspaceFolder}/target/debug/${workspaceFolderBasename}", "cwd": "${workspaceFolder}", "stopOnEntry": false, "sourceLanguages": ["rust"], "sourceMap": { "/rustc/*": "${env:HOME}/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/src/rust" } } ]}使用cmd+shift+b进行cargo build,然后F5启动调试。(解释性语言用多了会忘记build……)
注:后面在使用中发现,rust-analyzer每次更新的时候重新安装插件会很慢,建议科学上网,比如直接挂在有openwrt的wifi上,因为系统全局代理貌似在vscode升级的时候不起作用。
2 实录
花了一周的空闲时间大概看了一下runoob和简单教程的rust基础知识。把里面的代码基本打了一遍,了解了一下写rust的感觉。也大概摸清了在vscode下写rust的一般步骤:
-
cargo new xxx创建项目; -
cmd + shift + B打开build快捷菜单,点后面的齿轮按钮; -
此时vscode会创建
.vscode/task.json文件,用于显式定义build任务,该文件定义的初始任务只有cargo build; -
我们一般还需要使用
cargo check和cargo run,只需要复制初始task并替换command字段就可以实现:{"version": "2.0.0","tasks": [{"type": "cargo","command": "build","problemMatcher": ["$rustc"],"group": "build","label": "rust: cargo build"},{"type": "cargo","command": "run","problemMatcher": ["$rustc"],"group": "build","label": "rust: cargo run"}]}
必须看官方教程,决不能只看速通tutorial,这个语言的细节感觉并不比C/C++系少多少。如果看中文版有不懂的地方,建议移步原文……
-
ch02-00:介绍安装和更新(
cargo update)dependency,以及通过Result处理无效输入:let guess: u32 = match guess.trim().parse() {Ok(num) => num,Err(_) => continue,}; -
ch03-01:shadowing常用于重复利用变量名:
let spaces = " ";let spaces = spaces.len(); -
ch03-02:关于元组的一个细节,如果元组数据均为基础类型,则该元组的内容均存放在栈上,进行赋值时,如
let tup2 = tup1,元组内容会在栈上复制一份新的给tup2。而如果元组数据类型包含非基础类型,则该院组的内容将存放在堆上,此时赋值就相当于移动(move),即tup2仅包含tup1内容的指针,移动后tup1失效,因为内容的所有权转移给了tup2。 -
ch03-03:Rust是一门基于表达式(expression-based)的语言,表达式有返回值,语句则没有返回值,因此可以使用代码块和包裹表达式:
let y = {let x = 3;x + 1}; -
ch03-05:三元运算符的写法:
let number = if condition { 5 } else { 6 }; -
ch04-01:Rust的基础类型和复杂类型的指针是存放于栈上的,复杂类型本身存在于堆上。栈内的内容赋值时(如
let int1 = int2)会拷贝,因为栈编译时大小确定所以快;而堆内的内容赋值时(如let string1 = string2)默认不拷贝(Rust的设计就是不会主动进行深拷贝,因此Rust提供的自动复制对运行时效率影响都很小),只拷贝其栈内指针,并且使原来的变量失效(即string1失效),以避免二次释放等bug。很明确,基础类型的赋值操作会自动赋值,基础类型的各种组合元组也延续这一特性。 -
ch04-02:Rust的引用作用域是首次声明到最后一次使用,因此在只读引用的最后一次使用后,再进行可变引用,是可以通过编译的。也就是说,只要只读引用和可变引用的作用域不重叠,就没有问题。
-
ch04-03:
&str就是slice,这是不可变引用。(Rust的这种把问题消灭在编译中的特性,是不是也顺便消灭了一批有问题的程序员……) -
ch05-01:通常在结构体中不使用引用,如果使用引用,则需要用生命周期标识,以确保结构体引用的数据有效性跟结构体本身保持一致。没有任何字段的类单元结构体被称为类单元结构体(unit-like structs)因为它们类似于
(),即unit类型。类单元结构体常常在你想要在某个类型上实现trait但不需要在类型中存储数据的时候发挥作用。 -
ch05-02:想要通过
println!("{}", xxx)打印结构体,需要实现std::fmt::Display;想要通过
println!("{:?}", xxx)打印结构体,需要实现std::fmt::Debug,或手动为结构体加上#[derive(Debug)]注解,此时打印出:Terminal window rect1 is Rectangle { width: 30, height: 50 }或者使用pretty-print风格
println!("{:#?}", xxx),此时打印出:Terminal window rect1 is Rectangle {width: 30,height: 50}Rust提供了很多可以使用derive注解来使用的trait。
-
ch05-03:C++中的静态函数(使用
::调用),即附着在类而不是类实例上的函数,在Rust中称为关联函数,impl中不以&self作为第一个参数的函数均为关联函数。 -
ch06-01:枚举类型的定义:
enum Message {Quit, // 没有包含任何数据Move { x: i32, y: i32 }, // 包含匿名结构体Write(String), // 包含单独一个`String`ChangeColor(i32, i32, i32), // 包含三个 i32} -
ch06-03:
if let...else...常用于代替match..._...的情况:let mut count = 0;match coin {Coin::Quarter(state) => println!("State quarter from {:?}!", state),_ => count += 1,}// ↓↓↓↓↓↓↓↓↓↓ 使用`if let`代替 ↓↓↓↓↓↓↓↓↓↓let mut count = 0;if let Coin::Quarter(state) = coin {println!("State quarter from {:?}!", state);} else {count += 1;} -
ch07-02:新建二进制项目用
cargo new xxx,生成src/main.rs文件为bin执行入口;新建库项目用cargo new --lib xxx,生成src/lib.rs为lib编译入口; -
ch07-03:公有结构体若包含私有字段,则应当提供公有关系函数(
::)来构造实例,因为无法在外部初始化私有变量;而枚举一旦公有,则所有成员均为公有。 -
ch07-04:
pub use可以将导入的模块变为公有,供外部调用的模块使用,单纯use是私有的。 -
ch08-01:虽说
vector<T>只能包含类型T,但是也可以用枚举类型向vector里塞多种类型:enum SpreadsheetCell {Int(i32),Float(f64),Text(String),}let row = vec![SpreadsheetCell::Int(3),SpreadsheetCell::Text(String::from("blue")),SpreadsheetCell::Float(10.12),]; -
ch09-02:
unwrap返回正常的Ok中的值或触发panic!,expect类似,只不过可以通过expect自定义错误信息(unwrap只有默认信息,有多个unwrap时容易混淆)。 使用unwrap_or_else简化match: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(fc) => fc,Err(e) => panic!("Problem creating the file: {:?}", e),},other_error => panic!("Problem opening the file: {:?}", other_error),},};}// ↓↓↓↓↓↓↓↓↓↓ 更老练的Rustacean会这么写 ↓↓↓↓↓↓↓↓↓↓fn main() {let f = File::open("hello.txt").unwrap_or_else(|error| {if error.kind() == ErrorKind::NotFound {File::create("hello.txt").unwrap_or_else(|error| {panic!("Problem creating the file: {:?}", error);})} else {panic!("Problem opening the file: {:?}", error);}});}用
?简化match(注意:?运算符只能用在返回值是Result类型的函数中,用来传播异常):use std::io::{self, Read};use std::fs::File;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),}}// ↓↓↓↓↓↓↓↓↓↓ 使用`?`简化 ↓↓↓↓↓↓↓↓↓↓fn read_username_from_file() -> Result<String, io::Error> {let mut f = File::open("hello.txt")?;let mut s = String::new();f.read_to_string(&mut s)?;Ok(s)}// ↓↓↓↓↓↓↓↓↓↓ 链式`?`再简化 ↓↓↓↓↓↓↓↓↓↓fn read_username_from_file() -> Result<String, io::Error> {let mut s = String::new();File::open("hello.txt")?.read_to_string(&mut s)?;Ok(s)} -
ch10-02:几种trait声明写法:
// 普通公有traitpub trait Summary {fn summarize(&self) -> String;}// 带默认实现pub trait Summary {fn summarize(&self) -> String {String::from("(Read more...)")}}// trait中默认实现调用其他函数pub trait Summary {fn summarize_author(&self) -> String;fn summarize(&self) -> String {format!("(Read more from {}...)", self.summarize_author())}}// impl trait语法,trait作为参数pub fn notify(item: impl Summary) {println!("Breaking news! {}", item.summarize());}// ↓↓↓↓↓↓↓↓↓↓ trait bound语法pub fn notify<T: Summary>(item: T) {println!("Breaking news! {}", item.summarize());}// 多个impl traitpub fn notify(item1: impl Summary, item2: impl Summary) {}// ↓↓↓↓↓↓↓↓↓↓ 使用trait bound语法强行指定同类型参数pub fn notify<T: Summary>(item1: T, item2: T) {}// + 同时指定多个traitpub fn notify(item: impl Summary + Display) {}// ↓↓↓↓↓↓↓↓↓↓ + 同时指定多个trait的trait boundpub fn notify<T: Summary + Display>(item: T) {}// 复杂trait boundfn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {}// ↓↓↓↓↓↓↓↓↓↓ where简化复杂trait bound函数签名fn some_function<T, U>(t: T, u: U) -> i32where T: Display + Clone,U: Clone + Debug{}// 函数返回值为trait// 不能直接通过如if/else等方法在同一个函数中返回不同的均实现Summary trait的类// 如if => Tweet else => NewsArticlefn returns_summarizable() -> impl Summary {Tweet {username: String::from("horse_ebooks"),// --snip--}}// 为符合特定条件的泛型实现方法struct Pair<T> {x: T,y: T,}// 为所有泛型实现new方法impl<T> Pair<T> {fn new(x: T, y: T) -> Self {Self {x,y,}}}// 为满足Display和PartialOrd trait的类实现cmp_display方法impl<T: Display + PartialOrd> Pair<T> {fn cmp_display(&self) {if self.x >= self.y {println!("The largest member is x = {}", self.x);} else {println!("The largest member is y = {}", self.y);}}}// 为实现了特定trait的类有条件的实现traitimpl<T: Display> ToString for T {} -
ch10-03:生命周期标注确实抽象……
- 首先,生命周期一般出现在变量借用环节;
- 其次,函数的生命周期标注只是为了告诉借用检查器,各借用变量的生命周期长短关系,并不会改变函数行为;
- 再次,类似“协变”概念,为方便可以理解为长生命周期是短生命周期的子类。类似面向对象中里氏代换的概念——“接受父类型的位置,子类型也可以占位”,对类型而言,范围大类型是范围小类型的父类,比如
i32是i16的父类;那么对生命周期而言长生命周期的变量是短生命周期变量的子类,也就是活得长的变量可以代替活的短的变量的位置,这样就能避免引用参数过早释放; - 最后,生命周期一般标注在输入(参数)和输出(返回值)有关系的变量(参数或返回值)上,无关变量即使是借用也可以不标注。
- 函数或方法的参数的生命周期被称为 输入生命周期(input lifetimes),而返回值的生命周期被称为 输出生命周期(output lifetimes)。
- 编译器采用三条规则来判断引用何时不需要明确的注解。第一条规则适用于输入生命周期,后两条规则适用于输出生命周期。如果编译器检查完这三条规则后仍然存在没有计算出生命周期的引用,编译器将会停止并生成错误。这些规则适用于
fn定义,以及impl块。- 第一条规则是每一个是引用的参数都有它自己的生命周期参数。换句话说就是,有一个引用参数的函数有一个生命周期参数:
fn foo<'a>(x: &'a i32),有两个引用参数的函数有两个不同的生命周期参数,fn foo<'a, 'b>(x: &'a i32, y: &'b i32),依此类推。 - 第二条规则是如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数:
fn foo<'a>(x: &'a i32) -> &'a i32。 - 第三条规则是如果方法有多个输入生命周期参数并且其中一个参数是
&self或&mut self,说明是个对象的方法,那么所有输出生命周期参数被赋予self的生命周期。第三条规则使得方法更容易读写,因为只需更少的符号。
- 第一条规则是每一个是引用的参数都有它自己的生命周期参数。换句话说就是,有一个引用参数的函数有一个生命周期参数:
-
ch11-01:测试某函数应该在特定条件下panic时,
expected的内容就是panic输出时应该包含的内容:#[test]#[should_panic(expected = "Guess value must be less than or equal to 100")]fn greater_than_100() {Guess::new(200);}利用返回值为
Result<T, E>的函数进行测试,可以在函数中使用?运算符,也可以方便的编写任何运算符会返回Err成员的测试。不能对使用Result<T, E>的测试使用#[should_panic]注解。相反应该在测试失败时直接返回Err值:#[test]fn it_works() -> Result<(), String> {if 2 + 2 == 4 {Ok(())} else {Err(String::from("two plus two does not equal four"))}} -
ch11-02:
cargo test选项:Terminal window # 单线程运行,若各测试间存资源抢占可以使用线程指定选项test-threadscargo test -- --test-threads=1# 测试默认会截获程序所有输出,使得测试输出更具备可读性,希望打印程序输出使用nocapture选项cargo test -- --nocapture# 测试可以指定测试名称cargo test it_works# 测试可以匹配名称,如匹配各种it开头的测试cargo test it# 可以在代码中使用#[ignore]注解来禁止诸如耗时测试的进行,使用ignored选项运行这些被禁止的测试注解cargo test -- --ignored -
ch11-03:测试规范:
- 单元测试:单元测试与他们要测试的代码共同存放在位于
src目录下相同的文件中。规范是在每个文件中创建包含测试函数的tests模块,并使用#cfg(test)标注模块(即在test时才编译运行,build事不做以节省时间)。 - 集成测试:应在项目根目录创建一个
tests目录,与src同级,cargo会去找到这个文件夹中的所有文件并编译成一个个单独的crate(无需#[cfg(test)]注解,tests是cargo认为的特殊文件夹,只有测试是才编译)。 - 输出依次为单元测试、集成测试、文档测试,使用
cargo test --test integration_test可以指定集成测试。 tests目录中的子目录不会被作为单独的crate编译或作为一个测试结果部分出现在测试输出中,因此,需要诸如多个测试均涉及的工具函数可以放在tests/common/mod.rs中。
- 单元测试:单元测试与他们要测试的代码共同存放在位于
-
ch12-01:
collect方法的返回值需要显示注明类型,因为rust无法推断出collect返回的类型,比如:let args: Vec<String> = env::args().collect(); -
ch12-03:当函数返回一个Result,失败时希望返回失败原因字符串,则泛型应有static生命周期注释,如
pub fn new(mut args: std::env::Args) -> Result<Config, &'static str>。 -
ch13-01:闭包可以通过三种方式捕获其环境,他们直接对应函数的三种获取参数的方式:获取所有权,可变借用和不可变借用。这三种捕获值的方式被编码为如下三个
Fntrait:FnOnce消费从周围作用域捕获的变量,闭包周围的作用域被称为其环境。为了消费捕获到的变量,闭包必须获取其所有权并在定义闭包时将其移动进闭包。其名称的Once部分代表了闭包不能多次获取相同变量的所有权的事实,所以它只能被调用一次。FnMut获取可变的借用值所以可以改变其环境。Fn从其环境获取不可变的借用值 大部分需要指定一个Fn系列trait bound的时候,可以从Fn开始,而编译器会根据闭包体中的情况告诉你是否需要FnMut或FnOnce。
-
ch13-03:函数式编程风格倾向于最小化可变状态的数量来使代码更简洁:
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {let mut results = Vec::new();for line in contents.lines() {if line.contains(query) {results.push(line);}}results}// ↓↓↓↓↓↓↓↓↓↓ 通过filter省略result中间变量pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {contents.lines().filter(|line: &&str| line.contains(query)).collect()} -
ch14-02:使用重导出功能可以重新组织希望暴露在外的API,编写者和使用者可以面对不同的API结构,如此,使用者无需了解crate内部结构即可方便的调用公开的API:
pub use self::kinds::PrimaryColor;pub use self::kinds::SecondaryColor;pub use self::utils::mix; -
ch14-03:按照教程做,添加
rand依赖时,保存后vscode自动运行cargo check,然后显示”fetch metadata”很长时间不动,手动运行cargo metadata,发现卡在Updating crates.io index下载很慢,只好祭出替换镜像源大法……在/Users/xxx/.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" -
ch15-01:除了数据被储存在堆上而不是栈上之外,box 没有性能损失。不过也没有很多额外的功能。它们多用于如下场景:
- 当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候。
- 当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候。
- 当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候。
-
ch15-02:Rust 在发现类型和 trait 实现满足三种情况时会进行解引用强制多态:
- 当
T: Deref<Target=U>时(即当T实现了Deref返回U时),从&T到&U。 - 当
T: DerefMut<Target=U>时从&mut T到&mut U。 - 当
T: Deref<Target=U>时从&mut T到&U。(只允许将可变借用转为不可变借用,反之则禁止)
- 当
-
ch15-06:
Rc::clone使Rc::strong_count加一,Rc::downgrade使Rc::weak_count加一。强引用共享不可变所有权,弱引用不涉及所有权。 -
ch16-01:rust标准库仅提供1:1线程,即语言线程映射系统线程,不存在运行时的线程管理损耗。
-
ch16-02:
mpsc(multiple producer, single consumer)的channel通过mpsc::Sender::clone(&tx)来实现多生产者:let (tx, rx) = mpsc::channel();let tx1 = mpsc::Sender::clone(&tx);thread::spawn(move || {let vals = vec![String::from("hi"),String::from("from"),String::from("the"),String::from("thread"),];for val in vals {tx1.send(val).unwrap();thread::sleep(Duration::from_secs(1));}});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 received in rx {println!("Got: {}", received);} -
ch16-03:十五章中使用
Rc::new(RefCell::new(x))提供Rc内部值的可变引用,类似的,多线程中使用Arc::new(Mutex::new(x))提供Arc内部值的可变引用。 -
ch16-04:
Send(允许在线程间转移所有权)和Sync(允许多线程访问)是标记trait,基本类型均具有该标记,有该标记的类型的复合类型也具有该标记,而Rc、RefCell等智能指针并不具有该标记,Arc、Mutex具有该标记。 -
ch17-03:
Option.take将获取Some中资源的所有权,然后为Option留下一个None:pub struct Post {state: Option<Box<dyn State>>,content: String,}if let Some(s) = self.state.take() {self.state = Some(s.request_review())}// 区别于下面unwrap实现,unwrap()会获取state的所有权并消费掉state,而这里的state是在可变引用self后的,// 所以unwrap无法获取state的所有权,自然无法消费这个state,也就无法通过编译了// 而take无需state的所有权,只需要从state引用中拿到Some中的内容,即Box指针的所有权并赋给s,同时给state这个Option类型留下一个Noneself.state = Some(self.state.unwrap().request_review()); -
ch18-03:match匹配的其他类型:
// 逻辑或match x {1 | 2 => println!("one or two"),3 => println!("three"),_ => println!("anything"),}// 匹配范围:仅能匹配数字和char类型match x {1..=5 => println!("one through five"),_ => println!("something else"),}match x {'a'..='j' => println!("early ASCII letter"),'k'..='z' => println!("late ASCII letter"),_ => println!("something else"),}解构结构体:
let p = Point { x: 0, y: 7 };let Point { x: a, y: b } = p;// 或let p = Point { x: 0, y: 7 };let Point { a, b } = p;解构并匹配:
let p = Point { x: 0, y: 7 };match p {Point { x, y: 0 } => println!("On the x axis at {}", x),Point { x: 0, y } => println!("On the y axis at {}", y),Point { x, y } => println!("On neither axis: ({}, {})", x, y),}解构枚举:
enum Message {Quit,Move { x: i32, y: i32 },Write(String),ChangeColor(i32, i32, i32),}fn main() {let msg = Message::ChangeColor(0, 160, 255);match msg {Message::Quit => {println!("The Quit variant has no data to destructure.")}Message::Move { x, y } => {println!("Move in the x direction {} and in the y direction {}",x,y);}Message::Write(text) => println!("Text message: {}", text),Message::ChangeColor(r, g, b) => {println!("Change the color to red {}, green {}, and blue {}",r,g,b)}}}解构嵌套的枚举和结构体:
enum Color {Rgb(i32, i32, i32),Hsv(i32, i32, i32),}enum Message {Quit,Move { x: i32, y: i32 },Write(String),ChangeColor(Color),}fn main() {let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));match msg {Message::ChangeColor(Color::Rgb(r, g, b)) => {println!("Change the color to red {}, green {}, and blue {}",r,g,b)}Message::ChangeColor(Color::Hsv(h, s, v)) => {println!("Change the color to hue {}, saturation {}, and value {}",h,s,v)}_ => ()}}解构嵌套的结构体和元组:
let ((feet, inches), Point {x, y}) = ((3, 10), Point { x: 3, y: -10 });变量命名使用下划线开头时,即使不使用该变量编译器也不会警告“变量未使用”。
使用
..忽略剩余值:let origin = Point { x: 0, y: 0, z: 0 };match origin {Point { x, .. } => println!("x is {}", x),}// 或let numbers = (2, 4, 8, 16, 32);match numbers {(first, .., last) => {println!("Some numbers: {}, {}", first, last);},}匹配守卫(match guard):
// 输出 less than five: 4let num = Some(4);match num {Some(x) if x < 5 => println!("less than five: {}", x),Some(x) => println!("{}", x),None => (),}// 使用匹配守卫解决match会引入新作用域的问题,match中的y覆盖了外部的yfn main() {let x = Some(5);let y = 10;match x {Some(50) => println!("Got 50"),Some(y) => println!("Matched, y = {:?}", y),_ => println!("Default case, x = {:?}", x),}println!("at the end: x = {:?}, y = {:?}", x, y);}// ↓↓↓↓↓↓↓↓↓↓ 利用匹配守卫就可以用到外部的y ↓↓↓↓↓↓↓↓↓↓fn main() {let x = Some(5);let y = 10;match x {Some(50) => println!("Got 50"),Some(n) if n == y => println!("Matched, n = {}", n),_ => println!("Default case, x = {:?}", x),}println!("at the end: x = {:?}, y = {}", x, y);}// 匹配守卫结合或条件,意为此分支值匹配x值为4、5或6同时y为true的情况,本例中y为false所以不会匹配首个条件:let x = 4;let y = false;match x {4 | 5 | 6 if y => println!("yes"),_ => println!("no"),}````@`运算符,本例希望测试`Message::Hello`的`id`字段是否位于`3..=7`范围内,同时也希望能将其值绑定到`id_variable`变量中以便此分支相关联的代码可以使用它(可以将`id_variable`命名为`id`,与字段同名,不过出于示例的目的这里选择了不同的名称):```rustenum 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)},} -
ch19-01:
unsafe的用法:// 不会崩溃的用法,因为裸指针范围恰好不重叠use std::slice;fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {let len = slice.len();let ptr = slice.as_mut_ptr();assert!(mid <= len);unsafe {(slice::from_raw_parts_mut(ptr, mid),slice::from_raw_parts_mut(ptr.add(mid), len - mid))}}// 可能会崩溃的用法,因为i32型的裸指针向后取一万个生成的序列,所用的内存并不在这段程序的掌控之中use std::slice;let address = 0x01234usize;let r = address as *mut i32;let slice: &[i32] = unsafe {slice::from_raw_parts_mut(r, 10000)};使用
extern调用外部代码,调用外部代码总被认为是unsafe的,比如调用C语言:extern "C" {fn abs(input: i32) -> i32;}fn main() {unsafe {println!("Absolute value of -3 according to C: {}", abs(-3));}}使用
extern编译成被外部代码调用的ABI:#[no_mangle]pub extern "C" fn call_from_c() {println!("Just called a Rust function from C!");}声明静态变量(全局变量),静态变量只能储存拥有
'static生命周期的引用,这意味着 Rust 编译器可以自己计算出其生命周期而无需显式标注。访问不可变静态变量是安全的。(常量与不可变静态变量可能看起来很类似,不过一个微妙的区别是静态变量中的值有一个固定的内存地址。使用这个值总是会访问相同的地址。另一方面,常量则允许在任何被用到的时候复制其数据。常量与静态变量的另一个区别在于静态变量可以是可变的。访问和修改可变静态变量都是不安全的)。声明并读取不可变静态变量:static HELLO_WORLD: &str = "Hello, world!";fn main() {println!("name is: {}", HELLO_WORLD);}声明、修改并读取可变静态变量:
static mut COUNTER: u32 = 0;fn add_to_count(inc: u32) {unsafe {COUNTER += inc;}}fn main() {add_to_count(3);unsafe {println!("COUNTER: {}", COUNTER);}}不安全的trait,典型场景为ch16-04中线程共享数据,如果实现了一个包含一些不是
Send或Sync的类型(如裸指针),并希望将此实现类型标记为Send或Sync,则必须使用unsafe。Rust不能验证我们的类型保证可以安全的跨线程发送或在多线程间访问,所以需要我们自己进行检查并通过unsafe表明。:unsafe trait Foo {// methods go here}unsafe impl Foo for i32 {// method implementations go here}union和struct类似,但是在一个实例中同时只能使用一个声明的字段。联合体主要用于和C代码中的联合体交互。访问联合体的字段是不安全的,因为 Rust 无法保证当前存储在联合体实例中数据的类型。不安全的使用场景:
- 解引用裸指针
- 调用不安全的函数或方法
- 访问或修改可变静态变量
- 实现不安全
trait - 访问
union的字段
-
ch19-03:高级trait中的关联类型的默认类型,其典型场景为:运算符重载时,符号两侧数据类型不同的情况(如“米+毫米”):
// 观察Add运算符trait的定义:trait Add<RHS=Self> {type Output;fn add(self, rhs: RHS) -> Self::Output;}// 运算符两端的数据类型相同时,不需要显示声明`Add`右侧数据类型,默认与左侧相同:use std::ops::Add;#[derive(Debug, PartialEq)]struct Point {x: i32,y: i32,}impl Add for Point {type Output = Point;fn add(self, other: Point) -> Point {Point {x: self.x + other.x,y: self.y + other.y,}}}fn main() {assert_eq!(Point { x: 1, y: 0 } + Point { x: 2, y: 3 },Point { x: 3, y: 3 });}// 左侧类型与右侧类型不同时,才需要显示声明类型:use std::ops::Add;struct Millimeters(u32);struct Meters(u32);impl Add<Meters> for Millimeters {type Output = Millimeters;fn add(self, other: Meters) -> Millimeters {Millimeters(self.0 + (other.0 * 1000))}}在trait中要求实现该trait的类型具有某些其他trait,类似于为trait添加”trait bound”,例如要求实现
OutlinePrint这一trait的类型具有Displaytrait,以便我们在outline_print()中调用to_string():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!("*{}*", " ".repeat(len + 2));println!("* {} *", output);println!("*{}*", " ".repeat(len + 2));println!("{}", "*".repeat(len + 4));}} -
ch19-05:将函数以参数的方式传递给另一个函数,除了传递闭包外,也可以传递
fn定义的函数,函数的类型是fn(使用小写的“f”)以免与Fn闭包trait相混淆。fn被称为 函数指针(function pointer):fn add_one(x: i32) -> i32 {x + 1}fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {f(arg) + f(arg)}fn main() {let answer = do_twice(add_one, 5);println!("The answer is: {}", answer);}返回一个闭包函数时,与往常一样,由于rust并不知道闭包需要多少空间来储存,因此需要返回指针包裹的闭包,如
Fntrait对象:fn returns_closure() -> Box<dyn Fn(i32) -> i32> {Box::new(|x| x + 1)} -
ch19-06:类似于
vec![1, 2, 3]的声明宏(declarative macros):// 简化版的vec!宏#[macro_export]macro_rules! vec {( $( $x:expr ),* ) => {{let mut temp_vec = Vec::new();$(temp_vec.push($x);)*temp_vec}};}// 调用这个宏vec![1, 2, 3]生成的代码类似于:let mut temp_vec = Vec::new();temp_vec.push(1);temp_vec.push(2);temp_vec.push(3);temp_vec过程宏(procedural macros)实现较复杂,我大概率只用不写。
类属性宏,示例有两个
TokenStream类型的参数;第一个用于属性内容本身,也就是GET, "/"部分。第二个是属性所标记的项:在本例中,是fn index() {}和剩下的函数体。除此之外,类属性宏与自定义派生宏工作方式一致:创建proc-macrocrate类型的crate并实现希望生成代码的函数:#[route(GET, "/")]fn index() {...}// ↓↓↓↓↓↓↓↓↓↓ 实现 ↓↓↓↓↓↓↓↓↓↓#[proc_macro_attribute]pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {...}类函数宏,定义看起来像函数调用的宏。类似于
macro_rules!,它们比函数更灵活(例如,可以接受未知数量的参数,macro_rules!宏只能使用前面vec!例子中匹配语法定义)。类函数宏获取TokenStream参数,其定义使用Rust代码操纵TokenStream,就像另两种过程宏一样。一个类函数宏例子是可以像这样被调用的sql!宏:let sql = sql!(SELECT * FROM posts WHERE id=1);// ↓↓↓↓↓↓↓↓↓↓ 实现 ↓↓↓↓↓↓↓↓↓↓#[proc_macro]pub fn sql(input: TokenStream) -> TokenStream {...} -
ch20-03:多线程webserver范例:目录结构大致为:
Terminal window hello├── 404.html├── Cargo.lock├── Cargo.toml├── hello.html├── src│ ├── bin│ │ └── main.rs│ └── lib.rs...这个范例通过实现线程模型间消息的传递、线程的结束等目标,利用了很多前面十几章提到的知识点:
-
利用
type缩写类型; -
通过
Option交换所有权; -
智能指针
Arc与Mutux在线程模型中的用法; -
如何在线程模型中将mpsc通道变为“spmc”;
-
通过观察标准库中的
thread::spawn(),模仿着编写一个类似的接受闭包指针且能够在线程模型中使用的函数; -
…
use std::{fs, net::{TcpListener, TcpStream}, thread, time::Duration};use hello::ThreadPool;use std::io::prelude::*;fn main() {let listener = TcpListener::bind("127.0.0.1:7878").unwrap();let pool = ThreadPool::new(4);for stream in listener.incoming() {let stream = stream.unwrap();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")};let contents = fs::read_to_string(filename).unwrap();let response = format!("{}{}", status_line, contents);stream.write(response.as_bytes()).unwrap();stream.flush().unwrap();}use std::{sync::{Arc, Mutex, mpsc}, thread};pub struct ThreadPool {workers: Vec<Worker>,sender: mpsc::Sender<Message>,}type Job = Box<dyn FnOnce() + Send + 'static>;enum Message {NewJob(Job),Terminate,}impl ThreadPool {/// 创建线程池////// `size`为线程池中线程的数量////// # Panics////// `new`函数在`size`为`0`时会panic。pub fn new(size: usize) -> ThreadPool {assert!(size > 0);let mut workers = Vec::with_capacity(size);let (sender, receiver) = mpsc::channel();// Arc指针负责提供线程间复制功能,Mutex指针负责提供线程间共享数据功能。let receiver = Arc::new(Mutex::new(receiver));for id in 0..size {workers.push(Worker::new(id, Arc::clone(&receiver)));}ThreadPool {workers,sender,}}pub fn execute<F>(&self, f: F)where F: FnOnce() + Send + 'static{let job = Box::new(f);self.sender.send(Message::NewJob(job)).unwrap();}}impl Drop for ThreadPool {fn drop(&mut self) {println!("Sending terminate message to all workers.");// 这里必须使用两个迭代发送终止消息,因为消息的发送无法针对特定worker// 使用同一个循环先发送终止消息再join,并不能保证收取消息的worker与join的worker是同一个workerfor _ in &self.workers {self.sender.send(Message::Terminate).unwrap();}println!("Shutting down all workers.");for worker in &mut self.workers {println!("Shutting down worker {}", worker.id);if let Some(thread) = worker.thread.take() {thread.join().unwrap();}}}}pub struct Worker {id: usize,// 使用Option的原因在于join线程时需要拿到句柄所有权后消费掉这个句柄,Option的take方法可以拿到Some中内容的所有权同时交还一个None。// 若不通过Option封装,则结构体的thread字段无法交出其中的句柄所有权,因为结构体不允许thread字段悬空。thread: Option<thread::JoinHandle<()>>,}impl Worker {fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Message>>>) -> Worker {let thread = thread::spawn(move || loop {let message = receiver.lock().unwrap().recv().unwrap();match message {Message::NewJob(job) => {println!("Worker {} got a job; executing.", id);job();},Message::Terminate => {println!("Worker {} was told to terminate.", id);break;}}});Worker {id,thread: Some(thread),}}}
-