rustlings : Rust 小练习
Rustlings 是非常适合入门的 Rust 教程,在学习过程中我以一个新手的角度记录了一些做题的思索,希望能帮到你。
当然,实际上 Rustlings 是完全不需要看答案的,文档和 hint 以及足够帮我们完成所有实验了。
Intro1
移除 // I AM NOT DONE
这一行即可。
Intro2
报错
1
2
3
4
5
6
7
8
|
⚠️ Compiling of exercises/intro/intro2.rs failed! Please try again. Here's the output:
error: 1 positional argument in format string, but no arguments were given
--> exercises/intro/intro2.rs:8:21
|
8 | println!("Hello {}!");
| ^^
error: aborting due to previous error
|
这应该是没有参数的问题,那么 Rust 怎么添加参数呢?参考 Formatted print,有非常多的方式,这里使用最简单的,可以直接插入字符串。
1
2
3
|
fn main() {
println!("Hello {}!", "QRZ");
}
|
variables1
难点是 x 赋值,用 let 赋值即可。
variables2
本以为是没有给变量类型,后来发现是没有初始化。
variables3
变量没有添加 mut 可变字段
variables4
虽然给了变量类型,但是没有初始化。
variables5
开始的时候给参数的类型是 str,下面变成了 int,我应该改名字吗?
不用,参考 https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#shadowing,给下面的变量加上 let 关键字就可以复用变量名称了。
variables6
报错信息提示很明显,给 const 的值要加变量类型。
functions1
函数没定义,我们定义一个空函数 call_me()
即可。
functions2
函数参数需要指定类型。
functions3
调用带参数的函数需要给出参数值,不能为空。
functions4
需要定义函数的返回值。
functions5
返回值要么加上 return 关键字,要么就被加分号。否则的话类型是 ()
。
if1
实现一个比大小函数。
1
2
3
4
5
6
|
if a > b {
a
}
else {
b
}
|
我这代码写得磕磕绊绊(笑
if2
第一个报错的原因是返回值不一样,接下来实现 test 的函数即可。
quiz1
一个简单的函数
1
2
3
4
5
6
7
|
fn calculate_apple_price (num: i32) -> i32 {
if num > 40 {
num
} else {
num * 2
}
}
|
move_semantics1
尽管 vec0 不是 mut 的,但是在 fill_vec 函数中控制权被转移了。返回的值是 mut 的,因此 vec1 需要是 mut 的。
Move_semantics2
该问题出现在 vec0 的所有权被转移到 vec1 了。hint 给了三种方法来修复:
- Make another, separate version of the data that’s in
vec0
and pass that to fill_vec
instead. 意思是说将 vec0 传给别的值(比如 vec2)再传给 fill_vec
函数。由于 Vec 没有实现 Copy trait,因此直接 let vec2=vec0
的话仍然会导致所有权的转移。想要实现完整的深复制,需要:
1
2
|
let vec2 = (&vec0).to_vec();
let mut vec1 = fill_vec(vec2);
|
``
- Make
fill_vec
borrow its argument instead of taking ownership of it, and then copy the data within the function in order to return an owned Vec<i32>
. 意思时说让 fill_vec
函数借用这个参数而不是改变所有权,在这个函数中进行复制并返回带有所有权的 Vec<i32>
,做如下修改:
1
2
3
4
5
|
let mut vec1 = fill_vec(&vec0);
......
fn fill_vec(vec: &Vec<i32>) -> Vec<i32> {
let mut vec = vec.to_vec();
|
``
- Make
fill_vec
mutably borrow its argument (which will need to be mutable), modify it directly, then not return anything. Then you can get rid of vec1
entirely – note that this will change what gets printed by the first println!
. 意思是说让 fill_vec
函数 mutably 借用这个参数,但是这样的话 vec0 也需要是 mutable 的。
改的地方相比 2 来说并不多,但是为没理解,这是用在什么时候的 feature 呢
1
2
3
4
5
6
7
8
|
let mut vec0 = Vec::new();
let mut vec1 = fill_vec(&mut vec0);
......
fn fill_vec(vec: &mut Vec<i32>) -> Vec<i32> {
let mut vec = vec.to_vec();
|
``
move_semantics3
要求是不添加新行,在已有的行上修改。报错主要是参数不是 mutable 的。
这里只用了 vec1,我们不需要管 vec0 的所有权
还是没搞懂,看一下 hint,说的是
这个和 [[#move_semantics2]] 的区别是 fill_vec
函数的第一行,上一个的第一行是一个转换:
` rust let mut vec = vec. to_vec ();
它将控制权转移并且使其可变。 而这道题目是没有的。我们可以把这一行加回来,也可以给某个位置加上
mut` 关键字,修改当前存在的一个绑定,使其变成 mutable 的绑定。
那么加在哪里呢
忽然发现自己犯蠢了,直接加在函数参数里就可以了:
1
|
fn fill_vec(mut vec: Vec<i32>) -> Vec<i32> {
|
之前的参数是 immutable 的,我们可以加上 mut 关键字(而不必借用)。
move_semantics4
它直接把 fill_vec
函数的参数去掉了,还说:fill_vec()
no longer takes vec: Vec<i32>
as argument。有点没看懂,先看一下注释
重构 (Refactor) 这段代码,我们不再使用 vec0
并在 fn main
中创建向量,而是在 fn fill_vec
中创建它,并将新创建的向量从 fill_vec 传输给它的调用者。执行 rustlings hint move_semantics4
以获得提示!
咳咳,还是没看懂,看看 hint 吧
只要你觉得你有足够的方向,就停止阅读:) 或者尝试做一个步骤,然后修复导致的编译器错误!
只做一步?难道是把某个变量变成全局变量吗
哦。。行吧,我们在 fill_vec()
中使用 Vec::new()
创建 vec,然后删去 vec0
即可。
move_semantics5
1
2
3
4
5
6
|
let mut x = 100;
let y = &mut x;
let z = &mut x;
*y += 100;
*z += 1000;
assert_eq!(x, 1200);
|
参考 Mutable References,这里的问题和它类似,但是要求我们仅通过换行使编译通过。这里其实用到了 Rust 的性质,我们把 z 借用这一行和 *y
赋值的行交换即可。因为给 y 赋值之后不会再使用 y 了,此时就可以重新 mutable 借用 x 了。
move_semantics6
本题要求只改变引用,不修改其他值。报错原因是 get_char
函数改变的 data 的所有权,导致下面调用 string_uppercase
函数时出错。那么怎样才能在调用 get_char
时不改变 data 的所有权呢?借用。要做的事情很简单。我们首先看一下原始代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
fn main() {
let data = "Rust is great!".to_string();
get_char(data);
string_uppercase(&data);
}
// Should not take ownership
fn get_char(data: String) -> char {
data.chars().last().unwrap()
}
// Should take ownership
fn string_uppercase(mut data: &String) {
data = &data.to_uppercase();
println!("{}", data);
}
|
我们要做的其实是和注释中说的一致:get_char
不应当获取所有权,我们将其修改为借用的版本,而 string_uppercase
需要获得所有权,我们将其改成直接传入的版本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
fn main() {
let data = "Rust is great!".to_string();
get_char(&data);
string_uppercase(data);
}
// Should not take ownership
fn get_char(data: &String) -> char {
data.chars().last().unwrap()
}
// Should take ownership
fn string_uppercase(mut data: String) {
data = data.to_uppercase();
println!("{}", data);
}
|
为什么 string_uppercase
需要获取所有权而不是借用呢?如果我们借用的话会怎样呢?恢复一下:
1
2
3
4
5
6
7
8
9
|
string_uppercase(&data);
}
...
// Should take ownership
fn string_uppercase(mut data: &String) {
data = &data.to_uppercase();
println!("{}", data);
}
|
报错:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
error[E0716]: temporary value dropped while borrowed
--> exercises/move_semantics/move_semantics6.rs:22:13
|
21 | fn string_uppercase(mut data: &String) {
| - let's call the lifetime of this reference `'1`
22 | data = &data.to_uppercase();
| --------^^^^^^^^^^^^^^^^^^^- temporary value is freed at the end of this statement
| | |
| | creates a temporary which is freed while still in use
| assignment requires that borrow lasts for `'1`
error: aborting due to previous error
For more information about this error, try `rustc --explain E0716`.
|
“A temporary value is being dropped while a borrow is still in active use”,意思是说借用仍然在使用中时删除了临时值。在这里,data.to_uppercase()
在执行完成之后就会删除,而我们却要借用它 &data.to_uppercase()
,自然会报错。那么我们不借用呢?也就是改成:
1
2
3
4
5
6
7
8
9
|
string_uppercase(&data);
}
...
// Should take ownership
fn string_uppercase(mut data: &String) {
data = data.to_uppercase();
println!("{}", data);
}
|
新报错为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
error[E0308]: mismatched types
--> exercises/move_semantics/move_semantics6.rs:22:12
|
21 | fn string_uppercase(mut data: &String) {
| ------- expected due to this parameter type
22 | data = data.to_uppercase();
| ^^^^^^^^^^^^^^^^^^^
| |
| expected `&String`, found struct `String`
| help: consider borrowing here: `&data.to_uppercase()`
error: aborting due to previous error
For more information about this error, try `rustc --explain E0308`.
|
因为这段代码
1
|
data = &data.to_uppercase();
|
中,data 的类型是 &String
,我们必须通过引用保持正确的参数。如果我们修改成
1
|
let data = &data.to_uppercase();
|
或
1
|
let data = data.to_uppercase();
|
都可以通过编译。如果我们打印一下此时 data 的类型:
1
2
3
4
5
6
7
8
9
10
11
|
fn print_type_of<T>(_: &T) {
println!("{}", std::any::type_name::<T>())
}
// Should take ownership
fn string_uppercase(mut data: &String) {
print_type_of(&data);
let data = data.to_uppercase();
print_type_of(&data);
println!("{}", data);
}
|
会发现 let 前后 data 的类型分别为
1
2
|
&alloc::string::String
alloc::string::String
|
而借用版本的则为
1
2
3
4
5
6
7
|
fn string_uppercase(mut data: &String) {
print_type_of(&data);
let data = &data.to_uppercase();
print_type_of(&data);
...
// &alloc::string::String
// &alloc::string::String
|
这个其实有点意思,为什么和上面的不大一样,是因为如果没有 let 的话,data.to_uppercase()
的生命周期到重新赋值给 data 为止。而 let 之后它的生命周期独立,到函数结束为止,因此能够正常编译。
primitive_types1
补充变量即可,这里学到的是 Bool
类型
primitive_types2
同上,这里学到的是 Char
类型,且对于 char,有几个很方便判断的函数:
会判断是否在字母表中
会判断是否在数字中
primitive_types3
这里是创建数组的方法,我们可以通过
创建长度为 100 的字符串数组
primitive_types4
这道题会学习切片。
1
|
let nice_slice = &a[1..4];
|
会对 a 切片,从 1 开始,到 3 截止,共 3 个元素。
primitive_types5
这道题会学习元组。
1
2
3
4
5
6
|
fn main() {
let cat = ("Furry McFurson", 3.5);
let (name, age) = cat;
println!("{} is {} years old.", name, age);
}
|
这个用法很符合直觉。
primitive_types6
这道题学习了元组中元素的 index
1
2
3
4
5
6
7
8
|
fn indexing_tuple() {
let numbers = (1, 2, 3);
// Replace below ??? with the tuple indexing syntax.
let second = numbers.1;
assert_eq!(2, second,
"This is not the 2nd number in the tuple!")
}
|
嗯,用法很新颖。
structs1
学习结构体的构造和使用。有传统的类似 C 的构造和元组方式的构造,还有一种被称为 Unit-Like
结构体,它可以没有任何结构,我们可以直接调用。
structs2
这里学习使用 update 语法。相对于 order_template
,只有 name 和 count 字段有变化,因此可以简写为
1
2
3
4
5
|
let your_order = Order {
name: String::from("Hacker in Rust"),
count: 1,
..order_template
};
|
structs3
学习使用 panic!
宏,以及一些简单的实现
enums1
学习 enum 的定义
enums2
学习 enums 中不同变量的定义,参考 Enum Values。
enums3
学习 match 的用法,参考 The match
Control Flow Construct
1
2
3
4
5
6
7
8
9
|
fn process(&mut self, message: Message) {
// TODO: create a match expression to process the different message variants
match message {
Message::ChangeColor(color) => self.change_color(color),
Message::Echo(s) => self.echo(s),
Message::Move(p) => self.move_position(p),
Message::Quit => self.quit()
}
}
|
这个参数的写法为之前还真没注意。
modules1
Rust 默认所有的结构体中的函数/模块都是 private 的,我们可以通过 pub 关键字使其 public,参考 Making Structs and Enums Public。
modules2
这道题目目的是学会 use ... as ...
的使用,默认也是 private 的,我们可以添加 pub
关键字使其 public。
1
2
3
|
// TODO: Fix these use statements
pub use self::fruits::PEAR as fruit;
pub use self::veggies::CUCUMBER as veggie;
|
modules3
注释告诉我们,可以用 use
关键字从任何地方导入 modules,尤其是 Rust 标准库到我们项目的区域(scope)中,这里要求我们导入 SystemTime
和 UNIX_EPOCH
。查询文档 Constant std::time::UNIX_EPOCH 可以直接找到导入方法:
1
|
use std::time::{SystemTime, UNIX_EPOCH};
|
我没看懂为什么 Err 的话报错 "SystemTime before UNIX EPOCH!"
。
vec1
要求我们用 vec!
宏初始化数组
1
|
let v = vec![10, 20, 30, 40]
|
看了一下 hint,第二种方法是先 Vec::new()
创建一个 vector 然后 push 填充。肯定是比宏麻烦的,宏的实现是否就是第二种方法的简写呢? #todo 看一下 vec!
宏的实现。
vec2
看一下 test 都干了啥:
首先定义 v:
1
|
let v: Vec<i32> = (1..).filter(|x| x % 2 == 0).take(5).collect();
|
这代码写得。。根本看不懂。简单解析一下应该是从 1 开始取数字,需要满足要求(filter)x 能整除 2,然后取(take)5 个,并 collect 成数组。有关 collect 的用法见 method.collect。里面的介绍是将迭代器转换为 collection。这道题实际上是要求改变数组的值。对于上面的循环:
1
2
3
4
|
for i in v.iter_mut() {
// TODO: Fill this up so that each element in the Vec `v` is
// multiplied by 2.
}
|
i 指向的是 v 的可变引用(iter_mut()
会返回 &mut v
),我们可以使用该迭代器修改当前的值。
1
2
3
4
5
|
for i in v.iter_mut() {
// TODO: Fill this up so that each element in the Vec `v` is
// multiplied by 2.
*i = *i*2;
}
|
hashmap1
学习 Rust 中哈希表的使用。我们需要
1
2
3
|
use std::collections::HashMap; // 引入
let mut basket = HashMap::new(); // 初始化
basket.insert(String::from("banana"), 2); // 插入
|
而对于它的键计数可以通过
对于它的值求和可以通过
1
|
basket.values().sum::<u32>()
|
hashmap2
这道题实际上是考察了 match 的用法,因为我们只需要匹配 Banana 和 Pineapple,因此其他的都不需要 match,可以用 _ => None
来放弃匹配。
1
2
3
4
5
6
7
8
9
10
|
for fruit in fruit_kinds {
// TODO: Put new fruits if not already present. Note that you
// are not allowed to put any type of fruit that's already
// present!
match fruit {
Fruit::Banana => basket.insert(Fruit::Banana, 6),
Fruit::Pineapple => basket.insert(Fruit::Pineapple, 7),
_ => None,
};
}
|
strings1
在不改变函数签名的条件下编译通过,编译错误报告给出了很明显的提示,返回值用 to_string()
转换即可。看了一下 hint,解释是我们返回的字符串生命周期和程序一样长,但是它是 &str
,我们需要的是 String
。我们还可以通过 String::from
创建字符串。
strings2
这里考察了 String 转换为 &str
(string slice)的方法,很简 zhe 单。
quiz2
这道题要求我们判断字符串的类型,要么是 String,要么是 &str
。
一些我不了解的函数:
errors1
Rust 可以通过 Option
结构描述错误信息
报错希望 Option<String>
,但是实际上是 Result
。
看一下 hint。
OK
和 Err
都是 Result
的变体。因此需要我们修改 generate_text
的返回值为 Result
而不是 Option
。
为了完成修改,我们需要
- 更新函数签名的返回值类型为
Result<String, String>
,使得返回值可以为 OK(String)
或 Err(String)
。
- 修改函数体以返回相应的值
即可。
errors2
看一下注释:
一个小游戏,目前它的问题是:
完全没有处理错误(也没有处理胜利的情况)
我们需要做的是:
如果我们调用 parse
函数时参数字符串不是数字,就返回 ParseIntError
,在这种情况下,我们立即从函数中返回错误并不会尝试乘或加
有两种实现方式,但是其中一种更短。
参考 Early returns,我们只需要 match 一下是否成功,在该返回的时候返回就行了:
1
2
3
4
|
let qty = match qty {
Ok(qty) => qty,
Err(e) => return Err(e)
};
|
如果不想针对错误进行匹配,就上上面那样的话,还有一种更简单的方法:参考 Introducing ?
。只需要加一个 ?
符号就行。
1
|
let qty = item_quantity.parse::<i32>()?;
|
errors3
报错
1
|
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
|
参考 Where The ?
Operator Can Be Used,main 函数的返回值是 ()
,而 ?
需求返回值是 Result
或 Option
。我们可以修改函数返回值,但是对于 main 而言显然是有限制的。所幸 Rust 的实现中,main 可以返回 Result<(), E>
。我们可以修改 main 的返回值并在最后加上 Ok(())
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
fn main() -> Result<(), ParseIntError> {
let mut tokens = 100;
let pretend_user_input = "8";
let cost = total_cost(pretend_user_input)?;
if cost > tokens {
println!("You can't afford that many!");
} else {
tokens -= cost;
println!("You now have {} tokens.", tokens);
}
Ok(())
}
|
errors4
有点没看懂要干啥。。看看 hint:
PositiveNonzeroInteger::new
总会创建一个新的实例(instance)并返回结果 Ok
,它应当做一些检查,在检查失败时返回 Err
,仅当检查确定一切正常时返回 Ok
。
所以我们在 new 函数中实现检查,小于 0 是报错负数,等于 0 时报错 0。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
impl PositiveNonzeroInteger {
fn new(value: i64) -> Result<PositiveNonzeroInteger, CreationError> {
if value < 0 {
Err(CreationError::Negative)
} else if value == 0 {
Err(CreationError::Zero)
}
else {
Ok(PositiveNonzeroInteger(value as u64))
}
}
}
|
除了 if 外,参考 [[#errors5]] 也可以使用 match 的版本:
1
2
3
4
5
6
7
8
9
|
impl PositiveNonzeroInteger {
fn new(value: i64) -> Result<PositiveNonzeroInteger, CreationError> {
match value {
x if x < 0 => Err(CreationError::Negative),
x if x == 0 => Err(CreationError::Zero),
_ => Ok(PositiveNonzeroInteger(value as u64))
}
}
}
|
errors5
看 TODO 要求我们更新 main 的返回值就能使得这段代码能通过编译了。
看一下报错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
error[E0277]: `?` couldn't convert the error to `ParseIntError`
--> exercises/error_handling/errors5.rs:17:59
|
14 | fn main() -> Result<(), ParseIntError> {
| ------------------------- expected `ParseIntError` because of this
...
17 | println!("output={:?}", PositiveNonzeroInteger::new(x)?);
| ^ the trait `From<CreationError>` is not implemented for `ParseIntError`
|
= note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
= note: required because of the requirements on the impl of `FromResidual<Result<Infallible, CreationError>>` for `Result<(), ParseIntError>`
error: aborting due to previous error
For more information about this error, try `rustc --explain E0277`.
|
main 的返回值是 Result<(), ParseIntError>
,而 PositiveNonzeroInteger::new(x)
的返回值是 Result<PositiveNonzeroInteger, CreationError>
。我们怎么能让 main 的返回值包含它们呢?看看 hint:
[!hint]
main 中会产生两种错误类型(ParseIntError
和 CreationError
),它们都通过 ?
传递。我们怎样在 main
中声明同时允许两种错误返回的情况呢?
还是参考 Where The ?
Operator Can Be Used,在底层实现中,?
对错误值调用 From::from
将其转换成一个装箱(boxed)的 trait 对象,也就是 Box<dyn error::Error>
,它是多态的,意味着很多不同类型的错误可以从同一个函数返回。所有的错误行为都是一样的,因为它们都实现了 error::Error
trait(特征)。
这样我们可以通过修改返回值简单地实现:
1
|
fn main() -> Result<(), Box<dyn error::Error>>
|
更多细节可以看 Using Trait Objects That Allow for Values of Different Types,我觉得以后还会遇到,到时候再回顾吧。
errors6
首先慢慢地看注释:
使用类似 Box<dyn error::Error>
捕获所有错误类型并不推荐用于库文件代码,因为调用者(caller)可以希望根据错误类型做决定,而不是打印或进一步传播(propagate)。这里,我们定义一个通用的(custom)错误类型,让 caller 决定当我们的函数发生错误时干什么。
接下来看 Don't change anything below this line.
下面的内容
首先是一些基本定义,和前几道题目类似。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
#[derive(PartialEq, Debug)]
struct PositiveNonzeroInteger(u64);
#[derive(PartialEq, Debug)]
enum CreationError {
Negative,
Zero,
}
impl PositiveNonzeroInteger {
fn new(value: i64) -> Result<PositiveNonzeroInteger, CreationError> {
match value {
x if x < 0 => Err(CreationError::Negative),
x if x == 0 => Err(CreationError::Zero),
x => Ok(PositiveNonzeroInteger(x as u64))
}
}
}
|
看一看报错:
1
2
3
4
5
6
7
8
9
10
11
12
|
error[E0599]: no variant or associated item named `from_creation` found for enum `ParsePosNonzeroError` in the current scope
--> exercises/error_handling/errors6.rs:33:40
|
17 | enum ParsePosNonzeroError {
| ------------------------- variant or associated item `from_creation` not found here
...
33 | .map_err(ParsePosNonzeroError::from_creation)
| ^^^^^^^^^^^^^ variant or associated item not found in `ParsePosNonzeroError`
error: aborting due to previous error
For more information about this error, try `rustc --explain E0599`.
|
意思是说在当前范围(scope)内没有为 enum ParsePosNonzeroError
找到名为 from_creation
的变体(variant)或关联项(associated item)。那我们首先需要实现它。在实现之前,我们知道 PositiveNonzeroInteger::new(x)
会返回 Result<PositiveNonzeroInteger, CreationError>
,而 map_err
,参考 map_err,其中的参数是一个函数,因此我们需要实现 from_creation
函数。
仔细研读一下 map_error
的源码和文档:
1
2
3
4
5
6
7
8
9
10
11
12
|
#[inline]
#[stable(feature = "rust1", since = "1.0.0")]
pub fn map_err<
F,
O: FnOnce(E) -> F
>(self, op: O) -> Result<T, F>
{
match self {
Ok(t) => Ok(t),
Err(e) => Err(op(e)),
}
}
|
在正确的时候直接传出不论,在错误的时候会调用参数,也就是函数 op
,在这里就是 from_creation
,用它来处理 error。看一下 error 的参数是 CreationError
,那么 from_creation
的参数就是 fn from_creation(e: CreationError)
。
接下来通过 test 看返回值,以其中第二个 test 为例
1
2
3
4
5
6
7
|
#[test]
fn test_negative() {
assert_eq!(
parse_pos_nonzero("-555"),
Err(ParsePosNonzeroError::Creation(CreationError::Negative))
);
}
|
Err
内部是 ParsePosNonzeroError::Creation(CreationError::Negative)
,也就是 ParsePosNonzeroError
。综上,函数定义为
1
|
fn from_creation(e: CreationError) -> ParsePosNonzeroError
|
这样我们就能实现好函数了:
1
2
3
4
5
6
7
8
9
|
impl ParsePosNonzeroError {
// TODO: add another error conversion function here.
fn from_creation(e: CreationError) -> ParsePosNonzeroError {
match e {
CreationError::Negative => ParsePosNonzeroError::Creation(CreationError::Negative),
CreationError::Zero => ParsePosNonzeroError::Creation(CreationError::Zero),
}
}
}
|
可以通过三个样例,没有通过的是
1
2
3
4
5
6
7
8
|
#[test]
fn test_parse_error() {
// We can't construct a ParseIntError, so we have to pattern match.
assert!(matches!(
parse_pos_nonzero("not a number"),
Err(ParsePosNonzeroError::ParseInt(_))
));
}
|
我们可以在 x 执行完 parse
之后 match 一下。
首先写一个符合为自己直觉的版本:
1
2
3
4
5
|
let x: i64 = s.parse().unwrap();
let x = match x {
Ok(x) => x,
Err(e) => return Err(ParsePosNonzeroError::ParseInt(e))
};
|
然而报错:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
error[E0308]: mismatched types
--> exercises/error_handling/errors6.rs:39:9
|
38 | let x = match x {
| - this expression has type `i64`
39 | Ok(x) => x,
| ^^^^^ expected `i64`, found enum `Result`
|
= note: expected type `i64`
found enum `Result<_, _>`
error[E0308]: mismatched types
--> exercises/error_handling/errors6.rs:40:9
|
38 | let x = match x {
| - this expression has type `i64`
39 | Ok(x) => x,
40 | Err(e) => return Err(ParsePosNonzeroError::ParseInt(e))
| ^^^^^^ expected `i64`, found enum `Result`
|
= note: expected type `i64`
found enum `Result<_, _>`
error: aborting due to 2 previous errors
For more information about this error, try `rustc --explain E0308`.
|
参考 [[#errors2]] 的代码,魔改了一番:
1
2
3
4
5
|
let x = s.parse::<i64>();
let x = match x {
Ok(x) => x,
Err(e) => return Err(ParsePosNonzeroError::ParseInt(e))
};
|
在这种情况下,编译通过了。但是这显然不符合我们的需求:对比一下之前的 x:
1
|
let x: i64 = s.parse().unwrap();
|
首先看一下 parse 的文档,parse
的定义是
1
|
pub fn parse<F>(&self) -> Result<F, <F as FromStr>::Err>
|
它还有一种可以自行推断类型的语法 ::<type>
,这么说我们用 let x: i64 = s.parse()
和 let x = s.parse::<i64>()
应该是相同的。那为什么上面还会报错呢?
看一下 hint 吧。。
在 TODO
要求更改的行的下方,有使用 Result
上的 map_err()
方法将一种类型的错误转换为另一种类型的示例,尝试在 parse()
的 Result
上使用类似的东西。可以使用 ?
运算符从函数中提前返回,或者使用 match
表达式,或是其他方法。
哦。。我又实现了一个 from_parse
函数:
1
2
3
4
5
|
fn from_parse(e: ParseIntError) -> ParsePosNonzeroError {
match e {
e => return ParsePosNonzeroError::ParseInt(e)
}
}
|
之后的调用如下:
1
2
3
|
let x: i64 = s.parse()
.map_err(ParsePosNonzeroError::from_parse)
.unwrap();
|
此时可以编译通过,但是还是无法完成 test::test_parse_error
测试:因为我们报错后没有直接返回,还是传入到了 unwrap()
函数中。
那我们在 map_err
后面加个 ?
试试?报错了。。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
error[E0282]: type annotations needed
--> exercises/error_handling/errors6.rs:42:20
|
42 | let x: i64 = s.parse()
| ^^^^^ cannot infer type
|
= note: type must be known at this point
help: consider specifying the type argument in the method call
|
42 | let x: i64 = s.parse::<F>()
| +++++
error: aborting due to previous error
For more information about this error, try `rustc --explain E0282`.
|
告诉我们 parse 无法推断类型。它认为我们可以添加 ::<F>
这个语法糖。那我们试一下,添加语法糖后:
1
2
3
4
5
6
7
8
9
|
error[E0599]: no method named `unwrap` found for type `i64` in the current scope
--> exercises/error_handling/errors6.rs:44:10
|
44 | .unwrap();
| ^^^^^^ method not found in `i64`
error: aborting due to previous error
For more information about this error, try `rustc --explain E0599`.
|
告诉我们对于 i64
没有 unwrap
方法。看一下 unwrap
的定义,发现自己犯蠢了:
错误情况可以通过 match
显式处理,也可以通过 unwrap
隐式处理。隐式处理会返回内部元素或 panic。
因此我们不必在后面使用 unwrap
方法了。
最终的代码为
1
2
|
let x: i64 = s.parse::<i64>()
.map_err(ParsePosNonzeroError::from_parse)?;
|
或者上面的
1
2
3
4
5
|
let x = s.parse::<i64>();
let x = match x {
Ok(x) => x,
Err(e) => return Err(ParsePosNonzeroError::ParseInt(e))
};
|
都是正确的。
generics1
首先是 Vec 的类型为 String,那么下面的字符串就需要 .to_string()
,还可以设置 Vec
的类型为 &str
,就不需要修改下面的 push 了。
generics2
看一下注释:
强大的 wrapper 提供了存储正整数的能力,将它重写以支持 wrapping 任意值
嗯,还是没搞懂咋写,看看 hint 吧。
目前我们只能 wrap
u32,也许我们可以以某种方式更新对该数据类型的显式引用?
如果还卡住的话,参考 https://doc.rust-lang.org/stable/book/ch10-01-syntax.html#in-method-definitions。
哦,是 Rust 中的泛型。原来的代码是
1
2
3
4
5
6
7
8
9
|
struct Wrapper {
value: u32,
}
impl Wrapper {
pub fn new(value: u32) -> Self {
Wrapper { value }
}
}
|
修改后的代码是:
1
2
3
4
5
6
7
8
9
|
struct Wrapper<T> {
value: T,
}
impl<T> Wrapper<T> {
pub fn new(value: T) -> Self {
Wrapper { value }
}
}
|
感觉和 C++ 差不多?
generics3
首先简单地参考 [[#generics2]] 实现一下泛型:
1
2
3
4
5
6
7
8
9
10
11
12
|
pub struct ReportCard<T> {
pub grade: T,
pub student_name: String,
pub student_age: u8,
}
impl<T> ReportCard<T> {
pub fn print(&self) -> String {
format!("{} ({}) - achieved a grade of {}",
&self.student_name, &self.student_age, &self.grade)
}
}
|
接下来报错:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
error[E0277]: `T` doesn't implement `std::fmt::Display`
--> exercises/generics/generics3.rs:24:52
|
24 | &self.student_name, &self.student_age, &self.grade)
| ^^^^^^^^^^^ `T` cannot be formatted with the default formatter
|
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
= note: this error originates in the macro `$crate::__export::format_args` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider restricting type parameter `T`
|
21 | impl<T: std::fmt::Display> ReportCard<T> {
| +++++++++++++++++++
error: aborting due to previous error
For more information about this error, try `rustc --explain E0277`.
|
看一下 rustc --explain E0277
解释是这样的:
使用未实现某些 trait 的类型时会报这个错误。
假设有下面的一段代码:
`` ` rust
// here we declare the Foo trait with a bar method
trait Foo {
fn bar (&self);
}
// we now declare a function which takes an object implementing the Foo trait
fn some_func <T: Foo> (foo: T) {
foo. bar ();
}
fn main () {
// we now call the method with the i32 type, which doesn’t implement
// the Foo trait
some_func (5i32); // error: the trait bound i32 : Foo
is not satisfied
}
`` `
由于我们没有对 Foo trait 实现 i32 type,因此会报错
之后给出了为 Foo trait 实现 i32 type 的示例。
看上去有点麻烦啊,或者有没有什么更优雅的实现呢?看一下 hint 吧。
为了找到解决这个挑战的最好办法,你需要回想关于 trait 的知识,尤其是 Trait Bound Syntax,你也可能需要 use std::fmt::Display;
。
你不仅需要让 ReportCard
结构体更泛用,还需要正确实现——你也需要轻轻地修改结构体的实现。
那么接着让我们回顾一下错误。看一下 std::fmt::Display
的文档,它是一个 trait。很明显代码的输出不可能满足所有可能的类型。我们需要参考它的 Example 为 Display 实现相应类型。参考 Fixing the largest
Function with Trait Bounds 和上面的报错,我们在 impl
中添加约束即可:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
use std::fmt::Display;
pub struct ReportCard<T> {
pub grade: T,
pub student_name: String,
pub student_age: u8,
}
impl<T: Display> ReportCard<T> {
pub fn print(&self) -> String {
format!("{} ({}) - achieved a grade of {}",
&self.student_name, &self.student_age, &self.grade)
}
}
|
option1
看一下注释,我们可以修改任何位置,除了 print_number
的函数签名。该函数定义如下:
1
2
3
4
|
// you can modify anything EXCEPT for this function's signature
fn print_number(maybe_number: Option<u16>) {
println!("printing: {}", maybe_number.unwrap());
}
|
第一处错误是类型不匹配,给数字添加 Some
得过。
第二处也是一样的,但是添加了 Some
之后报了新错:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
warning: variable does not need to be mutable
--> exercises/option/option1.rs:15:9
|
15 | let mut numbers: [Option<u16>; 5];
| ----^^^^^^^
| |
| help: remove this `mut`
|
= note: `#[warn(unused_mut)]` on by default
error[E0381]: use of possibly-uninitialized variable: `numbers`
--> exercises/option/option1.rs:21:9
|
21 | numbers[iter as usize] = Some(number_to_add);
| ^^^^^^^^^^^^^^^^^^^^^^ use of possibly-uninitialized `numbers`
error: aborting due to previous error; 1 warning emitted
For more information about this error, try `rustc --explain E0381`.
|
为什么反而认为 numbers
没有被初始化了呢?
首先,Option<u16>
会创建:
1
2
3
4
|
enum Option_u16 {
Some(u16),
None,
}
|
而初始化数组的操作 let mut numbers: [Option<u16>; 5];
是否意味着值不确定而没有被初始化呢?看了一下 hint,确实如此
[!hint]
数组的值没有合理的默认值;使用前需要填写值。
option2
第一处参考 Concise Control Flow with if let
,学习了 if let
表达式替代 match
的用法。
第二处参考 while let,注意到 vector
的 pop
会加一层 Option<T>
,因此用了 Some(Some(integer))
。
option3
问题出现在所有权转移给了 p,之后再调用 y 就会出错了。
那么 match 可不可以借用呢?当然可以。我们有两种改法:
1
2
3
4
5
6
7
8
9
|
fn main() {
let y: &Option<Point> = &Some(Point { x: 100, y: 200 });
match y {
Some(p) => println!("Co-ordinates are {},{} ", p.x, p.y),
_ => println!("no match"),
}
y; // Fix without deleting this line.
}
|
和
1
2
3
4
5
6
7
8
9
|
fn main() {
let y: Option<Point> = Some(Point { x: 100, y: 200 });
match &y {
Some(p) => println!("Co-ordinates are {},{} ", p.x, p.y),
_ => println!("no match"),
}
y; // Fix without deleting this line.
}
|
第一种改法让 y 的类型变成 &Option<Point>
,我们创建的 Some(Point { x: 100, y: 200 })
的生命周期和 main 函数一致,借用自然也可以被借用。
第二种改法让我们匹配 y 的时候借用,借用自然不会改变 y 的所有权。
看了一下 hint,这其实是考察 ref
关键字,不过自从 Rust 2018 开始我们就可以使用 &
来引用了,用 &
的写法还是很符合直觉的。第三种改法如下所示:
1
2
3
4
5
6
7
8
9
|
fn main() {
let y: Option<Point> = Some(Point { x: 100, y: 200 });
match y {
Some(ref p) => println!("Co-ordinates are {},{} ", p.x, p.y),
_ => println!("no match"),
}
y; // Fix without deleting this line.
}
|
另外 &
和 ref
还是有区别的:
&
表示 pattern 需要一个对对象的引用。因此,&
是上述 pattern 的一部分,&Foo
与 Foo
匹配的对象不同。
ref
表示想要引用一个未打包(unpacked)的值。它不被匹配(It is not matched against):Foo(ref foo)
和 Foo(foo)
匹配相同的对象。
traits1
参考 Implementing a Trait on a Type 实现即可
traits2
我们需要为 Vector<String>
实现 traint。
实现方法和 trait1 类似(感谢编译器帮了大忙)
1
2
3
4
5
6
|
impl AppendBar for Vec<String> {
fn append_bar(mut self) -> Self {
self.push(String::from("Bar"));
self
}
}
|
看一下 hint 自己有没有遗漏的地方
注意到 trait 获取了 self
的所有权,返回了 Self
。
tests1
学习 assert!()
宏的使用。
test2
学习 assert_eq!()
宏的使用。
test3
简单测试,我们可以在 assert()
宏中使用单目运算符 !
quiz3
自己编写 test 的小 quiz。
Box1
首先看注释:
在编译时,Rust 需要知道一个类型会占用多少空间。这对于递归类型(recursive type)是有问题的,递归类型是可以将另一个相同类型的值作为自身一部分的类型。为了解决这个问题,我们可以使用 Box
,它是一个用于堆上存储数据的智能指针,它也允许我们打包一个递归类型。
本实验值要实现的递归类型是 cons list
,它是函数式编程语言中的一种常见(frequently)数据结构。cons list
中的每一项有两个元素:当前项的值和下一个项的值。最后一项的值为 Nil
。
- 步骤一:在 enum 中使用
Box
让代码可以通过编译
- 步骤二:通过移除
unimplemented!()
创建空的和非空的 cons list
。
感谢报错,它提示了我们接下来应该怎么做:
1
2
3
4
5
6
7
8
9
10
11
12
|
error[E0072]: recursive type `List` has infinite size
--> exercises/standard_library_types/box1.rs:22:1
|
22 | pub enum List {
| ^^^^^^^^^^^^^ recursive type has infinite size
23 | Cons(i32, List),
| ---- recursive without indirection
|
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable
|
23 | Cons(i32, Box<List>),
| ++++ +
|
还可以参考 cons list 的更多内容
之后就是构造 empty_list
和 non_empty_list
了。对于 empty list 来说,直接返回 Nil 就行,但是对于 non_empty_list
来说,只有第一个值是显然的,第二个值需要用 Box new 一下。
arc1
[!tip]
在这个实验中,给定一个 u32 的 Vec
,称其 numbers,值从 0 到 99。
我们希望在 8 个不同的线程中同时(simultaneously)使用这组数字。
每一个线程会获得总和的 1/8,带有偏移:
- 对于第一个线程(offset 0),会对 0, 8, 16, … 求和
- 对于第二个线程(offset 1),会对 1, 9, 17, … 求和
- 对于第三个线程(offset 2),会对 2, 10, 18, … 求和
- …
- 对于第八个线程(offset 7),会对 7, 15, 23, … 求和
因为我们使用了线程,我们的值需要是线程安全的。因此,我们使用 Arc
。
通过填充第一个 TODO 所在的 shared_numbers
使代码通过编译,在第二个 TODO 所在的位置为 child_numbers
创建初始绑定。尽量不要创建 numbers
Vec 的任何复制。
有关 Arc 的知识,参考原子引用计数 Arc<T>
和 Struct std::sync::Arc。它的本质就是对Rc<T>
引用计数智能指针 的一种重构,使得它可以安全的用于并发环境。
写完之后来理一下主要逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
fn main() {
let numbers: Vec<_> = (0..100u32).collect();
let shared_numbers = Arc::new(numbers);// TODO
let mut joinhandles = Vec::new();
for offset in 0..8 {
let child_numbers = Arc::clone(&shared_numbers); // TODO
joinhandles.push(thread::spawn(move || {
let sum: u32 = child_numbers.iter().filter(|n| *n % 8 == offset).sum();
println!("Sum of offset {} is {}", offset, sum);
}));
}
for handle in joinhandles.into_iter() {
handle.join().unwrap();
}
}
|
shared_numbers
通过 Arc
创建引用计数的 numbers,在下面的循环中,child_numbers
每通过 Arc clone 一次,shared_numbers
的引用计数就会加一。
在计算 sum 时,filter 会获取满足 *n % 8 == offset
的数字并求和,最终输出。为了避免创建 numbers
的 copy,我们需要在 main 线程中创建 child_numbers
,而不是在子线程中。
iterators1
迭代器的基本知识。参考使用迭代器处理元素序列和 Trait std::iter::Iterator
我们可用 .iter()
生成迭代器并通过 .next()
使用,迭代器的结束是 None
。
iterators2
Step 1
对于 iters,如果我们消费了一个,那么再次使用的时候不会使用第一个了。参考下面这个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
#![allow(unused)]
pub fn capitalize_first(input: &str) {
let mut c = input.chars();
println!("{:?}", &c);
match c.next() {
None => String::new(),
Some(first) => {
println!("{:?}", &c);
}
};
}
fn main() {
let s = "hello";
capitalize_first(&s);
}
|
输出为
1
2
|
Chars(['h', 'e', 'l', 'l', 'o'])
Chars(['e', 'l', 'l', 'o'])
|
Step 2
参考 collect,比较简单。
Step 3
本来我是想参考 flat_map 的,但是有点没搞明白为什么怎么写都有问题。突发奇想按照 [[#Step 2]] 的写,居然成功通过了。。看一下 hint,对此的解释是:
[!hint]
它和之前的解答惊人地相似。Collect
非常强大和通用,Rust 仅需要知道希望的类型。
等全做完之后可用整理一下为什么 flat_map
不能通过吧。 #todo
iterators3
[!tip]
这个练习比之前所有的都要大,相信你可以完成它。
接下来是任务
- 完成除法功能以通过前四个测试
- 完成
result_with_list
和 list_of_results
函数以通过剩余的测试
除法功能咋用 match_error
实现啊,我用 if else 实现了。 #todo
接下来看 result_with_list
的实现。原始实现为
1
2
3
4
5
6
|
// Complete the function and return a value of the correct type so the test passes.
// Desired output: Ok([1, 11, 1426, 3])
fn result_with_list() -> () {
let numbers = vec![27, 297, 38502, 81];
let division_results = numbers.into_iter().map(|n| divide(n, 27));
}
|
测试为
1
2
3
4
|
#[test]
fn test_result_with_list() {
assert_eq!(format!("{:?}", result_with_list()), "Ok([1, 11, 1426, 3])");
}
|
分析一下 division_results
的值为 [1, 11, 1426, 3]
,我们希望返回的值为 Ok([1, 11, 1426, 3])
。
哦,强大的 collect
trait,两个函数通过简单的 collect 就都解决了。等有时间看一下 collect 到底是怎么实现的。
iterators4
要求我们实现一个阶乘(factorial)函数。不需要使用 return、循环和其他变量。Rust 提供了非常强的 API:
1
2
3
|
pub fn factorial(num: u64) -> u64 {
(1..num+1).product()
}
|
仅需要一行就能实现。product()
trait 实现了连乘的功能。
iterators5
看看注释:
[!hint]
让我们定义一个简单的模型以跟踪 Rustlings 练习的进程。这个进程会用哈希表建模,它的键是练习的名字,值是练习的进程。创建了两个计数函数以计算给定进程的练习数量。这些计数函数使用了至关重要的(imperative)循环风格。使用函数式的 iterators 重建这些计数。只需要修改 count_iterator
和 count_collection_iterator
这两个迭代器方法。
边看文档边做题:Trait std::iter::Iterator,文档还是很有用的。
第一个是遍历 map 的 iter 并对满足要求的值计数。我用 filter
看是否满足要求,用 count
计数。
1
2
3
4
5
|
fn count_iterator(map: &HashMap<String, Progress>, value: Progress) -> usize {
// map is a hashmap with String keys and Progress values.
// map = { "variables1": Complete, "from_str": None, ... }
map.values().filter(|val| *val == &value).count()
}
|
第二个的 map 夹在了 Vector
slice 里,遍历到还简单,用 iter()
+ map()
计算每个 iter
中的值,最后用 sum
计数。map 实现的闭包直接拿前一个填充了。
1
2
3
4
5
6
7
8
9
|
fn count_collection_iterator(collection: &[HashMap<String, Progress>], value: Progress) -> usize {
// collection is a slice of hashmaps.
// collection = [{ "variables1": Complete, "from_str": None, ... },
// { "variables2": Complete, ... }, ... ]
collection
.iter()
.map(|vals| vals.values().filter(|val| *val == &value).count())
.sum()
}
|
threads1
看看注释:
[!note]
这道题目的想法是在第 22 行产生(spawned)的线程完成作业,而主线程会监视作业直到完成 10 个作业。因为产生的线程的睡眠时间和等待线程睡眠时间的区别,你会看到 6 行 “waiting…” 且程序没有运行到超时就终止了。
唔,不管怎么样,先看一下程序的逻辑吧。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
fn main() {
let status = Arc::new(JobStatus { jobs_completed: 0 });
let status_shared = status.clone();
thread::spawn(move || {
for _ in 0..10 {
thread::sleep(Duration::from_millis(250));
status_shared.jobs_completed += 1;
}
});
while status.jobs_completed < 10 {
println!("waiting... ");
thread::sleep(Duration::from_millis(500));
}
}
|
status 是用 Arc 创建的,它支持原子操作。Spawn 参考 Function std:🧵:spawn,它会产生一个新线程,并为其返回一个 JoinHandle
。
看看它的原始实现
1
2
3
4
5
6
7
8
9
|
#[stable(feature = "rust1", since = "1.0.0")]
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
{
Builder::new().spawn(f).expect("failed to spawn thread")
}
|
spawn
函数的闭包和返回值都有约束:
-
'static
意味着闭包和返回值必须具有整个程序执行的生命周期,因为线程的生命周期可能超过它们被创建的生命周期。
如果线程及其返回值比它们的调用者存活时间更长,我们需要确保它们在之后也是有效的。由于我们不知道它们什么时候返回,因此我们需要让它尽可能长时间地有效,也就是直到程序结束,因此是 'static
的生命周期。
-
Send
约束是因为闭包需要从产生它的线程按值传递给新线程。它的返回值需要从新线程传递到它加入的线程。注意,Send
trait 表示从线程传递到线程是安全的。 Sync 表示在线程之间传递引用是安全的。
在该线程中会执行十次 sleep(250ms)
,每次会对某个计数器加一。最开始的错误也出现在这里:
1
2
3
4
5
6
7
8
9
10
11
|
error[E0594]: cannot assign to data in an `Arc`
--> exercises/threads/threads1.rs:25:13
|
25 | status_shared.jobs_completed += 1;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot assign
|
= help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `Arc<JobStatus>`
error: aborting due to previous error
For more information about this error, try `rustc --explain E0594`.
|
看看错误解释,以为是没有令 status_shared
mutable 的原因,但是添加 mut 之后也会报错。看一下 hint 吧。
第一处 hint:
[!hint]
Arc
是原子引用计数指针,允许对不可变数据进行安全的,共享的访问。但是我们希望修改 jobs_completed
的数量,因此我们需要使用一次仅允许在一个线程中改变值的类型。参考共享状态并发
哦,看懂了,我们在 Arc
里面包裹一个 Mutex
即可。这个章节的最后有一处总结:
[!info] RefCell<T>
/ Rc<T>
与 Mutex<T>
/ Arc<T>
的相似性
你可能注意到了,因为 counter
是不可变的,不过可以获取其内部值的可变引用;这意味着 Mutex<T>
提供了内部可变性,就像 Cell
系列类型那样。正如第十五章中使用 RefCell<T>
可以改变 Rc<T>
中的内容那样,同样的可以使用 Mutex<T>
来改变 Arc<T>
中的内容。
macros1
参考宏。这道练习是学习宏的基本用法,加个叹号就行。
macros2
这里考察了宏的性质:
[!caution]
宏和函数的最后一个重要的区别是:在一个文件里调用宏 之前 必须定义它,或将其引入作用域,而函数则可以在任何地方定义和调用。
macros3
这个练习考察了宏的作用域。可用参考 Macros By Example 中的 The macro_use
attribute 通过添加 macro_use
属性解决。它有两个用处:
- 首先,它可以用于使模块的宏范围在模块关闭时不会结束,方法是将其应用于模块:
- 其次,它可用于从另一个 crate 导入宏,方法是将其附加到 crate 根模块中出现的 extern crate 声明。
macros4
看上去缺个分号,补充上去果然编译通过了。主要考察的是宏的格式。
1
2
3
4
5
6
7
8
|
macro_rules! my_macro {
() => {
println!("Check out my macro!");
}
($val:expr) => {
println!("Look at this other macro: {}", $val);
}
}
|
宏的定义为
1
2
|
MacroRulesDefinition :
macro_rules ! IDENTIFIER MacroRulesDef
|
那么 MacroRulesDef
就是
1
2
3
4
5
6
7
8
|
{
() => {
println!("Check out my macro!");
}
($val:expr) => {
println!("Look at this other macro: {}", $val);
}
}
|
根据
1
2
3
4
|
MacroRulesDef :
( MacroRules ) ;
| [ MacroRules ] ;
| { MacroRules }
|
MacroRules
就是
1
2
3
4
5
6
|
() => {
println!("Check out my macro!");
}
($val:expr) => {
println!("Look at this other macro: {}", $val);
}
|
再往下分析
1
2
|
MacroRules :
MacroRule ( ; MacroRule )* ;?
|
因此要添加的符号是分号。
quiz4
编写一个宏即可通过。
clippy1
[!tip]
Clippy 工具是用于分析代码的 lint 的集合,因此你可以用它匹配常见的错误,提升代码质量。
对于这些练习,当存在 clippy warning 时就会报错。检查 clippy 输出的建议以完成练习。
对于本道练习的建议是
1
2
3
4
5
6
7
8
9
10
11
|
error: approximate value of `f32::consts::PI` found
--> clippy1.rs:14:14
|
14 | let pi = 3.14f32;
| ^^^^^^^
|
= note: `#[deny(clippy::approx_constant)]` on by default
= help: consider using the constant directly
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#approx_constant
error: could not compile `clippy1` due to previous error
|
找到了 f32::consts::PI
的近似值,参考 https://rust-lang.github.io/rust-clippy/master/index.html#approx_constant,当检查到近似于 std::f32::consts
或 std::f64::consts
的常量时就会报错。因为默认 #[deny(clippy::approx_constant)]
选项是开启的。我们可以用 f32::consts::PI
替代。
clippy2
在这里,clippy 认为对 Option
结构 for 循环没有用 if let
的可读性好。参考 https://rust-lang.github.io/rust-clippy/master/index.html#for_loops_over_fallibles,clippy 会检查是否对 Option
或 Result
值进行了循环(可能是因为没必要?)
using_as
[!tip]
在 Rust 中,as
操作符号被用来做类型转换。
请注意,as
不仅在类型转换中使用,它也被用来重命名 imports
的内容。
本练习的目的是保证除法实现不会编译错误。
我们直接让 usize
的值 as f64
即可。
from_into
[!tip]
From
方法被用于值到值的转换。如果 From
正确地实现给了一种类型,那么对应的 Into
方法应该会相反地工作。更多资料参考 https://doc.rust-lang.org/std/convert/trait.From.html
参考下面的注释,非常详细。可用实现这个 from trait。
首先实现了一个正常版本的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
if s.len() <= 0 {
Person::default()
} else {
let sp: Vec<_> = s.split(",").collect();
if sp.len() != 2 || sp[0].len() <= 0 {
Person::default()
} else {
let age = sp[1].parse::<usize>();
Person {
name: sp[0].to_string(),
age: match age {
Ok(age) => age,
_ => return Person::default(),
}
}
}
}
|
然后是稍微不正常版本的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
match s.len() {
x if x <= 0 => Person::default(),
_ => {
let sp: Vec<_> = s.split(",").collect();
if sp.len() != 2 || sp[0].len() <= 0 {
Person::default()
} else {
let age = sp[1].parse::<usize>();
Person {
name: sp[0].to_string(),
age: match age {
Ok(age) => age,
_ => return Person::default(),
},
}
}
}
}
|
最后是我写完之后觉得醍醐灌顶的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
match s.len() {
x if x <= 0 => Person::default(),
_ => {
match s.split(",").collect::<Vec<_>>() {
sp if sp.len() != 2 || sp[0].len() <= 0 => Person::default(),
sp => {
match sp[1].parse::<usize>() {
Ok(age) => Person {
name: sp[0].to_string(),
age: age,
},
_ => Person::default()
}
}
}
}
}
|
from_str
[!tip]
它和 [[#from_into]] 类似,但是在这里我们会实现 FromStr
并返回 errors
而不是返回一个默认值。另外,在实现 FromStr
时,你可以在 strings 上使用 parse
方法以生成实现者类型的对象。
不得不说 match 是真的好用。如果我们希望用其他方法的化,hint 也给了提示:
[!hint]
我们可以将 Result
的 map_err
方法与函数或闭包一起使用,以包装 parse::<usize>
的错误
#todo
[!hint]
如果希望用 ?
传播错误,可以参考 https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reenter_question_mark.html
#todo
try_from_into
[!tip]
TryFrom
是简单且安全的类型转换,它会在某些情况下以可控的方式失败。
在通常状况下,它和 From
是一致的。主要的区别是它会返回一个 Result
类型而不是目标类型
用一种非常鱼唇的方法解决了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
// Tuple implementation
impl TryFrom<(i16, i16, i16)> for Color {
type Error = IntoColorError;
fn try_from(tuple: (i16, i16, i16)) -> Result<Self, Self::Error> {
if tuple.0 >= 0
&& tuple.0 < 256
&& tuple.1 >= 0
&& tuple.1 < 256
&& tuple.2 >= 0
&& tuple.2 < 256
{
Ok(Self {
red: tuple.0 as u8,
green: tuple.1 as u8,
blue: tuple.2 as u8,
})
} else {
Err(IntoColorError::IntConversion)
}
}
}
// Array implementation
impl TryFrom<[i16; 3]> for Color {
type Error = IntoColorError;
fn try_from(arr: [i16; 3]) -> Result<Self, Self::Error> {
if arr[0] >= 0 && arr[0] < 256 && arr[1] >= 0 && arr[1] < 256 && arr[2] >= 0 && arr[2] < 256
{
Ok(Self {
red: arr[0] as u8,
green: arr[1] as u8,
blue: arr[2] as u8,
})
} else {
Err(IntoColorError::IntConversion)
}
}
}
// Slice implementation
impl TryFrom<&[i16]> for Color {
type Error = IntoColorError;
fn try_from(slice: &[i16]) -> Result<Self, Self::Error> {
if slice.len() != 3 {
Err(IntoColorError::BadLen)
} else if slice[0] >= 0
&& slice[0] < 256
&& slice[1] >= 0
&& slice[1] < 256
&& slice[2] >= 0
&& slice[2] < 256
{
Ok(Self {
red: slice[0] as u8,
green: slice[1] as u8,
blue: slice[2] as u8,
})
} else {
Err(IntoColorError::IntConversion)
}
}
}
|
看了一眼 hint,我这确实是比较麻烦的写法:
[!hint]
看官方文档的示例,https://doc.rust-lang.org/std/convert/trait.TryFrom.html
在 TryFrom
标准库中是否有一种实现,即可以做数字转换又能够检查输出的范围?
你可以用 Result
的 map_err
或 or
方法转换错误。
如果希望用 ?
传播错误,可以参考 https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reenter_question_mark.html
挑战:你是否可以让 TryFrom
在许多整数类型上通用?
#todo
as_ref_mut
[! tip]
AsRef
和 AsMut
允许廉价的引用到引用的转换。更多内容参考 https://doc.rust-lang.org/std/convert/trait.AsRef.html 和 https://doc.rust-lang.org/std/convert/trait.AsMut.html。
本练习是学习了 AsRef
的使用。正如参考内容的 examples 中写的那样:
[!cite]
通过使用特征边界(trait bounds),我们可以接受不同的可以被转换为指定的类型 T
的参数。
例如,通过创建一个接受 AsRef<str>
的通用参数,可以表示我们希望所有可以转换为 &str
作为参数的引用。由于 String
和 &str
都实现了 AsRef<str>
,因此可以接受这两者作为输入参数。
advanced_errs1
[!tip]
回顾一下 [[#errors6]],我们有多个映射函数,可以通过 map_err()
将低级的错误翻译成我们自定义的错误类型。那我们是不是可以直接用 ?
符号直接转换呢?
首先看一下错误。如果我们没有为 ?
实现 From
trait 的话,会报这样的错误:
1
2
3
4
5
6
7
8
|
error[E0277]: `?` couldn't convert the error to `ParsePosNonzeroError`
--> exercises/advanced_errors/advanced_errs1.rs:41:42
|
41 | Ok(PositiveNonzeroInteger::new(x)?)
| ^ the trait `From<CreationError>` is not implemented for `ParsePosNonzeroError`
|
= note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
= note: required because of the requirements on the impl of `FromResidual<Result<Infallible, CreationError>>` for `Result<PositiveNonzeroInteger, ParsePosNonzeroError>`
|
我们可以为 ParsePosNonzeroError
实现 From<CreationError>
。本练习的两道题目都是这样。
advanced_errs2
[!tip]
本练习演示了一些对实现自定义错误很有用的 trait,尤其是这种情况下其他代码可以有效地使用自定义错误类型。
再看一下 main
函数实现。
1
2
3
4
5
|
fn main() -> Result<(), Box<dyn Error>> {
println!("{:?}", "Hong Kong,1999,25.7".parse::<Climate>()?);
println!("{:?}", "".parse::<Climate>()?);
Ok(())
}
|
它将字符串通过 from_str
(在 [[#from_str]] 中有过介绍)转换成 Climate
类型,然后通过 ?
处理错误。这里只实现了一个相关的 From
trait:
1
2
3
4
5
6
7
|
// This `From` implementation allows the `?` operator to work on
// `ParseIntError` values.
impl From<ParseIntError> for ParseClimateError {
fn from(e: ParseIntError) -> Self {
Self::ParseInt(e)
}
}
|
这段代码为 ParseClimateError
实现了 From<ParseIntError>
。我们可以仿照这个例子实现下面的 From<ParseFloatError>
。
不过这和 main 函数无法通过编译无关,main 函数的报错为:
1
2
3
4
5
6
7
8
|
error[E0277]: the trait bound `ParseClimateError: std::error::Error` is not satisfied
--> exercises/advanced_errors/advanced_errs2.rs:111:62
|
111 | println!("{:?}", "Hong Kong,1999,25.7".parse::<Climate>()?);
| ^ the trait `std::error::Error` is not implemented for `ParseClimateError`
|
= note: required because of the requirements on the impl of `From<ParseClimateError>` for `Box<dyn std::error::Error>`
= note: required because of the requirements on the impl of `FromResidual<Result<Infallible, ParseClimateError>>` for `Result<(), Box<dyn std::error::Error>>`
|
意思是说我们需要为 ParseClimateError
实现 std::error::Error
。
参考 定义一个错误类型,我们可以实现一个 Error
trait,让其他错误可以包裹这个错误类型。
1
2
3
4
5
|
impl Error for ParseClimateError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
None
}
}
|
看看接下来的报错:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
error[E0004]: non-exhaustive patterns: `&Empty`, `&BadLen` and `&ParseInt(_)` not covered
--> exercises/advanced_errors/advanced_errs2.rs:70:15
|
29 | / enum ParseClimateError {
30 | | Empty,
| | ----- not covered
31 | | BadLen,
| | ------ not covered
32 | | NoCity,
33 | | ParseInt(ParseIntError),
| | -------- not covered
34 | | ParseFloat(ParseFloatError),
35 | | }
| |_- `ParseClimateError` defined here
...
70 | match self {
| ^^^^ patterns `&Empty`, `&BadLen` and `&ParseInt(_)` not covered
|
= help: ensure that all possible cases are being handled, possibly by adding wildcards or more match arms
= note: the matched value is of type `&ParseClimateError`
error: aborting due to previous error
For more information about this error, try `rustc --explain E0004`.
|
我们为 Display
trait 中 fmt
的 match
cover 相应的 pattern 即可。这样 main 函数就通过编译了,而有两个测试尚未通过。这里需要我们去完成 Climate
中的 from_str
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
impl FromStr for Climate {
type Err = ParseClimateError;
// TODO: Complete this function by making it handle the missing error
// cases.
fn from_str(s: &str) -> Result<Self, Self::Err> {
let v: Vec<_> = s.split(',').collect();
let (city, year, temp) = match &v[..] {
[city, year, temp] => match city.len() {
0 => return Err(ParseClimateError::NoCity),
_ => (city.to_string(), year, temp),
},
_ => match v.len() {
1 => return Err(ParseClimateError::Empty),
_ => return Err(ParseClimateError::BadLen),
}
};
let year: u32 = year.parse()?;
let temp: f32 = temp.parse()?;
Ok(Climate { city, year, temp })
}
}
|
完成之后,4.7.1 版本的 rustlings 就告一段落了。