Rust语言实战

通过有挑战性的示例、练习题、实践项目来提升 Rust 水平,建立从入门学习到上手实战的直通桥梁
Rust语言实战 的目标是通过大量的实战练习帮助大家更好的学习和上手使用 Rust 语言。书中的练习题非常易于使用:你所需的就是在线完成练习,并让它通过编译。
在线阅读
本地运行
我们使用 mdbook 构建在线练习题,你也可以下载到本地运行:
$ cargo install mdbook
$ cd rust-by-practice && mdbook serve
特性
部分示例和习题借鉴了 Rust By Example, 书中的示例真的非常棒!
尽管它们非常优秀,我们这本书也有自己的秘密武器 :)
-
每个章节分为三个可选部分:示例、练习和实践项目
-
除了示例外,我们还有大量的高质量练习题,你可以在线阅读、修改和编译它们
-
覆盖了 Rust 语言的几乎所有方面:基础语言特性、高级语言特性、async/await 异步编程、多线程、并发原语、性能优化、工具链使用、标准库、数据结构和算法等
-
每一道练习题都提供了解答
-
整体难度相对更高,更加贴近于实战难度: 简单 🌟 , 中等 🌟🌟 , 困难 🌟🌟🌟 , 地狱 🌟🌟🌟🌟
总之,我们想做的就是解决入门学习后,不知道该如何运用的问题,毕竟对于 Rust 来说,从学习到实战,中间还隔着数个 Go语言 的难度
关于我们
Rust语言实战 由 Rust 编程学院倾情打造。
同时我们还提供了一本目前最好也是最用心的开源 Rust 书籍 - Rust语言圣经, 适合从入门到精通所有阶段的学习,欢迎大家阅读使用。
对我们来说,来自读者大大的肯定比什么都重要,因此一个 Github star 要比一杯咖啡更让我们开心,而且现在它在跳楼打折,无需 998 , 仅需 0 元钱 :)
值得学习的小型项目
在国内外的各大 Rust 论坛上,以下问题非常常见:
- 作为 Rust 新手,有哪些项目值得推荐学习?
- 求推荐代码优雅的小型项目
- 有哪些值得推荐的简单、易读的项目
这些问题的答案往往只有一个,那就是实践:做一些优秀的练习题,然后阅读一些小而美的 Rust 项目。
这个恰恰跟本书的目标吻合,因此,我们决定收集一些优秀的资源,并在 Rust语言实战 中呈现给大家。
1. Ripgrep
以上的问题通常都会伴随着 ripgrep
的推荐, 虽然我不认为它是一个小型项目,但是依然非常推荐大家学习,当然,首先你得做好深挖的准备和耐心。
2. 教程:构建一个文本编辑器
该教程 https://www.philippflenker.com/hecto/
将带领我们从零开始构建一个文本编辑器.
3. Ncspot
Ncspot 是一个终端访问的 Spotify 客户端,小巧、简单、良好的代码组织以及异步编程,值得学习.
4. 命令行 Rust
这个项目 是书本 Command-Line Rust(O'Reily)
的配套项目,可以帮助大家理解该如何更好的编写命令行程序,例如 head
, cat
, ls
。
5. 在 PNG 中隐藏你的秘密
这本书 将带领大家编写一个命令行程序,功能是在 PNG 文件中隐藏一些秘密信息,首要目标是让我们熟悉 Rust 代码。
To be continued...
变量绑定与解构
绑定和可变性
- 🌟 变量只有在初始化后才能被使用
// 修复下面代码的错误并尽可能少的修改 fn main() { let x: i32; // 未初始化,但被使用 let y: i32; // 未初始化,也未被使用 println!("{} is equal to 5", x); }
- 🌟🌟 可以使用
mut
将变量标记为可变
// 完形填空,让代码编译 fn main() { let __ = 1; __ += 2; println!("{} = 3", x); }
变量作用域
- 🌟 作用域是一个变量在程序中能够保持合法的范围
// 修复下面代码的错误并使用尽可能少的改变 fn main() { let x: i32 = 10; { let y: i32 = 5; println!("x 的值是 {}, y 的值是 {}", x, y); } println!("x 的值是 {}, y 的值是 {}", x, y); }
- 🌟🌟
// 修复错误 fn main() { println!("{}, world", x); } fn define_x() { let x = "hello"; }
变量遮蔽( Shadowing )
- 🌟🌟 若后面的变量声明的名称和之前的变量相同,则我们说:第一个变量被第二个同名变量遮蔽了( shadowing )
// 只允许修改 `assert_eq!` 来让 `println!` 工作(在终端输出 `42`) fn main() { let x: i32 = 5; { let x = 12; assert_eq!(x, 5); } assert_eq!(x, 12); let x = 42; println!("{}", x); // 输出 "42". }
- 🌟🌟 删除一行代码以通过编译
fn main() { let mut x: i32 = 1; x = 7; // 遮蔽且再次绑定 let x = x; x += 3; let y = 4; // 遮蔽 let y = "I can also be bound to text!"; }
未使用的变量
- 使用以下方法来修复编译器输出的 warning :
- 🌟 一种方法
- 🌟🌟 两种方法
注意: 你可以使用两种方法解决,但是它们没有一种是移除
let x = 1
所在的代码行
fn main() { let x = 1; } // compiler warning: unused variable: `x`
变量解构
- 🌟🌟 我们可以将
let
跟一个模式一起使用来解构一个元组,最终将它解构为多个独立的变量
提示: 可以使用变量遮蔽或可变性
// 修复下面代码的错误并尽可能少的修改 fn main() { let (x, y) = (1, 2); x += 2; assert_eq!(x, 3); assert_eq!(y, 2); }
解构式赋值
该功能于 Rust 1.59 版本引入:你可以在赋值语句的左式中使用元组、切片或结构体进行匹配赋值。
- 🌟🌟
Note: 解构式赋值只能在 Rust 1.59 或者更高版本中使用
fn main() { let (x, y); (x,..) = (3, 4); [.., y] = [1, 2]; // 填空,让代码工作 assert_eq!([x,y], __); }
你可以在这里找到答案(在 solutions 路径下)
基本类型
学习资料:
- English: Rust Book 3.2 and 3.3
- 简体中文: Rust语言圣经 - 基本类型
数值类型
整数
- 🌟
Tips: 如果我们没有显式的给予变量一个类型,那编译器会自动帮我们推导一个类型
// 移除某个部分让代码工作 fn main() { let x: i32 = 5; let mut y: u32 = 5; y = x; let z = 10; // 这里 z 的类型是? }
- 🌟
// 填空 fn main() { let v: u16 = 38_u8 as __; }
- 🌟🌟🌟
Tips: 如果我们没有显式的给予变量一个类型,那编译器会自动帮我们推导一个类型
// 修改 `assert_eq!` 让代码工作 fn main() { let x = 5; assert_eq!("u32".to_string(), type_of(&x)); } // 以下函数可以获取传入参数的类型,并返回类型的字符串形式,例如 "i8", "u8", "i32", "u32" fn type_of<T>(_: &T) -> String { format!("{}", std::any::type_name::<T>()) }
- 🌟🌟
// 填空,让代码工作 fn main() { assert_eq!(i8::MAX, __); assert_eq!(u8::MAX, __); }
- 🌟🌟
// 解决代码中的错误和 `panic` fn main() { let v1 = 251_u8 + 8; let v2 = i8::checked_add(251, 8).unwrap(); println!("{},{}",v1,v2); }
- 🌟🌟
// 修改 `assert!` 让代码工作 fn main() { let v = 1_024 + 0xff + 0o77 + 0b1111_1111; assert!(v == 1579); }
浮点数
- 🌟
// 将 ? 替换成你的答案 fn main() { let x = 1_000.000_1; // ? let y: f32 = 0.12; // f32 let z = 0.01_f64; // f64 }
- 🌟🌟 使用两种方法来让下面代码工作
fn main() { assert!(0.1+0.2==0.3); }
序列Range
- 🌟🌟 两个目标: 1. 修改
assert!
让它工作 2. 让println!
输出: 97 - 122
fn main() { let mut sum = 0; for i in -3..2 { sum += i } assert!(sum == -3); for c in 'a'..='z' { println!("{}",c); } }
- 🌟🌟
// 填空 use std::ops::{Range, RangeInclusive}; fn main() { assert_eq!((1..__), Range{ start: 1, end: 5 }); assert_eq!((1..__), RangeInclusive::new(1, 5)); }
计算
- 🌟
// 填空,并解决错误 fn main() { // 整数加法 assert!(1u32 + 2 == __); // 整数减法 assert!(1i32 - 2 == __); assert!(1u8 - 2 == -1); assert!(3 * 50 == __); assert!(9.6 / 3.2 == 3.0); // error ! 修改它让代码工作 assert!(24 % 5 == __); // 逻辑与或非操作 assert!(true && false == __); assert!(true || false == __); assert!(!true == __); // 位操作 println!("0011 AND 0101 is {:04b}", 0b0011u32 & 0b0101); println!("0011 OR 0101 is {:04b}", 0b0011u32 | 0b0101); println!("0011 XOR 0101 is {:04b}", 0b0011u32 ^ 0b0101); println!("1 << 5 is {}", 1u32 << 5); println!("0x80 >> 2 is 0x{:x}", 0x80u32 >> 2); }
你可以在这里找到答案(在 solutions 路径下)
字符、布尔、单元类型
字符
- 🌟
use std::mem::size_of_val; fn main() { let c1 = 'a'; assert_eq!(size_of_val(&c1),1); let c2 = '中'; assert_eq!(size_of_val(&c2),3); println!("Success!") }
- 🌟
fn main() { let c1 = "中"; print_char(c1); } fn print_char(c : char) { println!("{}", c); }
布尔
- 🌟
// make println! work fn main() { let _f: bool = false; let t = true; if !t { println!("Success!") } }
- 🌟
fn main() { let f = true; let t = true && false; assert_eq!(t, f); println!("Success!") }
单元类型
- 🌟🌟
// 让代码工作,但不要修改 `implicitly_ret_unit` ! fn main() { let _v: () = (); let v = (2, 3); assert_eq!(v, implicitly_ret_unit()) println!("Success!") } fn implicitly_ret_unit() { println!("I will returen a ()") } // 不要使用下面的函数,它只用于演示! fn explicitly_ret_unit() -> () { println!("I will returen a ()") }
- 🌟🌟 单元类型占用的内存大小是多少?
// 让代码工作:修改 `assert!` 中的 `4` use std::mem::size_of_val; fn main() { let unit: () = (); assert!(size_of_val(&unit) == 4); println!("Success!") }
你可以在这里找到答案(在 solutions 路径下)
语句与表达式
示例
fn main() { let x = 5u32; let y = { let x_squared = x * x; let x_cube = x_squared * x; // 下面表达式的值将被赋给 `y` x_cube + x_squared + x }; let z = { // 分号让表达式变成了语句,因此返回的不再是表达式 `2 * x` 的值,而是语句的值 `()` 2 * x; }; println!("x is {:?}", x); println!("y is {:?}", y); println!("z is {:?}", z); }
练习
- 🌟🌟
// 使用两种方法让代码工作起来 fn main() { let v = { let mut x = 1; x += 2 }; assert_eq!(v, 3); }
- 🌟
fn main() { let v = (let x = 3); assert!(v == 3); }
- 🌟
fn main() { let s = sum(1 , 2); assert_eq!(s, 3); } fn sum(x: i32, y: i32) -> i32 { x + y; }
你可以在这里找到答案(在 solutions 路径下)
函数
- 🌟🌟🌟
fn main() { // 不要修改下面两行代码! let (x, y) = (1, 2); let s = sum(x, y); assert_eq!(s, 3); } fn sum(x, y: i32) { x + y; }
- 🌟🌟
fn main() { print(); } // 使用另一个类型来替代 i32 fn print() -> i32 { println!("hello,world"); }
- 🌟🌟🌟
// 用两种方法求解 fn main() { never_return(); } fn never_return() -> ! { // 实现这个函数,不要修改函数签名! }
- 🌟🌟 发散函数( Diverging function )不会返回任何值,因此它们可以用于替代需要返回任何值的地方
fn main() { println!("Success!"); } fn get_option(tp: u8) -> Option<i32> { match tp { 1 => { // TODO } _ => { // TODO } }; // 这里与其返回一个 None,不如使用发散函数替代 never_return_fn() } // 使用三种方法实现以下发散函数 fn never_return_fn() -> ! { }
- 🌟🌟
fn main() { // 填空 let b = __; let v = match b { true => 1, // 发散函数也可以用于 `match` 表达式,用于替代任何类型的值 false => { println!("Success!"); panic!("we have no value for `false`, but we can panic") } }; println!("Excercise Failed if printing out this line!"); }
你可以在这里找到答案(在 solutions 路径下)
所有权与借用
学习资料 :
- English: Rust Book 4.1-4.4
- 简体中文: [Rust语言圣经 - 所有权与借用(https://course.rs/basic/ownership/index.html)
所有权
- 🌟🌟
fn main() { // 使用尽可能多的方法来通过编译 let x = String::from("hello, world"); let y = x; println!("{},{}",x,y); }
- 🌟🌟
// 不要修改 main 中的代码 fn main() { let s1 = String::from("hello, world"); let s2 = take_ownership(s1); println!("{}", s2); } // 只能修改下面的代码! fn take_ownership(s: String) { println!("{}", s); }
- 🌟🌟
fn main() { let s = give_ownership(); println!("{}", s); } // 只能修改下面的代码! fn give_ownership() -> String { let s = String::from("hello, world"); // convert String to Vec // 将 String 转换成 Vec 类型 let _s = s.into_bytes(); s }
- 🌟🌟
// 修复错误,不要删除任何代码行 fn main() { let s = String::from("hello, world"); print_str(s); println!("{}", s); } fn print_str(s: String) { println!("{}",s) }
- 🌟🌟
// 不要使用 clone,使用 copy 的方式替代 fn main() { let x = (1, 2, (), "hello".to_string()); let y = x.clone(); println!("{:?}, {:?}", x, y); }
可变性
当所有权转移时,可变性也可以随之改变。
- 🌟
fn main() { let s = String::from("hello, "); // 只修改下面这行代码 ! let s1 = s; s1.push_str("world") }
- 🌟🌟🌟
fn main() { let x = Box::new(5); let ... // 完成该行代码,不要修改其它行! *y = 4; assert_eq!(*x, 5); }
部分 move
当解构一个变量时,可以同时使用 move
和引用模式绑定的方式。当这么做时,部分 move
就会发生:变量中一部分的所有权被转移给其它变量,而另一部分我们获取了它的引用。
在这种情况下,原变量将无法再被使用,但是它没有转移所有权的那一部分依然可以使用,也就是之前被引用的那部分。
示例
fn main() { #[derive(Debug)] struct Person { name: String, age: Box<u8>, } let person = Person { name: String::from("Alice"), age: Box::new(20), }; // 通过这种解构式模式匹配,person.name 的所有权被转移给新的变量 `name` // 但是,这里 `age` 变量确是对 person.age 的引用, 这里 ref 的使用相当于: let age = &person.age let Person { name, ref age } = person; println!("The person's age is {}", age); println!("The person's name is {}", name); // Error! 原因是 person 的一部分已经被转移了所有权,因此我们无法再使用它 //println!("The person struct is {:?}", person); // 虽然 `person` 作为一个整体无法再被使用,但是 `person.age` 依然可以使用 println!("The person's age from person struct is {}", person.age); }
练习
- 🌟
fn main() { let t = (String::from("hello"), String::from("world")); let _s = t.0; // 仅修改下面这行代码,且不要使用 `_s` println!("{:?}", t); }
- 🌟🌟
fn main() { let t = (String::from("hello"), String::from("world")); // 填空,不要修改其它代码 let (__, __) = __; println!("{:?}, {:?}, {:?}", s1, s2, t); // -> "hello", "world", ("hello", "world") }
你可以在这里找到答案(在 solutions 路径下)
引用和借用
引用
- 🌟
fn main() { let x = 5; // 填写空白处 let p = __; println!("x 的内存地址是 {:p}", p); // output: 0x16fa3ac84 }
- 🌟
fn main() { let x = 5; let y = &x; // 只能修改以下行 assert_eq!(5, y); }
- 🌟
// 修复错误 fn main() { let mut s = String::from("hello, "); borrow_object(s) } fn borrow_object(s: &String) {}
- 🌟
// 修复错误 fn main() { let mut s = String::from("hello, "); push_str(s) } fn push_str(s: &mut String) { s.push_str("world") }
- 🌟🌟
fn main() { let mut s = String::from("hello, "); // 填写空白处,让代码工作 let p = __; p.push_str("world"); }
ref
ref
与 &
类似,可以用来获取一个值的引用,但是它们的用法有所不同。
- 🌟🌟🌟
fn main() { let c = '中'; let r1 = &c; // 填写空白处,但是不要修改其它行的代码 let __ r2 = c; assert_eq!(*r1, *r2); // 判断两个内存地址的字符串是否相等 assert_eq!(get_addr(r1),get_addr(r2)); } // 获取传入引用的内存地址的字符串形式 fn get_addr(r: &char) -> String { format!("{:p}", r) }
借用规则
- 🌟
// 移除代码某个部分,让它工作 // 你不能移除整行的代码! fn main() { let mut s = String::from("hello"); let r1 = &mut s; let r2 = &mut s; println!("{}, {}", r1, r2); }
可变性
- 🌟 错误: 从不可变对象借用可变
fn main() { // 通过修改下面一行代码来修复错误 let s = String::from("hello, "); borrow_object(&mut s) } fn borrow_object(s: &mut String) {}
- 🌟🌟 Ok: 从可变对象借用不可变
// 下面的代码没有任何错误 fn main() { let mut s = String::from("hello, "); borrow_object(&s); s.push_str("world"); } fn borrow_object(s: &String) {}
NLL
- 🌟🌟
// 注释掉一行代码让它工作 fn main() { let mut s = String::from("hello, "); let r1 = &mut s; r1.push_str("world"); let r2 = &mut s; r2.push_str("!"); println!("{}",r1); }
- 🌟🌟
fn main() { let mut s = String::from("hello, "); let r1 = &mut s; let r2 = &mut s; // 在下面增加一行代码人为制造编译错误:cannot borrow `s` as mutable more than once at a time // 你不能同时使用 r1 和 r2 }
你可以在这里找到答案(在 solutions 路径下)
复合类型
学习资料:
- English: Rust Book 4.3, 5.1, 6.1, 8.2
- 简体中文: Rust语言圣经 - 复合类型
字符串
字符串字面量的类型是 &str
, 例如 let s: &str = "hello, world"
中的 "hello, world"
的类型就是 &str
。
str
和 &str
- 🌟 正常情况下我们无法使用
str
类型,但是可以使用&str
来替代
// 修复错误,不要新增代码行 fn main() { let s: str = "hello, world"; }
- 🌟🌟 如果要使用
str
类型,只能配合Box
。&
可以用来将Box<str>
转换为&str
类型
// 使用至少两种方法来修复错误 fn main() { let s: Box<str> = "hello, world".into(); greetings(s) } fn greetings(s: &str) { println!("{}",s) }
String
String
是定义在标准库中的类型,分配在堆上,可以动态的增长。它的底层存储是动态字节数组的方式( Vec<u8>
),但是与字节数组不同,String
是 UTF-8
编码。
- 🌟
// 填空 fn main() { let mut s = __; s.push_str("hello, world"); s.push('!'); assert_eq!(s, "hello, world!"); }
- 🌟🌟🌟
// 修复所有错误,并且不要新增代码行 fn main() { let s = String::from("hello"); s.push(','); s.push(" world"); s += "!".to_string(); println!("{}", s) }
- 🌟🌟 我们可以用
replace
方法来替换指定的子字符串
// 填空 fn main() { let s = String::from("I like dogs"); // 以下方法会重新分配一块内存空间,然后将修改后的字符串存在这里 let s1 = s.__("dogs", "cats"); assert_eq!(s1, "I like cats") }
在标准库的 String 模块中,有更多的实用方法,感兴趣的同学可以看看。
- 🌟🌟 你只能将
String
跟&str
类型进行拼接,并且String
的所有权在此过程中会被 move
// 修复所有错误,不要删除任何一行代码 fn main() { let s1 = String::from("hello,"); let s2 = String::from("world!"); let s3 = s1 + s2; assert_eq!(s3,"hello,world!"); println!("{}",s1); }
&str
和 String
与 str
的很少使用相比,&str
和 String
类型却非常常用,因此也非常重要。
- 🌟🌟 我们可以使用两种方法将
&str
转换成String
类型
// 使用至少两种方法来修复错误 fn main() { let s = "hello, world"; greetings(s) } fn greetings(s: String) { println!("{}",s) }
- 🌟🌟 我们可以使用
String::from
或to_string
将&str
转换成String
类型
// 使用两种方法来解决错误,不要新增代码行 fn main() { let s = "hello, world".to_string(); let s1: &str = s; }
字符串转义
- 🌟
fn main() { // 你可以使用转义的方式来输出想要的字符,这里我们使用十六进制的值,例如 \x73 会被转义成小写字母 's' // 填空以输出 "I'm writing Rust" let byte_escape = "I'm writing Ru\x73__!"; println!("What are you doing\x3F (\\x3F means ?) {}", byte_escape); // 也可以使用 Unicode 形式的转义字符 let unicode_codepoint = "\u{211D}"; let character_name = "\"DOUBLE-STRUCK CAPITAL R\""; println!("Unicode character {} (U+211D) is called {}", unicode_codepoint, character_name ); // 还能使用 \ 来连接多行字符串 let long_string = "String literals can span multiple lines. The linebreak and indentation here \ can be escaped too!"; println!("{}", long_string); }
- 🌟🌟🌟 有时候需要转义的字符很多,我们会希望使用更方便的方式来书写字符串: raw string.
fn main() { let raw_str = r"Escapes don't work here: \x3F \u{211D}"; // 修改以下代码行,让它工作 assert_eq!(raw_str, "Escapes don't work here: ? ℝ"); // 如果你希望在字符串中使用双引号,可以使用以下形式 let quotes = r#"And then I said: "There is no escape!""#; println!("{}", quotes); // 如果希望在字符串中使用 # 号,可以如下使用: let delimiter = r###"A string with "# in it. And even "##!"###; println!("{}", delimiter); // 填空 let long_delimiter = __; assert_eq!(long_delimiter, "Hello, \"##\"") }
字节字符串
想要一个非 UTF-8 形式的字符串吗(我们之前的 str
, &str
, String
都是 UTF-8 字符串) ? 可以试试字节字符串或者说字节数组:
示例:
use std::str; fn main() { // 注意,这并不是 `&str` 类型了! let bytestring: &[u8; 21] = b"this is a byte string"; // 字节数组没有实现 `Display` 特征,因此只能使用 `Debug` 的方式去打印 println!("A byte string: {:?}", bytestring); // 字节数组也可以使用转义 let escaped = b"\x52\x75\x73\x74 as bytes"; // ...但是不支持 unicode 转义 // let escaped = b"\u{211D} is not allowed"; println!("Some escaped bytes: {:?}", escaped); // raw string let raw_bytestring = br"\u{211D} is not escaped here"; println!("{:?}", raw_bytestring); // 将字节数组转成 `str` 类型可能会失败 if let Ok(my_str) = str::from_utf8(raw_bytestring) { println!("And the same as text: '{}'", my_str); } let _quotes = br#"You can also use "fancier" formatting, \ like with normal raw strings"#; // 字节数组可以不是 UTF-8 格式 let shift_jis = b"\x82\xe6\x82\xa8\x82\xb1\x82\xbb"; // "ようこそ" in SHIFT-JIS // 但是它们未必能转换成 `str` 类型 match str::from_utf8(shift_jis) { Ok(my_str) => println!("Conversion successful: '{}'", my_str), Err(e) => println!("Conversion failed: {:?}", e), }; }
如果大家想要了解更多关于字符串字面量、转义字符的话,可以看看 Rust Reference 的 'Tokens' 章节.
字符串索引string index
- 🌟🌟 你无法通过索引的方式去访问字符串中的某个字符,但是可以使用切片的方式
&s1[start..end]
,但是start
和end
必须准确落在字符的边界处.
fn main() { let s1 = String::from("hi,中国"); let h = s1[0]; // 修改当前行来修复错误,提示: `h` 字符在 UTF-8 格式中只需要 1 个字节来表示 assert_eq!(h, "h"); let h1 = &s1[3..5];// 修改当前行来修复错误,提示: `中` 字符在 UTF-8 格式中需要 3 个字节来表示 assert_eq!(h1, "中"); }
操作 UTF-8 字符串
- 🌟
fn main() { // 填空,打印出 "你好,世界" 中的每一个字符 for c in "你好,世界".__ { println!("{}", c) } }
utf8_slice
我们可以使用三方库 utf8_slice 来访问 UTF-8 字符串的某个子串,但是与之前不同的是,该库索引的是字符,而不是字节.
Example
use utf8_slice; fn main() { let s = "The 🚀 goes to the 🌑!"; let rocket = utf8_slice::slice(s, 4, 5); // 结果是 "🚀" }
你可以在这里找到答案(在 solutions 路径下)
数组
数组的类型是 [T; Lengh]
, 就如你所看到的,数组的长度是类型签名的一部分,因此数组的长度必须在编译期就已知,例如你不能使用以下方式来声明一个数组:
#![allow(unused)] fn main() { fn create_arr(n: i32) { let arr = [1; n]; } }
以上函数将报错,因为编译器无法在编译期知道 n
的具体大小。
- 🌟
fn main() { // 使用合适的类型填空 let arr: __ = [1, 2, 3, 4, 5]; // 修改以下代码,让它顺利运行 assert!(arr.len() == 4); }
- 🌟🌟
fn main() { // 很多时候,我们可以忽略数组的部分类型,也可以忽略全部类型,让编译器帮助我们推导 let arr0 = [1, 2, 3]; let arr: [_; 3] = ['a', 'b', 'c']; // 填空 // 数组分配在栈上, `std::mem::size_of_val` 函数会返回整个数组占用的内存空间 // 数组中的每个 char 元素占用 4 字节的内存空间,因为在 Rust 中, char 是 Unicode 字符 assert!(std::mem::size_of_val(&arr) == __); }
- 🌟 数组中的所有元素可以一起初始化为同一个值
fn main() { // 填空 let list: [i32; 100] = __ ; assert!(list[0] == 1); assert!(list.len() == 100); }
- 🌟 数组中的所有元素必须是同一类型
fn main() { // 修复错误 let _arr = [1, 2, '3']; }
- 🌟 数组的下标索引从 0 开始.
fn main() { let arr = ['a', 'b', 'c']; let ele = arr[1]; // 只修改此行来让代码工作 assert!(ele == 'a'); }
- 🌟 越界索引会导致代码的
panic
.
// 修复代码中的错误 fn main() { let names = [String::from("Sunfei"), "Sunface".to_string()]; // `get` 返回 `Option<T>` 类型,因此它的使用非常安全 let name0 = names.get(0).unwrap(); // 但是下标索引就存在越界的风险了 let _name1 = &names[2]; }
你可以在这里找到答案(在 solutions 路径下)
切片( Slice )
切片跟数组相似,但是切片的长度无法在编译期得知,因此你无法直接使用切片类型。
- 🌟🌟 这里,
[i32]
和str
都是切片类型,但是直接使用它们会造成编译错误,如下代码所示。为了解决,你需要使用切片的引用:&[i32]
,&str
.
// 修复代码中的错误,不要新增代码行! fn main() { let arr = [1, 2, 3]; let s1: [i32] = arr[0..2]; let s2: str = "hello, world" as str; }
一个切片引用占用了2个字大小的内存空间( 从现在开始,为了简洁性考虑,如无特殊原因,我们统一使用切片来特指切片引用 )。 该切片的第一个字是指向数据的指针,第二个字是切片的长度。字的大小取决于处理器架构,例如在 x86-64
上,字的大小是 64 位也就是 8 个字节,那么一个切片引用就是 16 个字节大小。
切片( 引用 )可以用来借用数组的某个连续的部分,对应的签名是 &[T]
,大家可以与数组的签名对比下 [T; Length]
。
- 🌟🌟🌟
fn main() { let arr: [char; 3] = ['中', '国', '人']; let slice = &arr[..2]; // 修改数字 `6` 让代码工作 // 小提示: 切片和数组不一样,它是引用。如果是数组的话,那下面的 `assert!` 将会通过: 因为'中'和'国'是 UTF-8 字符,它们每个占用 3 个字节,2 个字符就是 6 个字节 assert!(std::mem::size_of_val(&slice) == 6); }
- 🌟🌟
fn main() { let arr: [i32; 5] = [1, 2, 3, 4, 5]; // 填空让代码工作起来 let slice: __ = __; assert_eq!(slice, &[2, 3, 4]); }
字符串切片
- 🌟
fn main() { let s = String::from("hello"); let slice1 = &s[0..2]; // 填空,不要再使用 0..2 let slice2 = &s[__]; assert_eq!(slice1, slice2); }
- 🌟
fn main() { let s = "你好,世界"; // 修改以下代码行,让代码工作起来 let slice = &s[0..2]; assert!(slice == "你"); }
- 🌟🌟
&String
可以被隐式地转换成&str
类型.
// 修复所有错误 fn main() { let mut s = String::from("hello world"); // 这里, &s 是 `&String` 类型,但是 `first_word` 函数需要的是 `&str` 类型。 // 尽管两个类型不一样,但是代码仍然可以工作,原因是 `&String` 会被隐式地转换成 `&str` 类型,如果大家想要知道更多,可以看看 Deref 章节: https://course.rs/advance/smart-pointer/deref.html let word = first_word(&s); s.clear(); // error! println!("the first word is: {}", word); } fn first_word(s: &str) -> &str { &s[..1] }
你可以在这里找到答案(在 solutions 路径下)
元组( Tuple )
- 🌟 元组中的元素可以是不同的类型。元组的类型签名是
(T1, T2, ...)
, 这里T1
,T2
是相对应的元组成员的类型.
fn main() { let _t0: (u8,i16) = (0, -1); // 元组的成员还可以是一个元组 let _t1: (u8, (i16, u32)) = (0, (-1, 1)); // 填空让代码工作 let t: (u8, __, i64, __, __) = (1u8, 2u16, 3i64, "hello", String::from(", world")); }
- 🌟 可以使用索引来获取元组的成员
// 修改合适的地方,让代码工作 fn main() { let t = ("i", "am", "sunface"); assert_eq!(t.1, "sunface"); }
- 🌟 过长的元组无法被打印输出
// 修复代码错误 fn main() { let too_long_tuple = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13); println!("too long tuple: {:?}", too_long_tuple); }
- 🌟 使用模式匹配来解构元组
fn main() { let tup = (1, 6.4, "hello"); // 填空 let __ = tup; assert_eq!(x, 1); assert_eq!(y, "hello"); assert_eq!(z, 6.4); }
- 🌟🌟 解构式赋值
fn main() { let (x, y, z); // 填空 __ = (1, 2, 3); assert_eq!(x, 3); assert_eq!(y, 1); assert_eq!(z, 2); }
- 🌟🌟 元组可以用于函数的参数和返回值
fn main() { // 填空,需要稍微计算下 let (x, y) = sum_multiply(__); assert_eq!(x, 5); assert_eq!(y, 6); } fn sum_multiply(nums: (i32, i32)) -> (i32, i32) { (nums.0 + nums.1, nums.0 * nums.1) }
你可以在这里找到答案(在 solutions 路径下)
结构体
三种类型的结构体
- 🌟 对于结构体,我们必须为其中的每一个字段都指定具体的值
// fix the error struct Person { name: String, age: u8, hobby: String } fn main() { let age = 30; let p = Person { name: String::from("sunface"), age, }; }
- 🌟 单元结构体没有任何字段。
struct Unit; trait SomeTrait { // ...定义一些行为 } // 我们并不关心结构体中有什么数据( 字段 ),但我们关心它的行为。 // 因此这里我们使用没有任何字段的单元结构体,然后为它实现一些行为 impl SomeTrait for Unit { } fn main() { let u = Unit; do_something_with_unit(u); } // 填空,让代码工作 fn do_something_with_unit(u: __) { }
- 🌟🌟🌟 元组结构体看起来跟元组很像,但是它拥有一个结构体的名称,该名称可以赋予它一定的意义。由于它并不关心内部数据到底是什么名称,因此此时元组结构体就非常适合。
// 填空并修复错误 struct Color(i32, i32, i32); struct Point(i32, i32, i32); fn main() { let v = Point(__, __, __); check_color(v); } fn check_color(p: Color) { let (x, _, _) = p; assert_eq!(x, 0); assert_eq!(p.1, 127); assert_eq!(__, 255); }
结构体上的一些操作
- 🌟 你可以在实例化一个结构体时将它整体标记为可变的,但是 Rust 不允许我们将结构体的某个字段专门指定为可变的.
// 填空并修复错误,不要增加或移除代码行 struct Person { name: String, age: u8, } fn main() { let age = 18; let p = Person { name: String::from("sunface"), age, }; // how can you believe sunface is only 18? p.age = 30; // 填空 __ = String::from("sunfei"); }
- 🌟 使用结构体字段初始化缩略语法可以减少一些重复代码
// 填空 struct Person { name: String, age: u8, } fn main() {} fn build_person(name: String, age: u8) -> Person { Person { age, __ } }
- 🌟 你可以使用结构体更新语法基于一个结构体实例来构造另一个
// 填空,让代码工作 struct User { active: bool, username: String, email: String, sign_in_count: u64, } fn main() { let u1 = User { email: String::from("someone@example.com"), username: String::from("sunface"), active: true, sign_in_count: 1, }; let u2 = set_email(u1); } fn set_email(u: User) -> User { User { email: String::from("contact@im.dev"), __ } }
打印结构体
- 🌟🌟 我们可以使用
#[derive(Debug)]
让结构体变成可打印的.
// 填空,让代码工作 #[__] struct Rectangle { width: u32, height: u32, } fn main() { let scale = 2; let rect1 = Rectangle { width: dbg!(30 * scale), // 打印 debug 信息到标准错误输出 stderr,并将 `30 * scale` 的值赋给 `width` height: 50, }; dbg!(&rect1); // 打印 debug 信息到标准错误输出 stderr println!(__, rect1); // 打印 debug 信息到标准输出 stdout }
结构体的所有权
当解构一个变量时,可以同时使用 move
和引用模式绑定的方式。当这么做时,部分 move
就会发生:变量中一部分的所有权被转移给其它变量,而另一部分我们获取了它的引用。
在这种情况下,原变量将无法再被使用,但是它没有转移所有权的那一部分依然可以使用,也就是之前被引用的那部分。
示例
fn main() { #[derive(Debug)] struct Person { name: String, age: Box<u8>, } let person = Person { name: String::from("Alice"), age: Box::new(20), }; // 通过这种解构式模式匹配,person.name 的所有权被转移给新的变量 `name` // 但是,这里 `age` 变量确是对 person.age 的引用, 这里 ref 的使用相当于: let age = &person.age let Person { name, ref age } = person; println!("The person's age is {}", age); println!("The person's name is {}", name); // Error! 原因是 person 的一部分已经被转移了所有权,因此我们无法再使用它 //println!("The person struct is {:?}", person); // 虽然 `person` 作为一个整体无法再被使用,但是 `person.age` 依然可以使用 println!("The person's age from person struct is {}", person.age); }
练习
- 🌟🌟
// 修复错误 #[derive(Debug)] struct File { name: String, data: String, } fn main() { let f = File { name: String::from("readme.md"), data: "Rust By Practice".to_string() }; let _name = f.name; // 只能修改这一行 println!("{}, {}, {:?}",f.name, f.data, f); }
你可以在这里找到答案(在 solutions 路径下)
枚举 Enum
- 🌟🌟 在创建枚举时,你可以使用显式的整数设定枚举成员的值。
// 修复错误 enum Number { Zero, One, Two, } enum Number1 { Zero = 0, One, Two, } // C语言风格的枚举定义 enum Number2 { Zero = 0.0, One = 1.0, Two = 2.0, } fn main() { // 通过 `as` 可以将枚举值强转为整数类型 assert_eq!(Number::One, Number1::One); assert_eq!(Number1::One, Number2::One); }
- 🌟 枚举成员可以持有各种类型的值
// 填空 enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } fn main() { let msg1 = Message::Move{__}; // 使用x = 1, y = 2 来初始化 let msg2 = Message::Write(__); // 使用 "hello, world!" 来初始化 }
- 🌟🌟 枚举成员中的值可以使用模式匹配来获取
// 仅填空并修复错误 enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } fn main() { let msg = Message::Move{x: 1, y: 2}; if let Message::Move{__} = msg { assert_eq!(a, b); } else { panic!("不要让这行代码运行!"); } }
- 🌟🌟 使用枚举对类型进行同一化
// 填空,并修复错误 enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } fn main() { let msgs: __ = [ Message::Quit, Message::Move{x:1, y:3}, Message::ChangeColor(255,255,0) ]; for msg in msgs { show_message(msg) } } fn show_message(msg: Message) { println!("{}", msg); }
- 🌟🌟 Rust 中没有
null
,我们通过Option<T>
枚举来处理值为空的情况
// 填空让 `println` 输出,同时添加一些代码不要让最后一行的 `panic` 执行到 fn main() { let five = Some(5); let six = plus_one(five); let none = plus_one(None); if let __ = six { println!("{}", n) } panic!("不要让这行代码运行!"); } fn plus_one(x: Option<i32>) -> Option<i32> { match x { __ => None, __ => Some(i + 1), } }
- 🌟🌟🌟🌟 使用枚举来实现链表.
// 填空,让代码运行 use crate::List::*; enum List { // Cons: 链表中包含有值的节点,节点是元组类型,第一个元素是节点的值,第二个元素是指向下一个节点的指针 Cons(u32, Box<List>), // Nil: 链表中的最后一个节点,用于说明链表的结束 Nil, } // 为枚举实现一些方法 impl List { // 创建空的链表 fn new() -> List { // 因为没有节点,所以直接返回 Nil 节点 // 枚举成员 Nil 的类型是 List Nil } // 在老的链表前面新增一个节点,并返回新的链表 fn prepend(self, elem: u32) -> __ { Cons(elem, Box::new(self)) } // 返回链表的长度 fn len(&self) -> u32 { match *self { // 这里我们不能拿走 tail 的所有权,因此需要获取它的引用 Cons(_, __ tail) => 1 + tail.len(), // 空链表的长度为 0 Nil => 0 } } // 返回链表的字符串表现形式,用于打印输出 fn stringify(&self) -> String { match *self { Cons(head, ref tail) => { // 递归生成字符串 format!("{}, {}", head, tail.__()) }, Nil => { format!("Nil") }, } } } fn main() { // 创建一个新的链表(也是空的) let mut list = List::new(); // 添加一些元素 list = list.prepend(1); list = list.prepend(2); list = list.prepend(3); // 打印列表的当前状态 println!("链表的长度是: {}", list.len()); println!("{}", list.stringify()); }
你可以在这里找到答案(在 solutions 路径下)
流程控制
if/else
- 🌟
// 填空 fn main() { let n = 5; if n < 0 { println!("{} is negative", n); } __ n > 0 { println!("{} is positive", n); } __ { println!("{} is zero", n); } }
- 🌟🌟 if/else 可以用作表达式来进行赋值
// 修复错误 fn main() { let n = 5; let big_n = if n < 10 && n > -10 { println!(" 数字太小,先增加 10 倍再说"); 10 * n } else { println!("数字太大,我们得让它减半"); n / 2.0 ; } println!("{} -> {}", n, big_n); }
for
- 🌟
for in
可以用于迭代一个迭代器,例如序列a..b
.
fn main() { for n in 1..=100 { // 修改此行,让代码工作 if n == 100 { panic!("NEVER LET THIS RUN") } } }
- 🌟🌟
// 修复错误,不要新增或删除代码行 fn main() { let names = [String::from("liming"),String::from("hanmeimei")]; for name in names { // do something with name... } println!("{:?}", names); let numbers = [1, 2, 3]; // numbers中的元素实现了 Copy,因此无需转移所有权 for n in numbers { // do something with name... } println!("{:?}", numbers); }
- 🌟
fn main() { let a = [4,3,2,1]; // 通过索引和值的方式迭代数组 `a` for (i,v) in a.__ { println!("第{}个元素是{}",i+1,v); } }
while
- 🌟🌟 当条件为 true 时,
while
将一直循环
// 填空,让最后一行的 println! 工作 ! fn main() { // 一个计数值 let mut n = 1; // 当条件为真时,不停的循环 while n __ 10 { if n % 15 == 0 { println!("fizzbuzz"); } else if n % 3 == 0 { println!("fizz"); } else if n % 5 == 0 { println!("buzz"); } else { println!("{}", n); } __; } println!("n 的值是 {}, 循环结束",n); }
continue and break
- 🌟 使用
break
可以跳出循环
// 填空,不要修改其它代码 fn main() { let mut n = 0; for i in 0..=100 { if n == 66 { __ } n += 1; } assert_eq!(n, 66); }
- 🌟🌟
continue
会结束当次循环并立即开始下一次循环
// 填空,不要修改其它代码 fn main() { let mut n = 0; for i in 0..=100 { if n != 66 { n+=1; __; } __ } assert_eq!(n, 66); }
loop
- 🌟🌟 loop 一般都需要配合
break
或continue
一起使用。
// 填空,不要修改其它代码 fn main() { let mut count = 0u32; println!("Let's count until infinity!"); // 无限循环 loop { count += 1; if count == 3 { println!("three"); // 跳过当此循环的剩余代码 __; } println!("{}", count); if count == 5 { println!("OK, that's enough"); __; } } assert_eq!(count, 5); }
- 🌟🌟 loop 是一个表达式,因此我们可以配合
break
来返回一个值
// 填空 fn main() { let mut counter = 0; let result = loop { counter += 1; if counter == 10 { __; } }; assert_eq!(result, 20); }
- 🌟🌟🌟 当有多层循环时,你可以使用
continue
或break
来控制外层的循环。要实现这一点,外部的循环必须拥有一个标签'label
, 然后在break
或continue
时指定该标签
// 填空 fn main() { let mut count = 0; 'outer: loop { 'inner1: loop { if count >= 20 { // 这只会跳出 inner1 循环 break 'inner1; // 这里使用 `break` 也是一样的 } count += 2; } count += 5; 'inner2: loop { if count >= 30 { break 'outer; } continue 'outer; } } assert!(count == __) }
你可以在这里找到答案(在 solutions 路径下)
Pattern Match
match, matches! 和 if let
match
- 🌟🌟
// 填空 enum Direction { East, West, North, South, } fn main() { let dire = Direction::South; match dire { Direction::East => println!("East"), __ => { // 在这里匹配 South 或 North println!("South or North"); }, _ => println!(__), }; }
- 🌟🌟
match
是一个表达式,因此可以用在赋值语句中
fn main() { let boolean = true; // 使用 match 表达式填空,并满足以下条件 // // boolean = true => binary = 1 // boolean = false => binary = 0 let binary = __; assert_eq!(binary, 1); }
- 🌟🌟 使用 match 匹配出枚举成员持有的值
// 填空 enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } fn main() { let msgs = [ Message::Quit, Message::Move{x:1, y:3}, Message::ChangeColor(255,255,0) ]; for msg in msgs { show_message(msg) } } fn show_message(msg: Message) { match msg { __ => { // 这里匹配 Message::Move assert_eq!(a, 1); assert_eq!(b, 3); }, Message::ChangeColor(_, g, b) => { assert_eq!(g, __); assert_eq!(b, __); } __ => println!("no data in these variants") } }
matches!
matches!
看起来像 match
, 但是它可以做一些特别的事情
- 🌟🌟
fn main() { let alphabets = ['a', 'E', 'Z', '0', 'x', '9' , 'Y']; // 使用 `matches` 填空 for ab in alphabets { assert!(__) } }
- 🌟🌟
enum MyEnum { Foo, Bar } fn main() { let mut count = 0; let v = vec![MyEnum::Foo,MyEnum::Bar,MyEnum::Foo]; for e in v { if e == MyEnum::Foo { // 修复错误,只能修改本行代码 count += 1; } } assert_eq!(count, 2); }
if let
在有些时候, 使用 match
匹配枚举有些太重了,此时 if let
就非常适合.
- 🌟
fn main() { let o = Some(7); // 移除整个 `match` 语句块,使用 `if let` 替代 match o { Some(i) => { println!("This is a really long string and `{:?}`", i); } _ => {} }; }
- 🌟🌟
// 填空 enum Foo { Bar(u8) } fn main() { let a = Foo::Bar(1); __ { println!("foobar 持有的值是: {}", i); } }
- 🌟🌟
enum Foo { Bar, Baz, Qux(u32) } fn main() { let a = Foo::Qux(10); // 移除以下代码,使用 `match` 代替 if let Foo::Bar = a { println!("match foo::bar") } else if let Foo::Baz = a { println!("match foo::baz") } else { println!("match others") } }
变量遮蔽( Shadowing )
- 🌟🌟
// 就地修复错误 fn main() { let age = Some(30); if let Some(age) = age { // 创建一个新的变量,该变量与之前的 `age` 变量同名 assert_eq!(age, Some(30)); } // 新的 `age` 变量在这里超出作用域 match age { // `match` 也能实现变量遮蔽 Some(age) => println!("age 是一个新的变量,它的值是 {}",age), _ => () } }
你可以在这里找到答案(在 solutions 路径下)
模式
- 🌟🌟 使用
|
可以匹配多个值, 而使用..=
可以匹配一个闭区间的数值序列
fn main() {} fn match_number(n: i32) { match n { // 匹配一个单独的值 1 => println!("One!"), // 使用 `|` 填空,不要使用 `..` 或 `..=` __ => println!("match 2 -> 5"), // 匹配一个闭区间的数值序列 6..=10 => { println!("match 6 -> 10") }, _ => { println!("match 11 -> +infinite") } } }
- 🌟🌟🌟
@
操作符可以让我们将一个与模式相匹配的值绑定到新的变量上
struct Point { x: i32, y: i32, } fn main() { // 填空,让 p 匹配第二个分支 let p = Point { x: __, y: __ }; match p { Point { x, y: 0 } => println!("On the x axis at {}", x), // 第二个分支 Point { x: 0..=5, y: y@ (10 | 20 | 30) } => println!("On the y axis at {}", y), Point { x, y } => println!("On neither axis: ({}, {})", x, y), } }
- 🌟🌟🌟
// 修复错误 enum Message { Hello { id: i32 }, } fn main() { let msg = Message::Hello { id: 5 }; match msg { Message::Hello { id: 3..=7, } => println!("id 值的范围在 [3, 7] 之间: {}", id), Message::Hello { id: newid@10 | 11 | 12 } => { println!("id 值的范围在 [10, 12] 之间: {}", newid) } Message::Hello { id } => println!("Found some other id: {}", id), } }
- 🌟🌟 匹配守卫(match guard)是一个位于 match 分支模式之后的额外 if 条件,它能为分支模式提供更进一步的匹配条件。
// 填空让代码工作,必须使用 `split` fn main() { let num = Some(4); let split = 5; match num { Some(x) __ => assert!(x < split), Some(x) => assert!(x >= split), None => (), } }
- 🌟🌟🌟 使用
..
忽略一部分值
// 填空,让代码工作 fn main() { let numbers = (2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048); match numbers { __ => { assert_eq!(first, 2); assert_eq!(last, 2048); } } }
- 🌟🌟 使用模式
&mut V
去匹配一个可变引用时,你需要格外小心,因为匹配出来的V
是一个值,而不是可变引用
// 修复错误,尽量少地修改代码 // 不要移除任何代码行 fn main() { let mut v = String::from("hello,"); let r = &mut v; match r { &mut value => value.push_str(" world!") } }
你可以在这里找到答案(在 solutions 路径下)
方法和关联函数
示例
struct Point { x: f64, y: f64, } // `Point` 的关联函数都放在下面的 `impl` 语句块中 impl Point { // 关联函数的使用方法跟构造器非常类似 fn origin() -> Point { Point { x: 0.0, y: 0.0 } } // 另外一个关联函数,有两个参数 fn new(x: f64, y: f64) -> Point { Point { x: x, y: y } } } struct Rectangle { p1: Point, p2: Point, } impl Rectangle { // 这是一个方法 // `&self` 是 `self: &Self` 的语法糖 // `Self` 是当前调用对象的类型,对于本例来说 `Self` = `Rectangle` fn area(&self) -> f64 { // 使用点操作符可以访问 `self` 中的结构体字段 let Point { x: x1, y: y1 } = self.p1; let Point { x: x2, y: y2 } = self.p2; // `abs` 是一个 `f64` 类型的方法,会返回调用者的绝对值 ((x1 - x2) * (y1 - y2)).abs() } fn perimeter(&self) -> f64 { let Point { x: x1, y: y1 } = self.p1; let Point { x: x2, y: y2 } = self.p2; 2.0 * ((x1 - x2).abs() + (y1 - y2).abs()) } // 该方法要求调用者是可变的,`&mut self` 是 `self: &mut Self` 的语法糖 fn translate(&mut self, x: f64, y: f64) { self.p1.x += x; self.p2.x += x; self.p1.y += y; self.p2.y += y; } } // `Pair` 持有两个分配在堆上的整数 struct Pair(Box<i32>, Box<i32>); impl Pair { // 该方法会拿走调用者的所有权 // `self` 是 `self: Self` 的语法糖 fn destroy(self) { let Pair(first, second) = self; println!("Destroying Pair({}, {})", first, second); // `first` 和 `second` 在这里超出作用域并被释放 } } fn main() { let rectangle = Rectangle { // 关联函数的调用不是通过点操作符,而是使用 `::~ p1: Point::origin(), p2: Point::new(3.0, 4.0), }; // 方法才是通过点操作符调用 // 注意,这里的方法需要的是 `&self` 但是我们并没有使用 `(&rectangle).perimeter()` 来调用,原因在于: // 编译器会帮我们自动取引用 // `rectangle.perimeter()` === `Rectangle::perimeter(&rectangle)` println!("Rectangle perimeter: {}", rectangle.perimeter()); println!("Rectangle area: {}", rectangle.area()); let mut square = Rectangle { p1: Point::origin(), p2: Point::new(1.0, 1.0), }; // 错误!`rectangle` 是不可变的,但是这个方法要求一个可变的对象 //rectangle.translate(1.0, 0.0); // TODO ^ 试着反注释此行,看看会发生什么 // 可以!可变对象可以调用可变的方法 square.translate(1.0, 1.0); let pair = Pair(Box::new(1), Box::new(2)); pair.destroy(); // Error! 上一个 `destroy` 调用拿走了 `pair` 的所有权 //pair.destroy(); // TODO ^ 试着反注释此行 }
Exercises
Method
- 🌟🌟 方法跟函数类似:都是使用
fn
声明,有参数和返回值。但是与函数不同的是,方法定义在结构体的上下文中(枚举、特征对象也可以定义方法),而且方法的第一个参数一定是self
或其变体&self
、&mut self
,self
代表了当前调用的结构体实例。
struct Rectangle { width: u32, height: u32, } impl Rectangle { // 完成 area 方法,返回矩形 Rectangle 的面积 fn area } fn main() { let rect1 = Rectangle { width: 30, height: 50 }; assert_eq!(rect1.area(), 1500); }
- 🌟🌟
self
会拿走当前结构体实例(调用对象)的所有权,而&self
却只会借用一个不可变引用,&mut self
会借用一个可变引用
// 只填空,不要删除任何代码行! #[derive(Debug)] struct TrafficLight { color: String, } impl TrafficLight { pub fn show_state(__) { println!("the current state is {}", __.color); } } fn main() { let light = TrafficLight{ color: "red".to_owned(), }; // 不要拿走 `light` 的所有权 light.show_state(); // 否则下面代码会报错 println!("{:?}", light); }
- 🌟🌟
&self
实际上是self: &Self
的缩写或者说语法糖
struct TrafficLight { color: String, } impl TrafficLight { // 使用 `Self` 填空 pub fn show_state(__) { println!("the current state is {}", self.color); } // 填空,不要使用 `Self` 或其变体 pub fn change_state() { self.color = "green".to_string() } } fn main() {}
Associated function
- 🌟🌟 定义在
impl
语句块中的函数被称为关联函数,因为它们跟当前类型关联在一起。关联函数与方法最大的区别就是它第一个参数不是self
,原因是它们不需要使用当前的实例,因此关联函数往往可以用于构造函数:初始化一个实例对象。
#[derive(Debug)] struct TrafficLight { color: String, } impl TrafficLight { // 1. 实现下面的关联函数 `new`, // 2. 该函数返回一个 TrafficLight 实例,包含 `color` "red" // 3. 该函数必须使用 `Self` 作为类型,不能在签名或者函数体中使用 `TrafficLight` pub fn new() pub fn get_state(&self) -> &str { &self.color } } fn main() { let light = TrafficLight::new(); assert_eq!(light.get_state(), "red"); }
多个 impl
语句块
- 🌟 每一个结构体允许拥有多个
impl
语句块
struct Rectangle { width: u32, height: u32, } // 使用多个 `impl` 语句块重写下面的代码 impl Rectangle { fn area(&self) -> u32 { self.width * self.height } fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } } fn main() {}
Enums
- 🌟🌟🌟 我们还可以为枚举类型定义方法
#[derive(Debug)] enum TrafficLightColor { Red, Yellow, Green, } // 为 TrafficLightColor 实现所需的方法 impl TrafficLightColor { } fn main() { let c = TrafficLightColor::Yellow; assert_eq!(c.color(), "yellow"); println!("{:?}",c); }
Practice
@todo
你可以在这里找到答案(在 solutions 路径下)
Generics and Traits
泛型
函数
- 🌟🌟🌟
// 填空 struct A; // 具体的类型 `A`. struct S(A); // 具体的类型 `S`. struct SGen<T>(T); // 泛型 `SGen`. fn reg_fn(_s: S) {} fn gen_spec_t(_s: SGen<A>) {} fn gen_spec_i32(_s: SGen<i32>) {} fn generic<T>(_s: SGen<T>) {} fn main() { // 使用非泛型函数 reg_fn(__); // 具体的类型 gen_spec_t(__); // 隐式地指定类型参数 `A`. gen_spec_i32(__); // 隐式地指定类型参数`i32`. // 显式地指定类型参数 `char` generic::<char>(__); // 隐式地指定类型参数 `char`. generic(__); }
- 🌟🌟
// 实现下面的泛型函数 sum fn sum fn main() { assert_eq!(5, sum(2i8, 3i8)); assert_eq!(50, sum(20, 30)); assert_eq!(2.46, sum(1.23, 1.23)); }
结构体和 impl
- 🌟
// 实现一个结构体 Point 让代码工作 fn main() { let integer = Point { x: 5, y: 10 }; let float = Point { x: 1.0, y: 4.0 }; }
- 🌟🌟
// 修改以下结构体让代码工作 struct Point<T> { x: T, y: T, } fn main() { // 不要修改这行代码! let p = Point{x: 5, y : "hello".to_string()}; }
- 🌟🌟
// 为 Val 增加泛型参数,不要修改 `main` 中的代码 struct Val { val: f64, } impl Val { fn value(&self) -> &f64 { &self.val } } fn main() { let x = Val{ val: 3.0 }; let y = Val{ val: "hello".to_string()}; println!("{}, {}", x.value(), y.value()); }
方法
- 🌟🌟🌟
struct Point<T, U> { x: T, y: U, } impl<T, U> Point<T, U> { // 实现 mixup,不要修改其它代码! fn mixup } fn main() { let p1 = Point { x: 5, y: 10 }; let p2 = Point { x: "Hello", y: '中'}; let p3 = p1.mixup(p2); assert_eq!(p3.x, 5); assert_eq!(p3.y, '中'); }
- 🌟🌟
// 修复错误,让代码工作 struct Point<T> { x: T, y: T, } impl Point<f32> { fn distance_from_origin(&self) -> f32 { (self.x.powi(2) + self.y.powi(2)).sqrt() } } fn main() { let p = Point{x: 5, y: 10}; println!("{}",p.distance_from_origin()) }
你可以在这里找到答案(在 solutions 路径下)
Const 泛型
在之前的泛型中,可以抽象为一句话:针对类型实现的泛型,所有的泛型都是为了抽象不同的类型,那有没有针对值的泛型?答案就是 Const 泛型
。
示例
- 下面的例子同时使用泛型和 const 泛型来实现一个结构体,该结构体的字段中的数组长度是可变的
struct ArrayPair<T, const N: usize> { left: [T; N], right: [T; N], } impl<T: Debug, const N: usize> Debug for ArrayPair<T, N> { // ... }
- 目前,const 泛型参数只能使用以下形式的实参:
- 一个单独的 const 泛型参数
- 一个字面量 (i.e. 整数, 布尔值或字符).
- 一个具体的 const 表达式( 表达式中不能包含任何 泛型参数)
fn foo<const N: usize>() {} fn bar<T, const M: usize>() { foo::<M>(); // ok: 符合第一种 foo::<2021>(); // ok: 符合第二种 foo::<{20 * 100 + 20 * 10 + 1}>(); // ok: 符合第三种 foo::<{ M + 1 }>(); // error: 违背第三种,const 表达式中不能有泛型参数 M foo::<{ std::mem::size_of::<T>() }>(); // error: 泛型表达式包含了泛型参数 T let _: [u8; M]; // ok: 符合第一种 let _: [u8; std::mem::size_of::<T>()]; // error: 泛型表达式包含了泛型参数 T } fn main() {}
- const 泛型还能帮我们避免一些运行时检查,提升性能
pub struct MinSlice<T, const N: usize> { pub head: [T; N], pub tail: [T], } fn main() { let slice: &[u8] = b"Hello, world"; let reference: Option<&u8> = slice.get(6); // 我们知道 `.get` 返回的是 `Some(b' ')` // 但编译器不知道 assert!(reference.is_some()); let slice: &[u8] = b"Hello, world"; // 当编译构建 MinSlice 时会进行长度检查,也就是在编译期我们就知道它的长度是 12 // 在运行期,一旦 `unwrap` 成功,在 `MinSlice` 的作用域内,就再无需任何检查 let minslice = MinSlice::<u8, 12>::from_slice(slice).unwrap(); let value: u8 = minslice.head[6]; assert_eq!(value, b' ') }
练习
- 🌟🌟
<T, const N: usize>
是结构体类型的一部分,和数组类型一样,这意味着长度不同会导致类型不同:Array<i32, 3>
和Array<i32, 4>
是不同的类型
// 修复错误 struct Array<T, const N: usize> { data : [T; N] } fn main() { let arrays = [ Array{ data: [1, 2, 3], }, Array { data: [1.0, 2.0, 3.0], }, Array { data: [1, 2] } ]; }
- 🌟🌟
// 填空 fn print_array<__>(__) { println!("{:?}", arr); } fn main() { let arr = [1, 2, 3]; print_array(arr); let arr = ["hello", "world"]; print_array(arr); }
- 🌟🌟🌟 有时我们希望能限制一个变量占用内存的大小,例如在嵌入式环境中,此时 const 泛型参数的第三种形式
const 表达式
就非常适合.
#![allow(incomplete_features)] #![feature(generic_const_exprs)] fn check_size<T>(val: T) where Assert<{ core::mem::size_of::<T>() < 768 }>: IsTrue, { //... } // 修复 main 函数中的错误 fn main() { check_size([0u8; 767]); check_size([0i32; 191]); check_size(["hello你好"; __]); // size of &str ? check_size(["hello你好".to_string(); __]); // size of String? check_size(['中'; __]); // size of char ? } pub enum Assert<const CHECK: bool> {} pub trait IsTrue {} impl IsTrue for Assert<true> {}
Traits
特征 Trait 可以告诉编译器一个特定的类型所具有的、且能跟其它类型共享的特性。我们可以使用特征通过抽象的方式来定义这种共享行为,还可以使用特征约束来限定一个泛型类型必须要具有某个特定的行为。
Note: 特征跟其它语言的接口较为类似,但是仍然有一些区别
示例
struct Sheep { naked: bool, name: String } impl Sheep { fn is_naked(&self) -> bool { self.naked } fn shear(&mut self) { if self.is_naked() { // `Sheep` 结构体上定义的方法可以调用 `Sheep` 所实现的特征的方法 println!("{} is already naked...", self.name()); } else { println!("{} gets a haircut!", self.name); self.naked = true; } } } trait Animal { // 关联函数签名;`Self` 指代实现者的类型 // 例如我们在为 Pig 类型实现特征时,那 `new` 函数就会返回一个 `Pig` 类型的实例,这里的 `Self` 指代的就是 `Pig` 类型 fn new(name: String) -> Self; // 方法签名 fn name(&self) -> String; fn noise(&self) -> String; // 方法还能提供默认的定义实现 fn talk(&self) { println!("{} says {}", self.name(), self.noise()); } } impl Animal for Sheep { // `Self` 被替换成具体的实现者类型: `Sheep` fn new(name: String) -> Sheep { Sheep { name: name, naked: false } } fn name(&self) -> String { self.name.clone() } fn noise(&self) -> String { if self.is_naked() { "baaaaah?".to_string() } else { "baaaaah!".to_string() } } // 默认的特征方法可以被重写 fn talk(&self) { println!("{} pauses briefly... {}", self.name, self.noise()); } } fn main() { // 这里的类型注释时必须的 let mut dolly: Sheep = Animal::new("Dolly".to_string()); // TODO ^ 尝试去除类型注释,看看会发生什么 dolly.talk(); dolly.shear(); dolly.talk(); }
Exercises
- 🌟🌟
// 完成两个 `impl` 语句块 // 不要修改 `main` 中的代码 trait Hello { fn say_hi(&self) -> String { String::from("hi") } fn say_something(&self) -> String; } struct Student {} impl Hello for Student { } struct Teacher {} impl Hello for Teacher { } fn main() { let s = Student {}; assert_eq!(s.say_hi(), "hi"); assert_eq!(s.say_something(), "I'm a good student"); let t = Teacher {}; assert_eq!(t.say_hi(), "Hi, I'm your new teacher"); assert_eq!(t.say_something(), "I'm not a bad teacher"); println!("Success!") }
Derive 派生
我们可以使用 #[derive]
属性来派生一些特征,对于这些特征编译器会自动进行默认实现,对于日常代码开发而言,这是非常方便的,例如大家经常用到的 Debug
特征,就是直接通过派生来获取默认实现,而无需我们手动去完成这个工作。
想要查看更多信息,可以访问这里。
- 🌟🌟
// `Centimeters`, 一个元组结构体,可以被比较大小 #[derive(PartialEq, PartialOrd)] struct Centimeters(f64); // `Inches`, 一个元组结构体可以被打印 #[derive(Debug)] struct Inches(i32); impl Inches { fn to_centimeters(&self) -> Centimeters { let &Inches(inches) = self; Centimeters(inches as f64 * 2.54) } } // 添加一些属性让代码工作 // 不要修改其它代码! struct Seconds(i32); fn main() { let _one_second = Seconds(1); println!("One second looks like: {:?}", _one_second); let _this_is_true = (_one_second == _one_second); let _this_is_true = (_one_second > _one_second); let foot = Inches(12); println!("One foot equals {:?}", foot); let meter = Centimeters(100.0); let cmp = if foot.to_centimeters() < meter { "smaller" } else { "bigger" }; println!("One foot is {} than one meter.", cmp); }
运算符
在 Rust 中,许多运算符都可以被重载,事实上,运算符仅仅是特征方法调用的语法糖。例如 a + b
中的 +
是 std::ops::Add
特征的 add
方法调用,因此我们可以为自定义类型实现该特征来支持该类型的加法运算。
- 🌟🌟
use std::ops; // 实现 fn multiply 方法 // 如上所述,`+` 需要 `T` 类型实现 `std::ops::Add` 特征 // 那么, `*` 运算符需要实现什么特征呢? 你可以在这里找到答案: https://doc.rust-lang.org/core/ops/ fn multipl fn main() { assert_eq!(6, multiply(2u8, 3u8)); assert_eq!(5.0, multiply(1.0, 5.0)); println!("Success!") }
- 🌟🌟🌟
// 修复错误,不要修改 `main` 中的代码! use std::ops; struct Foo; struct Bar; struct FooBar; struct BarFoo; // 下面的代码实现了自定义类型的相加: Foo + Bar = FooBar impl ops::Add<Bar> for Foo { type Output = FooBar; fn add(self, _rhs: Bar) -> FooBar { FooBar } } impl ops::Sub<Foo> for Bar { type Output = BarFoo; fn sub(self, _rhs: Foo) -> BarFoo { BarFoo } } fn main() { // 不要修改下面代码 // 你需要为 FooBar 派生一些特征来让代码工作 assert_eq!(Foo + Bar, FooBar); assert_eq!(Foo - Bar, BarFoo); println!("Success!") }
使用特征作为函数参数
除了使用具体类型来作为函数参数,我们还能通过 impl Trait
的方式来指定实现了该特征的参数:该参数能接受的类型必须要实现指定的特征。
- 🌟🌟🌟
// 实现 `fn summary` // 修复错误且不要移除任何代码行 trait Summary { fn summarize(&self) -> String; } #[derive(Debug)] struct Post { title: String, author: String, content: String, } impl Summary for Post { fn summarize(&self) -> String { format!("The author of post {} is {}", self.title, self.author) } } #[derive(Debug)] struct Weibo { username: String, content: String, } impl Summary for Weibo { fn summarize(&self) -> String { format!("{} published a weibo {}", self.username, self.content) } } fn main() { let post = Post { title: "Popular Rust".to_string(), author: "Sunface".to_string(), content: "Rust is awesome!".to_string(), }; let weibo = Weibo { username: "sunface".to_string(), content: "Weibo seems to be worse than Tweet".to_string(), }; summary(post); summary(weibo); println!("{:?}", post); println!("{:?}", weibo); } // 在下面实现 `fn summary` 函数
使用特征作为函数返回值
我们还可以在函数的返回值中使用 impl Trait
语法。然后只有在返回值是同一个类型时,才能这么使用,如果返回值是不同的类型,你可能更需要特征对象。
- 🌟🌟
struct Sheep {} struct Cow {} trait Animal { fn noise(&self) -> String; } impl Animal for Sheep { fn noise(&self) -> String { "baaaaah!".to_string() } } impl Animal for Cow { fn noise(&self) -> String { "moooooo!".to_string() } } // 返回一个类型,该类型实现了 Animal 特征,但是我们并不能在编译期获知具体返回了哪个类型 // 修复这里的错误,你可以使用虚假的随机,也可以使用特征对象 fn random_animal(random_number: f64) -> impl Animal { if random_number < 0.5 { Sheep {} } else { Cow {} } } fn main() { let random_number = 0.234; let animal = random_animal(random_number); println!("You've randomly chosen an animal, and it says {}", animal.noise()); }
特征约束
impl Trait
语法非常直观简洁,但它实际上是特征约束的语法糖。
当使用泛型参数时,我们往往需要为该参数指定特定的行为,这种指定方式就是通过特征约束来实现的。
- 🌟🌟
fn main() { assert_eq!(sum(1, 2), 3); } // 通过两种方法使用特征约束来实现 `fn sum` fn sum<T>(x: T, y: T) -> T { x + y }
- 🌟🌟
// 修复代码中的错误 struct Pair<T> { x: T, y: T, } impl<T> Pair<T> { fn new(x: T, y: T) -> Self { Self { x, y, } } } impl<T: std::fmt::Debug + 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); } } } struct Unit(i32); fn main() { let pair = Pair{ x: Unit(1), y: Unit(3) }; pair.cmp_display(); }
- 🌟🌟🌟
// 填空 fn example1() { // `T: Trait` 是最常使用的方式 // `T: Fn(u32) -> u32` 说明 `T` 只能接收闭包类型的参数 struct Cacher<T: Fn(u32) -> u32> { calculation: T, value: Option<u32>, } impl<T: Fn(u32) -> u32> Cacher<T> { fn new(calculation: T) -> Cacher<T> { Cacher { calculation, value: None, } } fn value(&mut self, arg: u32) -> u32 { match self.value { Some(v) => v, None => { let v = (self.calculation)(arg); self.value = Some(v); v }, } } } let mut cacher = Cacher::new(|x| x+1); assert_eq!(cacher.value(10), __); assert_eq!(cacher.value(15), __); } fn example2() { // 还可以使用 `where` 来约束 T struct Cacher<T> where T: Fn(u32) -> u32, { calculation: T, value: Option<u32>, } impl<T> Cacher<T> where T: Fn(u32) -> u32, { fn new(calculation: T) -> Cacher<T> { Cacher { calculation, value: None, } } fn value(&mut self, arg: u32) -> u32 { match self.value { Some(v) => v, None => { let v = (self.calculation)(arg); self.value = Some(v); v }, } } } let mut cacher = Cacher::new(|x| x+1); assert_eq!(cacher.value(20), __); assert_eq!(cacher.value(25), __); } fn main() { example1(); example2(); println!("Success!") }
You can find the solutions here(under the solutions path), but only use it when you need it :)
特征对象
在特征练习中 我们已经知道当函数返回多个类型时,impl Trait
是无法使用的。
对于数组而言,其中一个限制就是无法存储不同类型的元素,但是通过之前的学习,大家应该知道枚举可以在部分场景解决这种问题,但是这种方法局限性较大。此时就需要我们的主角登场了。
使用 dyn
返回特征
Rust 编译器需要知道一个函数的返回类型占用多少内存空间。由于特征的不同实现类型可能会占用不同的内存,因此通过 impl Trait
返回多个类型是不被允许的,但是我们可以返回一个 dyn
特征对象来解决问题。
- 🌟🌟🌟
trait Bird { fn quack(&self) -> String; } struct Duck; impl Duck { fn swim(&self) { println!("Look, the duck is swimming") } } struct Swan; impl Swan { fn fly(&self) { println!("Look, the duck.. oh sorry, the swan is flying") } } impl Bird for Duck { fn quack(&self) -> String{ "duck duck".to_string() } } impl Bird for Swan { fn quack(&self) -> String{ "swan swan".to_string() } } fn main() { // 填空 let duck = __; duck.swim(); let bird = hatch_a_bird(2); // 变成鸟儿后,它忘记了如何游,因此以下代码会报错 // bird.swim(); // 但它依然可以叫唤 assert_eq!(bird.quack(), "duck duck"); let bird = hatch_a_bird(1); // 这只鸟儿忘了如何飞翔,因此以下代码会报错 // bird.fly(); // 但它也可以叫唤 assert_eq!(bird.quack(), "swan swan"); println!("Success!") } // 实现以下函数 fn hatch_a_bird...
在数组中使用特征对象
- 🌟🌟
trait Bird { fn quack(&self); } struct Duck; impl Duck { fn fly(&self) { println!("Look, the duck is flying") } } struct Swan; impl Swan { fn fly(&self) { println!("Look, the duck.. oh sorry, the swan is flying") } } impl Bird for Duck { fn quack(&self) { println!("{}", "duck duck"); } } impl Bird for Swan { fn quack(&self) { println!("{}", "swan swan"); } } fn main() { // 填空 let birds __; for bird in birds { bird.quack(); // 当 duck 和 swan 变成 bird 后,它们都忘了如何翱翔于天际,只记得该怎么叫唤了。。 // 因此,以下代码会报错 // bird.fly(); } }
&dyn
and Box<dyn>
- 🌟🌟
// 填空 trait Draw { fn draw(&self) -> String; } impl Draw for u8 { fn draw(&self) -> String { format!("u8: {}", *self) } } impl Draw for f64 { fn draw(&self) -> String { format!("f64: {}", *self) } } fn main() { let x = 1.1f64; let y = 8u8; // draw x draw_with_box(__); // draw y draw_with_ref(&y); println!("Success!") } fn draw_with_box(x: Box<dyn Draw>) { x.draw(); } fn draw_with_ref(x: __) { x.draw(); }
静态分发和动态分发Static and Dynamic dispatch
关于这块内容的解析介绍,请参见 Rust语言圣经。
- 🌟🌟
trait Foo { fn method(&self) -> String; } impl Foo for u8 { fn method(&self) -> String { format!("u8: {}", *self) } } impl Foo for String { fn method(&self) -> String { format!("string: {}", *self) } } // 通过泛型实现以下函数 fn static_dispatch... // 通过特征对象实现以下函数 fn dynamic_dispatch... fn main() { let x = 5u8; let y = "Hello".to_string(); static_dispatch(x); dynamic_dispatch(&y); println!("Success!") }
对象安全
一个特征能变成特征对象,首先该特征必须是对象安全的,即该特征的所有方法都必须拥有以下特点:
- 返回类型不能是
Self
. - 不能使用泛型参数
- 🌟🌟🌟🌟
// 使用至少两种方法让代码工作 // 不要添加/删除任何代码行 trait MyTrait { fn f(&self) -> Self; } impl MyTrait for u32 { fn f(&self) -> Self { 42 } } impl MyTrait for String { fn f(&self) -> Self { self.clone() } } fn my_function(x: Box<dyn MyTrait>) { x.f() } fn main() { my_function(Box::new(13_u32)); my_function(Box::new(String::from("abc"))); println!("Success!") }
You can find the solutions here(under the solutions path), but only use it when you need it :)
进一步深入特征
关联类型
关联类型主要用于提升代码的可读性,例如以下代码 :
#![allow(unused)] fn main() { pub trait CacheableItem: Clone + Default + fmt::Debug + Decodable + Encodable { type Address: AsRef<[u8]> + Clone + fmt::Debug + Eq + Hash; fn is_null(&self) -> bool; } }
相比 AsRef<[u8]> + Clone + fmt::Debug + Eq + Hash
, Address
的使用可以极大的极少其它类型在实现该特征时所需的模版代码.
- 🌟🌟🌟
struct Container(i32, i32); // 使用关联类型实现重新实现以下特征 // trait Contains { // type A; // type B; trait Contains<A, B> { fn contains(&self, _: &A, _: &B) -> bool; fn first(&self) -> i32; fn last(&self) -> i32; } impl Contains<i32, i32> for Container { fn contains(&self, number_1: &i32, number_2: &i32) -> bool { (&self.0 == number_1) && (&self.1 == number_2) } // Grab the first number. fn first(&self) -> i32 { self.0 } // Grab the last number. fn last(&self) -> i32 { self.1 } } fn difference<A, B, C: Contains<A, B>>(container: &C) -> i32 { container.last() - container.first() } fn main() { let number_1 = 3; let number_2 = 10; let container = Container(number_1, number_2); println!("Does container contain {} and {}: {}", &number_1, &number_2, container.contains(&number_1, &number_2)); println!("First number: {}", container.first()); println!("Last number: {}", container.last()); println!("The difference is: {}", difference(&container)); }
定义默认的泛型类型参数
当我们使用泛型类型参数时,可以为该泛型参数指定一个具体的默认类型,这样当实现该特征时,如果该默认类型可以使用,那用户再无需手动指定具体的类型。
- 🌟🌟
use std::ops::Sub; #[derive(Debug, PartialEq)] struct Point<T> { x: T, y: T, } // 用三种方法填空: 其中两种使用默认的泛型参数,另外一种不使用 impl __ { type Output = Self; fn sub(self, other: Self) -> Self::Output { Point { x: self.x - other.x, y: self.y - other.y, } } } fn main() { assert_eq!(Point { x: 2, y: 3 } - Point { x: 1, y: 0 }, Point { x: 1, y: 3 }); println!("Success!") }
完全限定语法
在 Rust 中,两个不同特征的方法完全可以同名,且你可以为同一个类型同时实现这两个特征。这种情况下,就出现了一个问题:该如何调用这两个特征上定义的同名方法。为了解决这个问题,我们需要使用完全限定语法( Fully Qualified Syntax )。
示例
trait UsernameWidget { fn get(&self) -> String; } trait AgeWidget { fn get(&self) -> u8; } struct Form { username: String, age: u8, } impl UsernameWidget for Form { fn get(&self) -> String { self.username.clone() } } impl AgeWidget for Form { fn get(&self) -> u8 { self.age } } fn main() { let form = Form{ username: "rustacean".to_owned(), age: 28, }; // 如果你反注释下面一行代码,将看到一个错误: Fully Qualified Syntax // 毕竟,这里有好几个同名的 `get` 方法 // // println!("{}", form.get()); let username = UsernameWidget::get(&form); assert_eq!("rustacean".to_owned(), username); let age = AgeWidget::get(&form); // 你还可以使用以下语法 `<Form as AgeWidget>::get` assert_eq!(28, age); println!("Success!") }
练习题
- 🌟🌟
trait Pilot { fn fly(&self) -> String; } trait Wizard { fn fly(&self) -> String; } struct Human; impl Pilot for Human { fn fly(&self) -> String { String::from("This is your captain speaking.") } } impl Wizard for Human { fn fly(&self) -> String { String::from("Up!") } } impl Human { fn fly(&self) -> String { String::from("*waving arms furiously*") } } fn main() { let person = Human; assert_eq!(__, "This is your captain speaking."); assert_eq!(__, "Up!"); assert_eq!(__, "*waving arms furiously*"); println!("Success!") }
Supertraits
有些时候我们希望在特征上实现类似继承的特性,例如让一个特征 A
使用另一个特征 B
的功能。这种情况下,一个类型要实现特征 A
首先要实现特征 B
, 特征 B
就被称为 supertrait
- 🌟🌟🌟
trait Person { fn name(&self) -> String; } // Person 是 Student 的 supertrait . // 实现 Student 需要同时实现 Person. trait Student: Person { fn university(&self) -> String; } trait Programmer { fn fav_language(&self) -> String; } // CompSciStudent (computer science student) 是 Programmer // 和 Student 的 subtrait. 实现 CompSciStudent 需要先实现这两个 supertraits. trait CompSciStudent: Programmer + Student { fn git_username(&self) -> String; } fn comp_sci_student_greeting(student: &dyn CompSciStudent) -> String { format!( "My name is {} and I attend {}. My favorite language is {}. My Git username is {}", student.name(), student.university(), student.fav_language(), student.git_username() ) } struct CSStudent { name: String, university: String, fav_language: String, git_username: String } // 为 CSStudent 实现所需的特征 impl ... fn main() { let student = CSStudent { name: "Sunfei".to_string(), university: "XXX".to_string(), fav_language: "Rust".to_string(), git_username: "sunface".to_string() }; // 填空 println!("{}", comp_sci_student_greeting(__)); }
孤儿原则
关于孤儿原则的详细介绍请参见特征定义与实现的位置孤儿规则 和 在外部类型上实现外部特征。
- 🌟🌟
use std::fmt; // 定义一个 newtype `Pretty` impl fmt::Display for Pretty { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "\"{}\"", self.0.clone() + ", world") } } fn main() { let w = Pretty("hello".to_string()); println!("w = {}", w); }
You can find the solutions here(under the solutions path), but only use it when you need it :)
集合类型
学习资源:
- 简体中文: Rust语言圣经 - 集合类型
String
std::string::String
是 UTF-8 编码、可增长的动态字符串. 它也是我们日常开发中最常用的字符串类型,同时对于它所拥有的内容拥有所有权。
基本操作
- 🌟🌟
// 填空并修复错误 // 1. 不要使用 `to_string()` // 2. 不要添加/删除任何代码行 fn main() { let mut s: String = "hello, "; s.push_str("world".to_string()); s.push(__); move_ownership(s); assert_eq!(s, "hello, world!"); println!("Success!") } fn move_ownership(s: String) { println!("ownership of \"{}\" is moved here!", s) }
String and &str
虽然 String
的底层是 Vec<u8>
也就是字节数组的形式存储的,但是它是基于 UTF-8 编码的字符序列。String
分配在堆上、可增长且不是以 null
结尾。
而 &str
是切片引用类型( &[u8]
),指向一个合法的 UTF-8 字符序列,总之,&str
和 String
的关系类似于 &[T]
和 Vec<T>
。
如果大家想了解更多,可以看看易混淆概念解析 - &str 和 String。
- 🌟🌟
// 填空 fn main() { let mut s = String::from("hello, world"); let slice1: &str = __; // 使用两种方法 assert_eq!(slice1, "hello, world"); let slice2 = __; assert_eq!(slice2, "hello"); let slice3: __ = __; slice3.push('!'); assert_eq!(slice3, "hello, world!"); println!("Success!") }
- 🌟🌟
// 问题: 我们的代码中发生了多少次堆内存分配? // 你的回答: fn main() { // 基于 `&str` 类型创建一个 String, // 字符串字面量的类型是 `&str` let s: String = String::from("hello, world!"); // 创建一个切片引用指向 String `s` let slice: &str = &s; // 基于刚创建的切片来创建一个 String let s: String = slice.to_string(); assert_eq!(s, "hello, world!"); println!("Success!") }
UTF-8 & 索引
由于 String 都是 UTF-8 编码的,这会带来几个影响:
- 如果你需要的是非 UTF-8 字符串,可以考虑 OsString
- 无法通过索引的方式访问一个 String
具体请看字符串索引。
- 🌟🌟🌟 我们无法通过索引的方式访问字符串中的某个字符,但是可以通过切片的方式来获取字符串的某一部分
&s1[start..end]
// 填空并修复错误 fn main() { let s = String::from("hello, 世界"); let slice1 = s[0]; //提示: `h` 在 UTF-8 编码中只占用 1 个字节 assert_eq!(slice1, "h"); let slice2 = &s[3..5];// 提示: `中` 在 UTF-8 编码中占用 3 个字节 assert_eq!(slice2, "世"); // 迭代 s 中的所有字符 for (i, c) in s.__ { if i == 7 { assert_eq!(c, '世') } } println!("Success!") }
utf8_slice
我们可以使用 utf8_slice 来按照字符的自然索引方式对 UTF-8 字符串进行切片访问,与之前的切片方式相比,它索引的是字符,而之前的方式索引的是字节.
示例
use utf8_slice; fn main() { let s = "The 🚀 goes to the 🌑!"; let rocket = utf8_slice::slice(s, 4, 5); // Will equal "🚀" }
- 🌟🌟🌟
提示: 也许你需要使用
from_utf8
方法
// 填空 fn main() { let mut s = String::new(); __; let v = vec![104, 101, 108, 108, 111]; // 将字节数组转换成 String let s1 = __; assert_eq!(s, s1); println!("Success!") }
内部表示
事实上 String
是一个智能指针,它作为一个结构体存储在栈上,然后指向存储在堆上的字符串底层数据。
存储在栈上的智能指针结构体由三部分组成:一个指针只指向堆上的字节数组,已使用的长度以及已分配的容量 capacity (已使用的长度小于等于已分配的容量,当容量不够时,会重新分配内存空间)。
- 🌟🌟 如果 String 的当前容量足够,那么添加字符将不会导致新的内存分配
// 修改下面的代码以打印如下内容: // 25 // 25 // 25 // 循环中不会发生任何内存分配 fn main() { let mut s = String::new(); println!("{}", s.capacity()); for _ in 0..2 { s.push_str("hello"); println!("{}", s.capacity()); } println!("Success!") }
- 🌟🌟🌟
// 填空 use std::mem; fn main() { let story = String::from("Rust By Practice"); // 阻止 String 的数据被自动 drop let mut story = mem::ManuallyDrop::new(story); let ptr = story.__(); let len = story.__(); let capacity = story.__(); assert_eq!(16, len); // 我们可以基于 ptr 指针、长度和容量来重新构建 String. // 这种操作必须标记为 unsafe,因为我们需要自己来确保这里的操作是安全的 let s = unsafe { String::from_raw_parts(ptr, len, capacity) }; assert_eq!(*story, s); println!("Success!") }
常用方法
关于 String 的常用方法练习,可以查看这里.
You can find the solutions here(under the solutions path), but only use it when you need it
Vector
相比 [T; N]
形式的数组, Vector
最大的特点就是可以动态调整长度。
基本操作
- 🌟🌟🌟
fn main() { let arr: [u8; 3] = [1, 2, 3]; let v = Vec::from(arr); is_vec(v); let v = vec![1, 2, 3]; is_vec(v); // vec!(..) 和 vec![..] 是同样的宏,宏可以使用 []、()、{}三种形式,因此... let v = vec!(1, 2, 3); is_vec(v); // ...在下面的代码中, v 是 Vec<[u8; 3]> , 而不是 Vec<u8> // 使用 Vec::new 和 `for` 来重写下面这段代码 let v1 = vec!(arr); is_vec(v1); assert_eq!(v, v1); println!("Success!") } fn is_vec(v: Vec<u8>) {}
- 🌟🌟
Vec
可以使用extend
方法进行扩展
// 填空 fn main() { let mut v1 = Vec::from([1, 2, 4]); v1.pop(); v1.push(3); let mut v2 = Vec::new(); v2.__; assert_eq!(v1, v2); println!("Success!") }
将 X 类型转换(From/Into 特征)成 Vec
只要为 Vec
实现了 From<T>
特征,那么 T
就可以被转换成 Vec
。
- 🌟🌟🌟
// 填空 fn main() { // array -> Vec // impl From<[T; N]> for Vec let arr = [1, 2, 3]; let v1 = __(arr); let v2: Vec<i32> = arr.__(); assert_eq!(v1, v2); // String -> Vec // impl From<String> for Vec let s = "hello".to_string(); let v1: Vec<u8> = s.__(); let s = "hello".to_string(); let v2 = s.into_bytes(); assert_eq!(v1, v2); // impl<'_> From<&'_ str> for Vec let s = "hello"; let v3 = Vec::__(s); assert_eq!(v2, v3); // 迭代器 Iterators 可以通过 collect 变成 Vec let v4: Vec<i32> = [0; 10].into_iter().collect(); assert_eq!(v4, vec![0; 10]); println!("Success!") }
索引
- 🌟🌟🌟
// 修复错误并实现缺失的代码 fn main() { let mut v = Vec::from([1, 2, 3]); for i in 0..5 { println!("{:?}", v[i]) } for i in 0..5 { // 实现这里的代码... } assert_eq!(v, vec![2, 3, 4, 5, 6]); println!("Success!") }
切片
与 String
的切片类似, Vec
也可以使用切片。如果说 Vec
是可变的,那它的切片就是不可变或者说只读的,我们可以通过 &
来获取切片。
在 Rust 中,将切片作为参数进行传递是更常见的使用方式,例如当一个函数只需要可读性时,那传递 Vec
或 String
的切片 &[T]
/ &str
会更加适合。
- 🌟🌟
// 修复错误 fn main() { let mut v = vec![1, 2, 3]; let slice1 = &v[..]; // 越界访问将导致 panic. // 修改时必须使用 `v.len` let slice2 = &v[0..4]; assert_eq!(slice1, slice2); // 切片是只读的 // 注意:切片和 `&Vec` 是不同的类型,后者仅仅是 `Vec` 的引用,并可以通过解引用直接获取 `Vec` let vec_ref: &mut Vec<i32> = &mut v; (*vec_ref).push(4); let slice3 = &mut v[0..3]; slice3.push(4); assert_eq!(slice3, &[1, 2, 3, 4]); println!("Success!") }
容量
容量 capacity
是已经分配好的内存空间,用于存储未来添加到 Vec
中的元素。而长度 len
则是当前 Vec
中已经存储的元素数量。如果要添加新元素时,长度将要超过已有的容量,那容量会自动进行增长:Rust 会重新分配一块更大的内存空间,然后将之前的 Vec
拷贝过去,因此,这里就会发生新的内存分配( 目前 Rust 的容量调整策略是加倍,例如 2 -> 4 -> 8 ..)。
若这段代码会频繁发生,那频繁的内存分配会大幅影响我们系统的性能,最好的办法就是提前分配好足够的容量,尽量减少内存分配。
- 🌟🌟
// 修复错误 fn main() { let mut vec = Vec::with_capacity(10); assert_eq!(vec.len(), __); assert_eq!(vec.capacity(), 10); // 由于提前设置了足够的容量,这里的循环不会造成任何内存分配... for i in 0..10 { vec.push(i); } assert_eq!(vec.len(), __); assert_eq!(vec.capacity(), __); // ...但是下面的代码会造成新的内存分配 vec.push(11); assert_eq!(vec.len(), 11); assert!(vec.capacity() >= 11); // 填写一个合适的值,在 `for` 循环运行的过程中,不会造成任何内存分配 let mut vec = Vec::with_capacity(__); for i in 0..100 { vec.push(i); } assert_eq!(vec.len(), __); assert_eq!(vec.capacity(), __); println!("Success!") }
在 Vec 中存储不同类型的元素
Vec
中的元素必须是相同的类型,例如以下代码会发生错误:
fn main() { let v = vec![1, 2.0, 3]; }
但是我们可以使用枚举或特征对象来存储不同的类型.
- 🌟🌟
#[derive(Debug)] enum IpAddr { V4(String), V6(String), } fn main() { // 填空 let v : Vec<IpAddr>= __; // 枚举的比较需要派生 PartialEq 特征 assert_eq!(v[0], IpAddr::V4("127.0.0.1".to_string())); assert_eq!(v[1], IpAddr::V6("::1".to_string())); println!("Success!") }
- 🌟🌟
trait IpAddr { fn display(&self); } struct V4(String); impl IpAddr for V4 { fn display(&self) { println!("ipv4: {:?}",self.0) } } struct V6(String); impl IpAddr for V6 { fn display(&self) { println!("ipv6: {:?}",self.0) } } fn main() { // 填空 let v: __= vec![ Box::new(V4("127.0.0.1".to_string())), Box::new(V6("::1".to_string())), ]; for ip in v { ip.display(); } }
You can find the solutions here(under the solutions path), but only use it when you need it
HashMap
HashMap
默认使用 SipHash 1-3
哈希算法,该算法对于抵抗 HashDos
攻击非常有效。在性能方面,如果你的 key 是中型大小的,那该算法非常不错,但是如果是小型的 key( 例如整数 )亦或是大型的 key ( 例如字符串 ),那你需要采用社区提供的其它算法来提高性能。
哈希表的算法是基于 Google 的 SwissTable,你可以在这里找到 C++ 的实现,同时在 CppCon talk 上也有关于算法如何工作的演讲。
基本操作
- 🌟🌟
// 填空并修复错误 use std::collections::HashMap; fn main() { let mut scores = HashMap::new(); scores.insert("Sunface", 98); scores.insert("Daniel", 95); scores.insert("Ashley", 69.0); scores.insert("Katie", "58"); // get 返回一个 Option<&V> 枚举值 let score = scores.get("Sunface"); assert_eq!(score, Some(98)); if scores.contains_key("Daniel") { // 索引返回一个值 V let score = scores["Daniel"]; assert_eq!(score, __); scores.remove("Daniel"); } assert_eq!(scores.len(), __); for (name, score) in scores { println!("The score of {} is {}", name, score) } }
- 🌟🌟
use std::collections::HashMap; fn main() { let teams = [ ("Chinese Team", 100), ("American Team", 10), ("France Team", 50), ]; let mut teams_map1 = HashMap::new(); for team in &teams { teams_map1.insert(team.0, team.1); } // 使用两种方法实现 team_map2 // 提示:其中一种方法是使用 `collect` 方法 let teams_map2... assert_eq!(teams_map1, teams_map2); println!("Success!") }
- 🌟🌟
// 填空 use std::collections::HashMap; fn main() { // 编译器可以根据后续的使用情况帮我自动推断出 HashMap 的类型,当然你也可以显式地标注类型:HashMap<&str, u8> let mut player_stats = HashMap::new(); // 查询指定的 key, 若不存在时,则插入新的 kv 值 player_stats.entry("health").or_insert(100); assert_eq!(player_stats["health"], __); // 通过函数来返回新的值 player_stats.entry("health").or_insert_with(random_stat_buff); assert_eq!(player_stats["health"], __); let health = player_stats.entry("health").or_insert(50); assert_eq!(health, __); *health -= 50; assert_eq!(*health, __); println!("Success!") } fn random_stat_buff() -> u8 { // 为了简单,我们没有使用随机,而是返回一个固定的值 42 }
HashMap key 的限制
任何实现了 Eq
和 Hash
特征的类型都可以用于 HashMap
的 key,包括:
bool
(虽然很少用到,因为它只能表达两种 key)int
,uint
以及它们的变体,例如u8
、i32
等String
和&str
(提示:HashMap
的 key 是String
类型时,你其实可以使用&str
配合get
方法进行查询
需要注意的是,f32
和 f64
并没有实现 Hash
,原因是 浮点数精度 的问题会导致它们无法进行相等比较。
如果一个集合类型的所有字段都实现了 Eq
和 Hash
,那该集合类型会自动实现 Eq
和 Hash
。例如 Vect<T>
要实现 Hash
,那么首先需要 T
实现 Hash
。
- 🌟🌟
// 修复错误 // 提示: `derive` 是实现一些常用特征的好办法 use std::collections::HashMap; struct Viking { name: String, country: String, } impl Viking { fn new(name: &str, country: &str) -> Viking { Viking { name: name.to_string(), country: country.to_string(), } } } fn main() { // 使用 HashMap 来存储 viking 的生命值 let vikings = HashMap::from([ (Viking::new("Einar", "Norway"), 25), (Viking::new("Olaf", "Denmark"), 24), (Viking::new("Harald", "Iceland"), 12), ]); // 使用 derive 的方式来打印 vikong 的当前状态 for (viking, health) in &vikings { println!("{:?} has {} hp", viking, health); } }
容量
关于容量,我们在之前的 Vector 中有详细的介绍,而 HashMap
也可以调整容量: 你可以通过 HashMap::with_capacity(uint)
使用指定的容量来初始化,或者使用 HashMap::new()
,后者会提供一个默认的初始化容量。
示例
use std::collections::HashMap; fn main() { let mut map: HashMap<i32, i32> = HashMap::with_capacity(100); map.insert(1, 2); map.insert(3, 4); // 事实上,虽然我们使用了 100 容量来初始化,但是 map 的容量很可能会比 100 更多 assert!(map.capacity() >= 100); // 对容量进行收缩,你提供的值仅仅是一个允许的最小值,实际上,Rust 会根据当前存储的数据量进行自动设置,当然,这个值会尽量靠近你提供的值,同时还可能会预留一些调整空间 map.shrink_to(50); assert!(map.capacity() >= 50); // 让 Rust 自行调整到一个合适的值,剩余策略同上 map.shrink_to_fit(); assert!(map.capacity() >= 2); println!("Success!") }
所有权
对于实现了 Copy
特征的类型,例如 i32
,那类型的值会被拷贝到 HashMap
中。而对于有所有权的类型,例如 String
,它们的值的所有权将被转移到 HashMap
中。
- 🌟🌟
// 修复错误,尽可能少的去修改代码 // 不要移除任何代码行! use std::collections::HashMap; fn main() { let v1 = 10; let mut m1 = HashMap::new(); m1.insert(v1, v1); println!("v1 is still usable after inserting to hashmap : {}", v1); let v2 = "hello".to_string(); let mut m2 = HashMap::new(); // 所有权在这里发生了转移 m2.insert(v2, v1); assert_eq!(v2, "hello"); println!("Success!") }
三方库 Hash 库
在开头,我们提到过如果现有的 SipHash 1-3
的性能无法满足你的需求,那么可以使用社区提供的替代算法。
例如其中一个社区库的使用方式如下:
#![allow(unused)] fn main() { use std::hash::BuildHasherDefault; use std::collections::HashMap; // 引入第三方的哈希函数 use twox_hash::XxHash64; let mut hash: HashMap<_, _, BuildHasherDefault<XxHash64>> = Default::default(); hash.insert(42, "the answer"); assert_eq!(hash.get(&42), Some(&"the answer")); }
You can find the solutions here(under the solutions path), but only use it when you need it
Type conversions
There are several ways we can use to perform type conversions, such as as
, From/Intro
, TryFrom/TryInto
, transmute
etc.
使用 as 进行类型转换
Rust 并没有为基本类型提供隐式的类型转换( coercion ),但是我们可以通过 as
来进行显式地转换。
- 🌟
// 修复错误,填空 // 不要移除任何代码 fn main() { let decimal = 97.123_f32; let integer: __ = decimal as u8; let c1: char = decimal as char; let c2 = integer as char; assert_eq!(integer, 'b' as u8); println!("Success!") }
- 🌟🌟 默认情况下, 数值溢出会导致编译错误,但是我们可以通过添加一行全局注解的方式来避免编译错误(溢出还是会发生)
fn main() { assert_eq!(u8::MAX, 255); // 如上所示,u8 类型允许的最大值是 255. // 因此以下代码会报溢出的错误: literal out of range for `u8`. // **请仔细查看相应的编译错误,从中寻找到解决的办法** // **不要修改 main 中的任何代码** let v = 1000 as u8; println!("Success!") }
- 🌟🌟 当将任何数值转换成无符号整型
T
时,如果当前的数值不在新类型的范围内,我们可以对当前数值进行加值或减值操作( 增加或减少T::MAX + 1
),直到最新的值在新类型的范围内,假设我们要将300
转成u8
类型,由于u8
最大值是 255,因此300
不在新类型的范围内并且大于新类型的最大值,因此我们需要减去T::MAX + 1
,也就是300
-256
=44
。
fn main() { assert_eq!(1000 as u16, __); assert_eq!(1000 as u8, __); // 事实上,之前说的规则对于正整数而言,就是如下的取模 println!("1000 mod 256 is : {}", 1000 % 256); assert_eq!(-1_i8 as u8, __); // 从 Rust 1.45 开始,当浮点数超出目标整数的范围时,转化会直接取正整数取值范围的最大或最小值 assert_eq!(300.1_f32 as u8, __); assert_eq!(-100.1_f32 as u8, __); // 上面的浮点数转换有一点性能损耗,如果大家对于某段代码有极致的性能要求, // 可以考虑下面的方法,但是这些方法的结果可能会溢出并且返回一些无意义的值 // 总之,请小心使用 unsafe { // 300.0 is 44 println!("300.0 is {}", 300.0_f32.to_int_unchecked::<u8>()); // -100.0 as u8 is 156 println!("-100.0 as u8 is {}", (-100.0_f32).to_int_unchecked::<u8>()); // nan as u8 is 0 println!("nan as u8 is {}", f32::NAN.to_int_unchecked::<u8>()); } }
- 🌟🌟🌟 裸指针可以和代表内存地址的整数互相转换
// 填空 fn main() { let mut values: [i32; 2] = [1, 2]; let p1: *mut i32 = values.as_mut_ptr(); let first_address: usize = p1 __; let second_address = first_address + 4; // 4 == std::mem::size_of::<i32>() let p2: *mut i32 = second_address __; // p2 指向 values 数组中的第二个元素 unsafe { // 将第二个元素加 1 __ } assert_eq!(values[1], 3); println!("Success!") }
- 🌟🌟🌟
fn main() { let arr :[u64; 13] = [0; 13]; assert_eq!(std::mem::size_of_val(&arr), 8 * 13); let a: *const [u64] = &arr; let b = a as *const [u8]; unsafe { assert_eq!(std::mem::size_of_val(&*b), __) } }
From/Into
From
特征允许让一个类型定义如何基于另一个类型来创建自己,因此它提供了一个很方便的类型转换的方式。
From
和 Into
是配对的,我们只要实现了前者,那后者就会自动被实现:只要实现了 impl From<T> for U
, 就可以使用以下两个方法: let u: U = U::from(T)
和 let u:U = T.into()
,前者由 From
特征提供,而后者由自动实现的 Into
特征提供。
需要注意的是,当使用 into
方法时,你需要进行显式地类型标注,因为编译器很可能无法帮我们推导出所需的类型。
来看一个例子,我们可以简单的将 &str
转换成 String
。`
fn main() { let my_str = "hello"; // 以下三个转换都依赖于一个事实:String 实现了 From<&str> 特征 let string1 = String::from(my_str); let string2 = my_str.to_string(); // 这里需要显式地类型标注 let string3: String = my_str.into(); }
这种转换可以发生是因为标准库已经帮我们实现了 From
特征: impl From<&'_ str> for String
。你还可以在这里)找到其它实现 From
特征的常用类型。
- 🌟🌟🌟
fn main() { // impl From<bool> for i32 let i1:i32 = false.into(); let i2:i32 = i32::from(false); assert_eq!(i1, i2); assert_eq!(i1, 0); // 使用两种方式修复错误 // 1. 哪个类型实现 From 特征 : impl From<char> for ? , 你可以查看一下之前提到的文档,来找到合适的类型 // 2. 上一章节中介绍过的某个关键字 let i3: i32 = 'a'.into(); // 使用两种方法来解决错误 let s: String = 'a' as String; println!("Success!") }
为自定义类型实现 From
特征
- 🌟🌟
// From 被包含在 `std::prelude` 中,因此我们没必要手动将其引入到当前作用域来 // use std::convert::From; #[derive(Debug)] struct Number { value: i32, } impl From<i32> for Number { // 实现 `from` 方法 } // 填空 fn main() { let num = __(30); assert_eq!(num.value, 30); let num: Number = __; assert_eq!(num.value, 30); println!("Success!") }
- 🌟🌟🌟 当执行错误处理时,为我们自定义的错误类型实现
From
特征是非常有用。这样就可以通过?
自动将某个错误类型转换成我们自定义的错误类型
use std::fs; use std::io; use std::num; enum CliError { IoError(io::Error), ParseError(num::ParseIntError), } impl From<io::Error> for CliError { // 实现 from 方法 } impl From<num::ParseIntError> for CliError { // 实现 from 方法 } fn open_and_parse_file(file_name: &str) -> Result<i32, CliError> { // ? 自动将 io::Error 转换成 CliError let contents = fs::read_to_string(&file_name)?; // num::ParseIntError -> CliError let num: i32 = contents.trim().parse()?; Ok(num) } fn main() { println!("Success!") }
TryFrom/TryInto
类似于 From
和 Into
, TryFrom
和 TryInto
也是用于类型转换的泛型特。
但是又与 From/Into
不同, TryFrom
和 TryInto
可以对转换后的失败进行处理,然后返回一个 Result
。
- 🌟🌟
// TryFrom 和 TryInto 也被包含在 `std::prelude` 中, 因此以下引入是没必要的 // use std::convert::TryInto; fn main() { let n: i16 = 256; // Into 特征拥有一个方法`into`, // 因此 TryInto 有一个方法是 ? let n: u8 = match n.__() { Ok(n) => n, Err(e) => { println!("there is an error when converting: {:?}, but we catch it", e.to_string()); 0 } }; assert_eq!(n, __); println!("Success!") }
- 🌟🌟🌟
#[derive(Debug, PartialEq)] struct EvenNum(i32); impl TryFrom<i32> for EvenNum { type Error = (); // 实现 `try_from` fn try_from(value: i32) -> Result<Self, Self::Error> { if value % 2 == 0 { Ok(EvenNum(value)) } else { Err(()) } } } fn main() { assert_eq!(EvenNum::try_from(8), Ok(EvenNum(8))); assert_eq!(EvenNum::try_from(5), Err(())); // 填空 let result: Result<EvenNum, ()> = 8i32.try_into(); assert_eq!(result, __); let result: Result<EvenNum, ()> = 5i32.try_into(); assert_eq!(result, __); println!("Success!") }
其它转换
将任何类型转换成 String
只要为一个类型实现了 ToString
,就可以将任何类型转换成 String
。事实上,这种方式并不是最好的,大家还记得 fmt::Display
特征吗?它可以控制一个类型如何打印,在实现它的时候还会自动实现 ToString
。
- 🌟🌟
use std::fmt; struct Point { x: i32, y: i32, } impl fmt::Display for Point { // 实现 fmt 方法 } fn main() { let origin = Point { x: 0, y: 0 }; // 填空 assert_eq!(origin.__, "The point is (0, 0)"); assert_eq!(format!(__), "The point is (0, 0)"); println!("Success!") }
解析 String
- 🌟🌟🌟 使用
parse
方法可以将一个String
转换成i32
数字,这是因为在标准库中为i32
类型实现了FromStr
: :impl FromStr for i32
// 为了使用 `from_str` 方法, 你需要引入该特征到当前作用域中 use std::str::FromStr; fn main() { let parsed: i32 = "5".__.unwrap(); let turbo_parsed = "10".__.unwrap(); let from_str = __.unwrap(); let sum = parsed + turbo_parsed + from_str; assert_eq!(sum, 35); println!("Success!") }
- 🌟🌟 还可以为自定义类型实现
FromStr
特征
use std::str::FromStr; use std::num::ParseIntError; #[derive(Debug, PartialEq)] struct Point { x: i32, y: i32 } impl FromStr for Point { type Err = ParseIntError; fn from_str(s: &str) -> Result<Self, Self::Err> { let coords: Vec<&str> = s.trim_matches(|p| p == '(' || p == ')' ) .split(',') .collect(); let x_fromstr = coords[0].parse::<i32>()?; let y_fromstr = coords[1].parse::<i32>()?; Ok(Point { x: x_fromstr, y: y_fromstr }) } } fn main() { // 使用两种方式填空 // 不要修改其它地方的代码 let p = __; assert_eq!(p.unwrap(), Point{ x: 3, y: 4} ); println!("Success!") }
Deref 特征
Deref 特征在智能指针 - Deref章节中有更加详细的介绍。
transmute
std::mem::transmute
是一个 unsafe 函数,可以把一个类型按位解释为另一个类型,其中这两个类型必须有同样的位数( bits )。
transmute
相当于将一个类型按位移动到另一个类型,它会将源值的所有位拷贝到目标值中,然后遗忘源值。该函数跟 C 语言中的 memcpy
函数类似。
正因为此,transmute
非常非常不安全! 调用者必须要自己保证代码的安全性,当然这也是 unsafe 的目的。
示例
transmute
可以将一个指针转换成一个函数指针,该转换并不具备可移植性,原因是在不同机器上,函数指针和数据指针可能有不同的位数( size )。
fn foo() -> i32 { 0 } fn main() { let pointer = foo as *const (); let function = unsafe { std::mem::transmute::<*const (), fn() -> i32>(pointer) assert_eq!(function(), 0); }
transmute
还可以扩展或缩短一个不变量的生命周期,将 Unsafe Rust 的不安全性体现的淋漓尽致!
struct R<'a>(&'a i32); unsafe fn extend_lifetime<'b>(r: R<'b>) -> R<'static> { std::mem::transmute::<R<'b>, R<'static>>(r) } unsafe fn shorten_invariant_lifetime<'b, 'c>(r: &'b mut R<'static>) -> &'b mut R<'c> { std::mem::transmute::<&'b mut R<'static>, &'b mut R<'c>>(r) }
- 事实上我们还可以使用一些安全的方法来替代
transmute
.
fn main() { /*Turning raw bytes(&[u8]) to u32, f64, etc.: */ let raw_bytes = [0x78, 0x56, 0x34, 0x12]; let num = unsafe { std::mem::transmute::<[u8; 4], u32>(raw_bytes) }; // use `u32::from_ne_bytes` instead let num = u32::from_ne_bytes(raw_bytes); // or use `u32::from_le_bytes` or `u32::from_be_bytes` to specify the endianness let num = u32::from_le_bytes(raw_bytes); assert_eq!(num, 0x12345678); let num = u32::from_be_bytes(raw_bytes); assert_eq!(num, 0x78563412); /*Turning a pointer into a usize: */ let ptr = &0; let ptr_num_transmute = unsafe { std::mem::transmute::<&i32, usize>(ptr) }; // Use an `as` cast instead let ptr_num_cast = ptr as *const i32 as usize; /*Turning an &mut T into an &mut U: */ let ptr = &mut 0; let val_transmuted = unsafe { std::mem::transmute::<&mut i32, &mut u32>(ptr) }; // Now, put together `as` and reborrowing - note the chaining of `as` // `as` is not transitive let val_casts = unsafe { &mut *(ptr as *mut i32 as *mut u32) }; /*Turning an &str into a &[u8]: */ // this is not a good way to do this. let slice = unsafe { std::mem::transmute::<&str, &[u8]>("Rust") }; assert_eq!(slice, &[82, 117, 115, 116]); // You could use `str::as_bytes` let slice = "Rust".as_bytes(); assert_eq!(slice, &[82, 117, 115, 116]); // Or, just use a byte string, if you have control over the string // literal assert_eq!(b"Rust", &[82, 117, 115, 116]); }
Result and panic
Learning resources:
- English: Rust Book 9.1, 9.2
- 简体中文: Rust语言圣经 - 返回值和错误处理
panic!
Rust 中最简单的错误处理方式就是使用 panic
。它会打印出一条错误信息并打印出栈调用情况,最终结束当前线程:
- 若 panic 发生在
main
线程,那程序会随之退出 - 如果是在生成的( spawn )子线程中发生 panic, 那么当前的线程会结束,但是程序依然会继续运行
- 🌟🌟
// 填空 fn drink(beverage: &str) { if beverage == "lemonade" { println!("Success!"); // 实现下面的代码 __ } println!("Excercise Failed if printing out this line!"); } fn main() { drink(__); println!("Excercise Failed if printing out this line!"); }
常见的 panic
- 🌟🌟
// 修复所有的 panic,让代码工作 fn main() { assert_eq!("abc".as_bytes(), [96, 97, 98]); let v = vec![1, 2, 3]; let ele = v[3]; let ele = v.get(3).unwrap(); // 大部分时候编译器是可以帮我们提前发现溢出错误,并阻止编译通过。但是也有一些时候,这种溢出问题直到运行期才会出现 let v = production_rate_per_hour(2); divide(15, 0); println!("Success!") } fn divide(x:u8, y:u8) { println!("{}", x / y) } fn production_rate_per_hour(speed: u8) -> f64 { let cph: u8 = 221; match speed { 1..=4 => (speed * cph) as f64, 5..=8 => (speed * cph) as f64 * 0.9, 9..=10 => (speed * cph) as f64 * 0.77, _ => 0 as f64, } } pub fn working_items_per_minute(speed: u8) -> u32 { (production_rate_per_hour(speed) / 60 as f64) as u32 }
详细的栈调用信息
默认情况下,栈调用只会展示最基本的信息:
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
但是有时候,我们还希望获取更详细的信息:
- 🌟
## 填空以打印全部的调用栈
## 提示: 你可以在之前的默认 panic 信息中找到相关线索
$ __ cargo run
thread 'main' panicked at 'assertion failed: `(left == right)`
left: `[97, 98, 99]`,
right: `[96, 97, 98]`', src/main.rs:3:5
stack backtrace:
0: rust_begin_unwind
at /rustc/9d1b2106e23b1abd32fce1f17267604a5102f57a/library/std/src/panicking.rs:498:5
1: core::panicking::panic_fmt
at /rustc/9d1b2106e23b1abd32fce1f17267604a5102f57a/library/core/src/panicking.rs:116:14
2: core::panicking::assert_failed_inner
3: core::panicking::assert_failed
at /rustc/9d1b2106e23b1abd32fce1f17267604a5102f57a/library/core/src/panicking.rs:154:5
4: study_cargo::main
at ./src/main.rs:3:5
5: core::ops::function::FnOnce::call_once
at /rustc/9d1b2106e23b1abd32fce1f17267604a5102f57a/library/core/src/ops/function.rs:227:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
unwinding
和 abort
当出现 panic!
时,程序提供了两种方式来处理终止流程:栈展开和直接终止。
其中,默认的方式就是 栈展开
,这意味着 Rust 会回溯栈上数据和函数调用,因此也意味着更多的善后工作,好处是可以给出充分的报错信息和栈调用信息,便于事后的问题复盘。直接终止
,顾名思义,不清理数据就直接退出程序,善后工作交与操作系统来负责。
对于绝大多数用户,使用默认选择是最好的,但是当你关心最终编译出的二进制可执行文件大小时,那么可以尝试去使用直接终止的方式,例如下面的配置修改 Cargo.toml
文件,实现在 release
模式下遇到 panic
直接终止:
#![allow(unused)] fn main() { [profile.release] panic = 'abort' }
result and ?
Result<T>
是一个枚举类型用于描述返回的结果或错误,它包含两个成员(变体 variants) :
Ok(T)
: 返回一个结果值 TErr(e)
: 返回一个错误,e
是具体的错误值
简而言之,如果期待一个正确的结果,就返回 Ok
,反之则是 Err
。
- 🌟🌟
// 填空并修复错误 use std::num::ParseIntError; fn multiply(n1_str: &str, n2_str: &str) -> __ { let n1 = n1_str.parse::<i32>(); let n2 = n2_str.parse::<i32>(); Ok(n1.unwrap() * n2.unwrap()) } fn main() { let result = multiply("10", "2"); assert_eq!(result, __); let result = multiply("t", "2"); assert_eq!(result.__, 8); println!("Success!") }
?
?
跟 unwrap
非常像,但是 ?
会返回一个错误,而不是直接 panic.
- 🌟🌟
use std::num::ParseIntError; // 使用 `?` 来实现 multiply // 不要使用 unwrap ! fn multiply(n1_str: &str, n2_str: &str) -> __ { } fn main() { assert_eq!(multiply("3", "4").unwrap(), 12); println!("Success!") }
- 🌟🌟
use std::fs::File; use std::io::{self, Read}; fn read_file1() -> 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_file2() -> Result<String, io::Error> { let mut s = String::new(); __; Ok(s) } fn main() { assert_eq!(read_file1().unwrap_err().to_string(), read_file2().unwrap_err().to_string()); println!("Success!") }
map & and_then
map and and_then 是两个常用的组合器( combinator ),可以用于 Result<T, E>
(也可用于 Option<T>
).
- 🌟🌟
use std::num::ParseIntError; // 使用两种方式填空: map, and then fn add_two(n_str: &str) -> Result<i32, ParseIntError> { n_str.parse::<i32>().__ } fn main() { assert_eq!(add_two("4").unwrap(), 6); println!("Success!") }
- 🌟🌟🌟
use std::num::ParseIntError; // 使用 Result 重写后,我们使用模式匹配的方式来处理,而无需使用 `unwrap` // 但是这种写法实在过于啰嗦.. fn multiply(n1_str: &str, n2_str: &str) -> Result<i32, ParseIntError> { match n1_str.parse::<i32>() { Ok(n1) => { match n2_str.parse::<i32>() { Ok(n2) => { Ok(n1 * n2) }, Err(e) => Err(e), } }, Err(e) => Err(e), } } // 重写上面的 `multiply` ,让它尽量简介 // 提示:使用 `and_then` 和 `map` fn multiply1(n1_str: &str, n2_str: &str) -> Result<i32, ParseIntError> { // 实现... } fn print(result: Result<i32, ParseIntError>) { match result { Ok(n) => println!("n is {}", n), Err(e) => println!("Error: {}", e), } } fn main() { let twenty = multiply1("10", "2"); print(twenty); // 下面的调用会提供更有帮助的错误信息 let tt = multiply("t", "2"); print(tt); println!("Success!") }
类型别名
如果我们要在代码中到处使用 std::result::Result<T, ParseIntError>
,那毫无疑问,代码将变得特别冗长和啰嗦,对于这种情况,可以使用类型别名来解决。
例如在标准库中,就在大量使用这种方式来简化代码: io::Result
.
- 🌟
use std::num::ParseIntError; // 填空 type __; // 使用上面的别名来引用原来的 `Result` 类型 fn multiply(first_number_str: &str, second_number_str: &str) -> Res<i32> { first_number_str.parse::<i32>().and_then(|first_number| { second_number_str.parse::<i32>().map(|second_number| first_number * second_number) }) } // 同样, 这里也使用了类型别名来简化代码 fn print(result: Res<i32>) { match result { Ok(n) => println!("n is {}", n), Err(e) => println!("Error: {}", e), } } fn main() { print(multiply("10", "2")); print(multiply("t", "2")); println!("Success!") }
在 fn main
中使用 Result
一个典型的 main
函数长这样:
fn main() { println!("Hello World!"); }
事实上 main
函数还可以返回一个 Result
类型:如果 main
函数内部发生了错误,那该错误会被返回并且打印出一条错误的 debug 信息。
use std::num::ParseIntError; fn main() -> Result<(), ParseIntError> { let number_str = "10"; let number = match number_str.parse::<i32>() { Ok(number) => number, Err(e) => return Err(e), }; println!("{}", number); Ok(()) }
包和模块
学习资料:
- 简体中文: Rust语言圣经 - 包和模块
Crate
Module
use and pub
Comments and Docs
Formatted output
Lifetime
生命周期消除
#![allow(unused)] fn main() { fn print(s: &str); // elided fn print<'a>(s: &'a str); // expanded fn debug(lvl: usize, s: &str); // elided fn debug<'a>(lvl: usize, s: &'a str); // expanded fn substr(s: &str, until: usize) -> &str; // elided fn substr<'a>(s: &'a str, until: usize) -> &'a str; // expanded fn get_str() -> &str; // ILLEGAL fn frob(s: &str, t: &str) -> &str; // ILLEGAL fn get_mut(&mut self) -> &mut T; // elided fn get_mut<'a>(&'a mut self) -> &'a mut T; // expanded fn args<T: ToCStr>(&mut self, args: &[T]) -> &mut Command // elided fn args<'a, 'b, T: ToCStr>(&'a mut self, args: &'b [T]) -> &'a mut Command // expanded fn new(buf: &mut [u8]) -> BufWriter; // elided fn new(buf: &mut [u8]) -> BufWriter<'_>; // elided (with `rust_2018_idioms`) fn new<'a>(buf: &'a mut [u8]) -> BufWriter<'a> // expanded }
&'static and T: 'static
use std::fmt::Display; fn main() { let mut string = "First".to_owned(); string.push_str(string.to_uppercase().as_str()); print_a(&string); print_b(&string); print_c(&string); // Compilation error print_d(&string); // Compilation error print_e(&string); print_f(&string); print_g(&string); // Compilation error } fn print_a<T: Display + 'static>(t: &T) { println!("{}", t); } fn print_b<T>(t: &T) where T: Display + 'static, { println!("{}", t); } fn print_c(t: &'static dyn Display) { println!("{}", t) } fn print_d(t: &'static impl Display) { println!("{}", t) } fn print_e(t: &(dyn Display + 'static)) { println!("{}", t) } fn print_f(t: &(impl Display + 'static)) { println!("{}", t) } fn print_g(t: &'static String) { println!("{}", t); }
advance
Functional programing
Closure
下面代码是Rust圣经课程中闭包章节的课内练习题答案:
struct Cacher<T,E> where T: Fn(E) -> E, E: Copy { query: T, value: Option<E>, } impl<T,E> Cacher<T,E> where T: Fn(E) -> E, E: Copy { fn new(query: T) -> Cacher<T,E> { Cacher { query, value: None, } } fn value(&mut self, arg: E) -> E { match self.value { Some(v) => v, None => { let v = (self.query)(arg); self.value = Some(v); v } } } } fn main() { } #[test] fn call_with_different_values() { let mut c = Cacher::new(|a| a); let v1 = c.value(1); let v2 = c.value(2); assert_eq!(v2, 1); }
Iterator
newtype and Sized
Smart pointers
Box
Deref
Drop
Rc and Arc
Cell and RefCell
Weak and Circle reference
Self referential
Threads
Basic using
Message passing
Sync
Atomic
Send and Sync
Global variables
Errors
Unsafe doing
内联汇编
Rust provides support for inline assembly via the asm!
macro.
It can be used to embed handwritten assembly in the assembly output generated by the compiler.
Generally this should not be necessary, but might be where the required performance or timing
cannot be otherwise achieved. Accessing low level hardware primitives, e.g. in kernel code, may also demand this functionality.
Note: the examples here are given in x86/x86-64 assembly, but other architectures are also supported.
Inline assembly is currently supported on the following architectures:
- x86 and x86-64
- ARM
- AArch64
- RISC-V
Basic usage
Let us start with the simplest possible example:
#![allow(unused)] fn main() { use std::arch::asm; unsafe { asm!("nop"); } }
This will insert a NOP (no operation) instruction into the assembly generated by the compiler.
Note that all asm!
invocations have to be inside an unsafe
block, as they could insert
arbitrary instructions and break various invariants. The instructions to be inserted are listed
in the first argument of the asm!
macro as a string literal.
Inputs and outputs
Now inserting an instruction that does nothing is rather boring. Let us do something that actually acts on data:
#![allow(unused)] fn main() { use std::arch::asm; let x: u64; unsafe { asm!("mov {}, 5", out(reg) x); } assert_eq!(x, 5); }
This will write the value 5
into the u64
variable x
.
You can see that the string literal we use to specify instructions is actually a template string.
It is governed by the same rules as Rust format strings.
The arguments that are inserted into the template however look a bit different than you may
be familiar with. First we need to specify if the variable is an input or an output of the
inline assembly. In this case it is an output. We declared this by writing out
.
We also need to specify in what kind of register the assembly expects the variable.
In this case we put it in an arbitrary general purpose register by specifying reg
.
The compiler will choose an appropriate register to insert into
the template and will read the variable from there after the inline assembly finishes executing.
Let us see another example that also uses an input:
#![allow(unused)] fn main() { use std::arch::asm; let i: u64 = 3; let o: u64; unsafe { asm!( "mov {0}, {1}", "add {0}, 5", out(reg) o, in(reg) i, ); } assert_eq!(o, 8); }
This will add 5
to the input in variable i
and write the result to variable o
.
The particular way this assembly does this is first copying the value from i
to the output,
and then adding 5
to it.
The example shows a few things:
First, we can see that asm!
allows multiple template string arguments; each
one is treated as a separate line of assembly code, as if they were all joined
together with newlines between them. This makes it easy to format assembly
code.
Second, we can see that inputs are declared by writing in
instead of out
.
Third, we can see that we can specify an argument number, or name as in any format string. For inline assembly templates this is particularly useful as arguments are often used more than once. For more complex inline assembly using this facility is generally recommended, as it improves readability, and allows reordering instructions without changing the argument order.
We can further refine the above example to avoid the mov
instruction:
#![allow(unused)] fn main() { use std::arch::asm; let mut x: u64 = 3; unsafe { asm!("add {0}, 5", inout(reg) x); } assert_eq!(x, 8); }
We can see that inout
is used to specify an argument that is both input and output.
This is different from specifying an input and output separately in that it is guaranteed to assign both to the same register.
It is also possible to specify different variables for the input and output parts of an inout
operand:
#![allow(unused)] fn main() { use std::arch::asm; let x: u64 = 3; let y: u64; unsafe { asm!("add {0}, 5", inout(reg) x => y); } assert_eq!(y, 8); }
Late output operands
The Rust compiler is conservative with its allocation of operands. It is assumed that an out
can be written at any time, and can therefore not share its location with any other argument.
However, to guarantee optimal performance it is important to use as few registers as possible,
so they won't have to be saved and reloaded around the inline assembly block.
To achieve this Rust provides a lateout
specifier. This can be used on any output that is
written only after all inputs have been consumed.
There is also a inlateout
variant of this specifier.
Here is an example where inlateout
cannot be used:
#![allow(unused)] fn main() { use std::arch::asm; let mut a: u64 = 4; let b: u64 = 4; let c: u64 = 4; unsafe { asm!( "add {0}, {1}", "add {0}, {2}", inout(reg) a, in(reg) b, in(reg) c, ); } assert_eq!(a, 12); }
Here the compiler is free to allocate the same register for inputs b
and c
since it knows they have the same value. However it must allocate a separate register for a
since it uses inout
and not inlateout
. If inlateout
was used, then a
and c
could be allocated to the same register, in which case the first instruction to overwrite the value of c
and cause the assembly code to produce the wrong result.
However the following example can use inlateout
since the output is only modified after all input registers have been read:
#![allow(unused)] fn main() { use std::arch::asm; let mut a: u64 = 4; let b: u64 = 4; unsafe { asm!("add {0}, {1}", inlateout(reg) a, in(reg) b); } assert_eq!(a, 8); }
As you can see, this assembly fragment will still work correctly if a
and b
are assigned to the same register.
Explicit register operands
Some instructions require that the operands be in a specific register.
Therefore, Rust inline assembly provides some more specific constraint specifiers.
While reg
is generally available on any architecture, explicit registers are highly architecture specific. E.g. for x86 the general purpose registers eax
, ebx
, ecx
, edx
, ebp
, esi
, and edi
among others can be addressed by their name.
#![allow(unused)] fn main() { use std::arch::asm; let cmd = 0xd1; unsafe { asm!("out 0x64, eax", in("eax") cmd); } }
In this example we call the out
instruction to output the content of the cmd
variable to port 0x64
. Since the out
instruction only accepts eax
(and its sub registers) as operand we had to use the eax
constraint specifier.
Note: unlike other operand types, explicit register operands cannot be used in the template string: you can't use
{}
and should write the register name directly instead. Also, they must appear at the end of the operand list after all other operand types.
Consider this example which uses the x86 mul
instruction:
#![allow(unused)] fn main() { use std::arch::asm; fn mul(a: u64, b: u64) -> u128 { let lo: u64; let hi: u64; unsafe { asm!( // The x86 mul instruction takes rax as an implicit input and writes // the 128-bit result of the multiplication to rax:rdx. "mul {}", in(reg) a, inlateout("rax") b => lo, lateout("rdx") hi ); } ((hi as u128) << 64) + lo as u128 } }
This uses the mul
instruction to multiply two 64-bit inputs with a 128-bit result.
The only explicit operand is a register, that we fill from the variable a
.
The second operand is implicit, and must be the rax
register, which we fill from the variable b
.
The lower 64 bits of the result are stored in rax
from which we fill the variable lo
.
The higher 64 bits are stored in rdx
from which we fill the variable hi
.
Clobbered registers
In many cases inline assembly will modify state that is not needed as an output. Usually this is either because we have to use a scratch register in the assembly or because instructions modify state that we don't need to further examine. This state is generally referred to as being "clobbered". We need to tell the compiler about this since it may need to save and restore this state around the inline assembly block.
use core::arch::asm; fn main() { // three entries of four bytes each let mut name_buf = [0_u8; 12]; // String is stored as ascii in ebx, edx, ecx in order // Because ebx is reserved, we get a scratch register and move from // ebx into it in the asm. The asm needs to preserve the value of // that register though, so it is pushed and popped around the main asm // (in 64 bit mode for 64 bit processors, 32 bit processors would use ebx) unsafe { asm!( "push rbx", "cpuid", "mov [{0}], ebx", "mov [{0} + 4], edx", "mov [{0} + 8], ecx", "pop rbx", // We use a pointer to an array for storing the values to simplify // the Rust code at the cost of a couple more asm instructions // This is more explicit with how the asm works however, as opposed // to explicit register outputs such as `out("ecx") val` // The *pointer itself* is only an input even though it's written behind in(reg) name_buf.as_mut_ptr(), // select cpuid 0, also specify eax as clobbered inout("eax") 0 => _, // cpuid clobbers these registers too out("ecx") _, out("edx") _, ); } let name = core::str::from_utf8(&name_buf).unwrap(); println!("CPU Manufacturer ID: {}", name); }
In the example above we use the cpuid
instruction to read the CPU manufacturer ID.
This instruction writes to eax
with the maximum supported cpuid
argument and ebx
, esx
, and ecx
with the CPU manufacturer ID as ASCII bytes in that order.
Even though eax
is never read we still need to tell the compiler that the register has been modified so that the compiler can save any values that were in these registers before the asm. This is done by declaring it as an output but with _
instead of a variable name, which indicates that the output value is to be discarded.
This code also works around the limitation that ebx
is a reserved register by LLVM. That means that LLVM assumes that it has full control over the register and it must be restored to its original state before exiting the asm block, so it cannot be used as an output. To work around this we save the register via push
, read from ebx
inside the asm block into a temporary register allocated with out(reg)
and then restoring ebx
to its original state via pop
. The push
and pop
use the full 64-bit rbx
version of the register to ensure that the entire register is saved. On 32 bit targets the code would instead use ebx
in the push
/pop
.
This can also be used with a general register class (e.g. reg
) to obtain a scratch register for use inside the asm code:
#![allow(unused)] fn main() { use std::arch::asm; // Multiply x by 6 using shifts and adds let mut x: u64 = 4; unsafe { asm!( "mov {tmp}, {x}", "shl {tmp}, 1", "shl {x}, 2", "add {x}, {tmp}", x = inout(reg) x, tmp = out(reg) _, ); } assert_eq!(x, 4 * 6); }
Symbol operands and ABI clobbers
By default, asm!
assumes that any register not specified as an output will have its contents preserved by the assembly code. The clobber_abi
argument to asm!
tells the compiler to automatically insert the necessary clobber operands according to the given calling convention ABI: any register which is not fully preserved in that ABI will be treated as clobbered. Multiple clobber_abi
arguments may be provided and all clobbers from all specified ABIs will be inserted.
#![allow(unused)] fn main() { use std::arch::asm; extern "C" fn foo(arg: i32) -> i32 { println!("arg = {}", arg); arg * 2 } fn call_foo(arg: i32) -> i32 { unsafe { let result; asm!( "call *{}", // Function pointer to call in(reg) foo, // 1st argument in rdi in("rdi") arg, // Return value in rax out("rax") result, // Mark all registers which are not preserved by the "C" calling // convention as clobbered. clobber_abi("C"), ); result } } }
Register template modifiers
In some cases, fine control is needed over the way a register name is formatted when inserted into the template string. This is needed when an architecture's assembly language has several names for the same register, each typically being a "view" over a subset of the register (e.g. the low 32 bits of a 64-bit register).
By default the compiler will always choose the name that refers to the full register size (e.g. rax
on x86-64, eax
on x86, etc).
This default can be overriden by using modifiers on the template string operands, just like you would with format strings:
#![allow(unused)] fn main() { use std::arch::asm; let mut x: u16 = 0xab; unsafe { asm!("mov {0:h}, {0:l}", inout(reg_abcd) x); } assert_eq!(x, 0xabab); }
In this example, we use the reg_abcd
register class to restrict the register allocator to the 4 legacy x86 registers (ax
, bx
, cx
, dx
) of which the first two bytes can be addressed independently.
Let us assume that the register allocator has chosen to allocate x
in the ax
register.
The h
modifier will emit the register name for the high byte of that register and the l
modifier will emit the register name for the low byte. The asm code will therefore be expanded as mov ah, al
which copies the low byte of the value into the high byte.
If you use a smaller data type (e.g. u16
) with an operand and forget the use template modifiers, the compiler will emit a warning and suggest the correct modifier to use.
Memory address operands
Sometimes assembly instructions require operands passed via memory addresses/memory locations.
You have to manually use the memory address syntax specified by the target architecture.
For example, on x86/x86_64 using Intel assembly syntax, you should wrap inputs/outputs in []
to indicate they are memory operands:
#![allow(unused)] fn main() { use std::arch::asm; fn load_fpu_control_word(control: u16) { unsafe { asm!("fldcw [{}]", in(reg) &control, options(nostack)); } } }
Labels
Any reuse of a named label, local or otherwise, can result in an assembler or linker error or may cause other strange behavior. Reuse of a named label can happen in a variety of ways including:
- explicitly: using a label more than once in one
asm!
block, or multiple times across blocks. - implicitly via inlining: the compiler is allowed to instantiate multiple copies of an
asm!
block, for example when the function containing it is inlined in multiple places. - implicitly via LTO: LTO can cause code from other crates to be placed in the same codegen unit, and so could bring in arbitrary labels.
As a consequence, you should only use GNU assembler numeric local labels inside inline assembly code. Defining symbols in assembly code may lead to assembler and/or linker errors due to duplicate symbol definitions.
Moreover, on x86 when using the default Intel syntax, due to an LLVM bug, you shouldn't use labels exclusively made of 0
and 1
digits, e.g. 0
, 11
or 101010
, as they may end up being interpreted as binary values. Using options(att_syntax)
will avoid any ambiguity, but that affects the syntax of the entire asm!
block. (See Options, below, for more on options
.)
#![allow(unused)] fn main() { use std::arch::asm; let mut a = 0; unsafe { asm!( "mov {0}, 10", "2:", "sub {0}, 1", "cmp {0}, 3", "jle 2f", "jmp 2b", "2:", "add {0}, 2", out(reg) a ); } assert_eq!(a, 5); }
This will decrement the {0}
register value from 10 to 3, then add 2 and store it in a
.
This example shows a few things:
- First, that the same number can be used as a label multiple times in the same inline block.
- Second, that when a numeric label is used as a reference (as an instruction operand, for example), the suffixes “b” (“backward”) or ”f” (“forward”) should be added to the numeric label. It will then refer to the nearest label defined by this number in this direction.
Options
By default, an inline assembly block is treated the same way as an external FFI function call with a custom calling convention: it may read/write memory, have observable side effects, etc. However, in many cases it is desirable to give the compiler more information about what the assembly code is actually doing so that it can optimize better.
Let's take our previous example of an add
instruction:
#![allow(unused)] fn main() { use std::arch::asm; let mut a: u64 = 4; let b: u64 = 4; unsafe { asm!( "add {0}, {1}", inlateout(reg) a, in(reg) b, options(pure, nomem, nostack), ); } assert_eq!(a, 8); }
Options can be provided as an optional final argument to the asm!
macro. We specified three options here:
pure
means that the asm code has no observable side effects and that its output depends only on its inputs. This allows the compiler optimizer to call the inline asm fewer times or even eliminate it entirely.nomem
means that the asm code does not read or write to memory. By default the compiler will assume that inline assembly can read or write any memory address that is accessible to it (e.g. through a pointer passed as an operand, or a global).nostack
means that the asm code does not push any data onto the stack. This allows the compiler to use optimizations such as the stack red zone on x86-64 to avoid stack pointer adjustments.
These allow the compiler to better optimize code using asm!
, for example by eliminating pure asm!
blocks whose outputs are not needed.
See the reference for the full list of available options and their effects.
macro
Tests
Write Tests
Benchmark
https://doc.rust-lang.org/unstable-book/library-features/test.html