rustlings : Rust 小练习
Rustlings 是非常适合入门的 Rust 教程,在学习过程中我以一个新手的角度记录了一些做题的思索,希望能帮到你。
当然,实际上 Rustlings 是完全不需要看答案的,文档和 hint 以及足够帮我们完成所有实验了。
Intro1
移除 // I AM NOT DONE
这一行即可。
Intro2
报错
⚠️ 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,有非常多的方式,这里使用最简单的,可以直接插入字符串。
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
实现一个比大小函数。
我这代码写得磕磕绊绊(笑
if2
第一个报错的原因是返回值不一样,接下来实现 test 的函数即可。
quiz1
一个简单的函数
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 tofill_vec
instead. 意思是说将 vec0 传给别的值(比如 vec2)再传给fill_vec
函数。由于 Vec 没有实现 Copy trait,因此直接let vec2=vec0
的话仍然会导致所有权的转移。想要实现完整的深复制,需要: - 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 ownedVec<i32>
. 意思时说让fill_vec
函数借用这个参数而不是改变所有权,在这个函数中进行复制并返回带有所有权的Vec<i32>
,做如下修改: - 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 ofvec1
entirely — note that this will change what gets printed by the firstprintln!
. 意思是说让fill_vec
函数 mutably 借用这个参数,但是这样的话 vec0 也需要是 mutable 的。 改的地方相比 2 来说并不多,但是为没理解,这是用在什么时候的 feature 呢
move_semantics3
要求是不添加新行,在已有的行上修改。报错主要是参数不是 mutable 的。
这里只用了 vec1,我们不需要管 vec0 的所有权
还是没搞懂,看一下 hint,说的是
这个和 move_semantics2 的区别是
fill_vec
函数的第一行,上一个的第一行是一个转换:` rust let mut vec = vec. to_vec ();
它将控制权转移并且使其可变。 而这道题目是没有的。我们可以把这一行加回来,也可以给某个位置加上
mut` 关键字,修改当前存在的一个绑定,使其变成 mutable 的绑定。
那么加在哪里呢
忽然发现自己犯蠢了,直接加在函数参数里就可以了:
之前的参数是 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
参考 Mutable References,这里的问题和它类似,但是要求我们仅通过换行使编译通过。这里其实用到了 Rust 的性质,我们把 z 借用这一行和 *y
赋值的行交换即可。因为给 y 赋值之后不会再使用 y 了,此时就可以重新 mutable 借用 x 了。
move_semantics6
本题要求只改变引用,不修改其他值。报错原因是 get_char
函数改变的 data 的所有权,导致下面调用 string_uppercase
函数时出错。那么怎样才能在调用 get_char
时不改变 data 的所有权呢?借用。要做的事情很简单。我们首先看一下原始代码:
我们要做的其实是和注释中说的一致:get_char
不应当获取所有权,我们将其修改为借用的版本,而 string_uppercase
需要获得所有权,我们将其改成直接传入的版本:
为什么 string_uppercase
需要获取所有权而不是借用呢?如果我们借用的话会怎样呢?恢复一下:
报错:
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()
,自然会报错。那么我们不借用呢?也就是改成:
新报错为
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`.
因为这段代码
中,data 的类型是 &String
,我们必须通过引用保持正确的参数。如果我们修改成
或
都可以通过编译。如果我们打印一下此时 data 的类型:
会发现 let 前后 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
这道题会学习切片。
会对 a 切片,从 1 开始,到 3 截止,共 3 个元素。
primitive_types5
这道题会学习元组。
这个用法很符合直觉。
primitive_types6
这道题学习了元组中元素的 index
嗯,用法很新颖。
structs1
学习结构体的构造和使用。有传统的类似 C 的构造和元组方式的构造,还有一种被称为 Unit-Like
结构体,它可以没有任何结构,我们可以直接调用。
structs2
这里学习使用 update 语法。相对于 order_template
,只有 name 和 count 字段有变化,因此可以简写为
structs3
学习使用 panic!
宏,以及一些简单的实现
enums1
学习 enum 的定义
enums2
学习 enums 中不同变量的定义,参考 Enum Values。
enums3
学习 match 的用法,参考 The match
Control Flow Construct
这个参数的写法我之前还真没注意。
modules1
Rust 默认所有的结构体中的函数/模块都是 private 的,我们可以通过 pub 关键字使其 public,参考 Making Structs and Enums Public。
modules2
这道题目目的是学会 use ... as ...
的使用,默认也是 private 的,我们可以添加 pub
关键字使其 public。
modules3
注释告诉我们,可以用 use
关键字从任何地方导入 modules,尤其是 Rust 标准库到我们项目的区域(scope)中,这里要求我们导入 SystemTime
和 UNIX_EPOCH
。查询文档 Constant std::time::UNIX_EPOCH 可以直接找到导入方法:
我没看懂为什么 Err 的话报错 "SystemTime before UNIX EPOCH!"
。
vec1
要求我们用 vec!
宏初始化数组
看了一下 hint,第二种方法是先 Vec::new()
创建一个 vector 然后 push 填充。肯定是比宏麻烦的,宏的实现是否就是第二种方法的简写呢?看一下 vec!
宏的实现。
vec2
看一下 test 都干了啥:
首先定义 v:
这代码写得。。根本看不懂。简单解析一下应该是从 1 开始取数字,需要满足要求(filter)x 能整除 2,然后取(take)5 个,并 collect 成数组。有关 collect 的用法见 method.collect。里面的介绍是将迭代器转换为 collection。这道题实际上是要求改变数组的值。对于上面的循环:
i 指向的是 v 的可变引用(iter_mut()
会返回 &mut v
),我们可以使用该迭代器修改当前的值。
hashmap1
学习 Rust 中哈希表的使用。我们需要
而对于它的键计数可以通过
对于它的值求和可以通过
hashmap2
这道题实际上是考察了 match 的用法,因为我们只需要匹配 Banana 和 Pineapple,因此其他的都不需要 match,可以用 _ => None
来放弃匹配。
strings1
在不改变函数签名的条件下编译通过,编译错误报告给出了很明显的提示,返回值用 to_string()
转换即可。看了一下 hint,解释是我们返回的字符串生命周期和程序一样长,但是它是 &str
,我们需要的是 String
。我们还可以通过 String::from
创建字符串。
strings2
这里考察了 String 转换为 &str
(string slice)的方法,很简单。
quiz2
这道题要求我们判断字符串的类型,要么是 String,要么是 &str
。
一些我不了解的函数:
to_owned()
函数:参考 Trait std::borrow::ToOwned 从借来的数据中创建自有数据,那应该是 Stringinto()
: 参考 Into,既然是from()
倒过来,那应该是&str
format!()
宏可以拼接字符串,拼接后的值和from()
类似,也就是 String。参考 Macro std::formattrim()
: 参考 pub fn trim(&self),会返回&str
replace()
: 参考 pub fn replace<‘a, P>(&‘a self, from: P, to: &str) → String 返回的是 Stringto_lowercase()
: 返回的也是 String。
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 一下是否成功,在该返回的时候返回就行了:
如果不想针对错误进行匹配,就上上面那样的话,还有一种更简单的方法:参考 Introducing ?
。只需要加一个 ?
符号就行。
errors3
报错
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(())
:
errors4
有点没看懂要干啥。。看看 hint:
PositiveNonzeroInteger::new
总会创建一个新的实例(instance)并返回结果Ok
,它应当做一些检查,在检查失败时返回Err
,仅当检查确定一切正常时返回Ok
。
所以我们在 new 函数中实现检查,小于 0 是报错负数,等于 0 时报错 0。
除了 if 外,参考 errors5 也可以使用 match 的版本:
errors5
看 TODO 要求我们更新 main 的返回值就能使得这段代码能通过编译了。
看一下报错
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(特征)。
这样我们可以通过修改返回值简单地实现:
更多细节可以看 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.
下面的内容
首先是一些基本定义,和前几道题目类似。
看一看报错:
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
的源码和文档:
在正确的时候直接传出不论,在错误的时候会调用参数,也就是函数 op
,在这里就是 from_creation
,用它来处理 error。看一下 error 的参数是 CreationError
,那么 from_creation
的参数就是 fn from_creation(e: CreationError)
。
接下来通过 test 看返回值,以其中第二个 test 为例
Err
内部是 ParsePosNonzeroError::Creation(CreationError::Negative)
,也就是 ParsePosNonzeroError
。综上,函数定义为
这样我们就能实现好函数了:
可以通过三个样例,没有通过的是
我们可以在 x 执行完 parse
之后 match 一下。
首先写一个符合为自己直觉的版本:
然而报错:
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 的代码,魔改了一番:
在这种情况下,编译通过了。但是这显然不符合我们的需求:对比一下之前的 x:
首先看一下 parse 的文档,parse
的定义是
它还有一种可以自行推断类型的语法 ::<type>
,这么说我们用 let x: i64 = s.parse()
和 let x = s.parse::<i64>()
应该是相同的。那为什么上面还会报错呢?
看一下 hint 吧。。
在
TODO
要求更改的行的下方,有使用Result
上的map_err()
方法将一种类型的错误转换为另一种类型的示例,尝试在parse()
的Result
上使用类似的东西。可以使用?
运算符从函数中提前返回,或者使用match
表达式,或是其他方法。
哦。。我又实现了一个 from_parse
函数:
之后的调用如下:
此时可以编译通过,但是还是无法完成 test::test_parse_error
测试:因为我们报错后没有直接返回,还是传入到了 unwrap()
函数中。
那我们在 map_err
后面加个 ?
试试?报错了。。
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>
这个语法糖。那我们试一下,添加语法糖后:
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
方法了。
最终的代码为
或者上面的
都是正确的。
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 中的泛型。原来的代码是
修改后的代码是:
感觉和 C++ 差不多?
generics3
首先简单地参考 generics2 实现一下泛型:
接下来报错:
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
中添加约束即可:
option1
看一下注释,我们可以修改任何位置,除了 print_number
的函数签名。该函数定义如下:
第一处错误是类型不匹配,给数字添加 Some
得过。
第二处也是一样的,但是添加了 Some
之后报了新错:
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>
会创建:
而初始化数组的操作 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 可不可以借用呢?当然可以。我们有两种改法:
和
第一种改法让 y 的类型变成 &Option<Point>
,我们创建的 Some(Point { x: 100, y: 200 })
的生命周期和 main 函数一致,借用自然也可以被借用。
第二种改法让我们匹配 y 的时候借用,借用自然不会改变 y 的所有权。
看了一下 hint,这其实是考察 ref
关键字,不过自从 Rust 2018 开始我们就可以使用 &
来引用了,用 &
的写法还是很符合直觉的。第三种改法如下所示:
另外 &
和 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 类似(感谢编译器帮了大忙)
看一下 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
。
感谢报错,它提示了我们接下来应该怎么做:
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>
引用计数智能指针 的一种重构,使得它可以安全的用于并发环境。
写完之后来理一下主要逻辑:
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,如果我们消费了一个,那么再次使用的时候不会使用第一个了。参考下面这个例子:
输出为
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
不能通过吧。
iterators3
Tip
这个练习比之前所有的都要大,相信你可以完成它。
接下来是任务
- 完成除法功能以通过前四个测试
- 完成
result_with_list
和list_of_results
函数以通过剩余的测试
除法功能咋用 match_error
实现啊,我用 if else 实现了。
接下来看 result_with_list
的实现。原始实现为
测试为
分析一下 division_results
的值为 [1, 11, 1426, 3]
,我们希望返回的值为 Ok([1, 11, 1426, 3])
。
哦,强大的 collect
trait,两个函数通过简单的 collect 就都解决了。等有时间看一下 collect 到底是怎么实现的。
iterators4
要求我们实现一个阶乘(factorial)函数。不需要使用 return、循环和其他变量。Rust 提供了非常强的 API:
仅需要一行就能实现。product()
trait 实现了连乘的功能。
iterators5
看看注释:
Hint
让我们定义一个简单的模型以跟踪 Rustlings 练习的进程。这个进程会用哈希表建模,它的键是练习的名字,值是练习的进程。创建了两个计数函数以计算给定进程的练习数量。这些计数函数使用了至关重要的(imperative)循环风格。使用函数式的 iterators 重建这些计数。只需要修改
count_iterator
和count_collection_iterator
这两个迭代器方法。
边看文档边做题:Trait std::iter::Iterator,文档还是很有用的。
第一个是遍历 map 的 iter 并对满足要求的值计数。我用 filter
看是否满足要求,用 count
计数。
第二个的 map 夹在了 Vector
slice 里,遍历到还简单,用 iter()
+ map()
计算每个 iter
中的值,最后用 sum
计数。map 实现的闭包直接拿前一个填充了。
threads1
看看注释:
Note
这道题目的想法是在第 22 行产生(spawned)的线程完成作业,而主线程会监视作业直到完成 10 个作业。因为产生的线程的睡眠时间和等待线程睡眠时间的区别,你会看到 6 行 “waiting…” 且程序没有运行到超时就终止了。
唔,不管怎么样,先看一下程序的逻辑吧。
status 是用 Arc 创建的,它支持原子操作。Spawn 参考 Function std::thread::spawn,它会产生一个新线程,并为其返回一个 JoinHandle
。
看看它的原始实现
spawn
函数的闭包和返回值都有约束:
-
'static
意味着闭包和返回值必须具有整个程序执行的生命周期,因为线程的生命周期可能超过它们被创建的生命周期。如果线程及其返回值比它们的调用者存活时间更长,我们需要确保它们在之后也是有效的。由于我们不知道它们什么时候返回,因此我们需要让它尽可能长时间地有效,也就是直到程序结束,因此是
'static
的生命周期。 -
Send
约束是因为闭包需要从产生它的线程按值传递给新线程。它的返回值需要从新线程传递到它加入的线程。注意,Send
trait 表示从线程传递到线程是安全的。 Sync 表示在线程之间传递引用是安全的。
在该线程中会执行十次 sleep(250ms)
,每次会对某个计数器加一。最开始的错误也出现在这里:
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
即可。这个章节的最后有一处总结:
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
看上去缺个分号,补充上去果然编译通过了。主要考察的是宏的格式。
宏的定义为
MacroRulesDefinition :
macro_rules ! IDENTIFIER MacroRulesDef
那么 MacroRulesDef
就是
{
() => {
println!("Check out my macro!");
}
($val:expr) => {
println!("Look at this other macro: {}", $val);
}
}
根据
MacroRulesDef :
( MacroRules ) ;
| [ MacroRules ] ;
| { MacroRules }
MacroRules
就是
() => {
println!("Check out my macro!");
}
($val:expr) => {
println!("Look at this other macro: {}", $val);
}
再往下分析
MacroRules :
MacroRule ( ; MacroRule )* ;?
因此要添加的符号是分号。
quiz4
编写一个宏即可通过。
clippy1
Tip
Clippy 工具是用于分析代码的 lint 的集合,因此你可以用它匹配常见的错误,提升代码质量。
对于这些练习,当存在 clippy warning 时就会报错。检查 clippy 输出的建议以完成练习。
对于本道练习的建议是
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。
首先实现了一个正常版本的
然后是稍微不正常版本的:
最后是我写完之后觉得醍醐灌顶的:
from_str
Tip
它和 from_into 类似,但是在这里我们会实现
FromStr
并返回errors
而不是返回一个默认值。另外,在实现FromStr
时,你可以在 strings 上使用parse
方法以生成实现者类型的对象。
不得不说 match 是真的好用。如果我们希望用其他方法的化,hint 也给了提示:
Hint
我们可以将
Result
的map_err
方法与函数或闭包一起使用,以包装parse::<usize>
的错误
Hint
try_from_into
Tip
TryFrom
是简单且安全的类型转换,它会在某些情况下以可控的方式失败。在通常状况下,它和
From
是一致的。主要的区别是它会返回一个Result
类型而不是目标类型
用一种非常鱼唇的方法解决了:
看了一眼 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
在许多整数类型上通用?
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 的话,会报这样的错误:
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
函数实现。
它将字符串通过 from_str
(在 from_str 中有过介绍)转换成 Climate
类型,然后通过 ?
处理错误。这里只实现了一个相关的 From
trait:
这段代码为 ParseClimateError
实现了 From<ParseIntError>
。我们可以仿照这个例子实现下面的 From<ParseFloatError>
。
不过这和 main 函数无法通过编译无关,main 函数的报错为:
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,让其他错误可以包裹这个错误类型。
看看接下来的报错:
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
。
完成之后,4.7.1 版本的 rustlings 就告一段落了。