Rustlings 练习回顾

Rustlings 是 Rust 入门的一点小练习,抽了几节课的时间做完了,来按顺序回顾一部分比较印象深刻的题目。

总的来说还是比较简单的,代码量也不大,作为入门练习刚刚好。不过一些不太熟的地方还是需要间接查一下,此外还有部分解法不够好,下面主要就是这些题目,记下来加深下印象。

我在 Windows Terminal 下完成的练习,拆分成两块,左边 PowerShell 运行 Rustlings,右边 Neovim 写代码。

此外有相当一部分题目是直接编译器就告知答案了,这部分我就比较少涉及了(因为通过比较快就没啥印象了)。还有编辑器的 LSP 也助力不少。

06 移动语义

第 2 题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn fill_vec(vec: Vec<i32>) -> Vec<i32> {
let mut vec = vec;

vec.push(88);

vec
}

...

fn move_semantics2() {
let vec0 = vec![22, 44, 66];

let vec1 = fill_vec(vec0);

assert_eq!(vec0, [22, 44, 66]);
assert_eq!(vec1, [22, 44, 66, 88]);
}

TODO 在这边,要让两个 vector 独立,很显然是要 clone 一下 vec0,即变成 fill_vec(v0.clone())

不过可能是刚开始做还不太熟练,抑或是脑子抽了,没去改 TODO 这边,我去改函数那块了,把 clone 放在了里面:

1
2
3
4
5
6
7
fn fill_vec(vec: &Vec<i32>) -> Vec<i32> {
let mut vec = vec.clone();

vec.push(88);

vec
}

然后用 fill_vec(&vec0) 来调用。

这样也能过,不过 Clippy 会警告

writing `&Vec` instead of `&[_]` involves a new object where a slice will do

即我应该改成 fill_vec(vec: &[i32]),这是因为 &[i32] 是一个更通用的类型,可以接受任何切片,例如说 &[1, 2, 3] 等,而 &Vec<i32> 只能接受 Vec<i32> 的引用。即前者只关心数据本身,而不关注数据的具体容器类型。

不过最好的解法还是直接在调用处 clone 一下。

09 字符串

第 4 题

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
fn string_slice(arg: &str) {
println!("{arg}");
}

fn string(arg: String) {
println!("{arg}");
}

// TODO: Here are a bunch of values - some are `String`, some are `&str`.
// Your task is to replace `placeholder(…)` with either `string_slice(…)`
// or `string(…)` depending on what you think each value is.
fn main() {
placeholder("blue");

placeholder("red".to_string());

placeholder(String::from("hi"));

placeholder("rust is fun!".to_owned());

placeholder("nice weather".into());

placeholder(format!("Interpolation {}", "Station"));

// WARNING: This is byte indexing, not character indexing.
// Character indexing can be done using `s.chars().nth(INDEX)`.
placeholder(&String::from("abc")[0..1]);

placeholder(" hello there ".trim());

placeholder("Happy Monday!".replace("Mon", "Tues"));

placeholder("mY sHiFt KeY iS sTiCkY".to_lowercase());
}

这道题是让我们根据传入的参数类型,选择调用 string_slice 还是 string 函数。

这其实可以作弊的,我在参数上 K 一下,类型就出来了。

不过其实我也没怎么这样用,唯一一个做错了的是倒数第三个 trim,答案是 string_slice,而我写的是 string。即 trim 返回的是一个字符串切片 &str,而不是一个新的 String

我比较奇怪的是都是修改,为何 replace 却是 String 呢?

查了一下,这是因为 trim 只需要找到第一个与最后一个非空白字符的索引,然后重新返回新的 &str 就可以了,不需要重新分配内存创建一个新的字符串,即这是一个视图操作。

replace 则几乎总是需要创建新的字符串数据。此外 String 默认不可变,replace 也不会修改原字符串,而是返回一个新的字符串。

可以,这很合理。

11 哈希表

第 2 题

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
// We're collecting different fruits to bake a delicious fruit cake. For this,
// we have a basket, which we'll represent in the form of a hash map. The key
// represents the name of each fruit we collect and the value represents how
// many of that particular fruit we have collected. Three types of fruits -
// Apple (4), Mango (2) and Lychee (5) are already in the basket hash map. You
// must add fruit to the basket so that there is at least one of each kind and
// more than 11 in total - we have a lot of mouths to feed. You are not allowed
// to insert any more of the fruits that are already in the basket (Apple,
// Mango, and Lychee).

use std::collections::HashMap;

#[derive(Hash, PartialEq, Eq, Debug)]
enum Fruit {
Apple,
Banana,
Mango,
Lychee,
Pineapple,
}

fn fruit_basket(basket: &mut HashMap<Fruit, u32>) {
let fruit_kinds = [
Fruit::Apple,
Fruit::Banana,
Fruit::Mango,
Fruit::Lychee,
Fruit::Pineapple,
];

for fruit in fruit_kinds {
// TODO: Insert new fruits if they are not already present in the
// basket. Note that you are not allowed to put any type of fruit that's
// already present!
}
}

大概就是说有 5 种水果,已经有了 3 种类型共 11 个,要求每种水果都至少要有 1 个,并且总数要超过 11 个。

我的解法:

1
2
3
4
5
6
7
8
for fruit in fruit_kinds {
match fruit {
Fruit::Apple | Fruit::Mango | Fruit::Lychee => {}
_ => {
basket.insert(fruit, 1);
}
}
}

原有的几种就不添加了,其他的都添加 1 个。

不过这种解法不好,要是有了一百个呢?不过也就是因为不多我才手动写了,要真有这么多,我应该会写一个 if-else,然后查一下检查是否存在的方法,不存在就插入。

这样的想法显然不够 Rusty,标答如下:

1
2
3
for fruit in fruit_kinds {
basket.entry(fruit).or_insert(1);
}

一行就结束了,非常优雅。

entry 方法会返回一个 Entry 枚举,表示某个键在哈希表中的条目。然后 or_insert(1) 会检查这个条目是否存在,如果不存在就插入值 1,如果存在则不做任何操作。

下面是标准库中 Entry 的定义(简化了部分标志):

1
2
3
4
5
6
7
8
9
10
11
pub enum Entry<
'a,
K: 'a,
V: 'a,
> {
/// A vacant entry.
Vacant(VacantEntry<'a, K, V, A>),

/// An occupied entry.
Occupied(OccupiedEntry<'a, K, V, A>),
}

那么 or_insert 的定义也就呼之欲出了:

1
2
3
4
5
6
pub fn or_insert(self, default: V) -> &'a mut V {
match self {
Occupied(entry) => entry.into_mut(),
Vacant(entry) => entry.insert(default),
}
}

第 3 题

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
#[derive(Default)]
struct TeamScores {
goals_scored: u8,
goals_conceded: u8,
}

fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> {
// The name of the team is the key and its associated struct is the value.
let mut scores = HashMap::<&str, TeamScores>::new();

for line in results.lines() {
let mut split_iterator = line.split(',');
// NOTE: We use `unwrap` because we didn't deal with error handling yet.
let team_1_name = split_iterator.next().unwrap();
let team_2_name = split_iterator.next().unwrap();
let team_1_score: u8 = split_iterator.next().unwrap().parse().unwrap();
let team_2_score: u8 = split_iterator.next().unwrap().parse().unwrap();

// TODO: Populate the scores table with the extracted details.
// Keep in mind that goals scored by team 1 will be the number of goals
// conceded by team 2. Similarly, goals scored by team 2 will be the
// number of goals conceded by team 1.
}

scores
}

大概就是统计每个队的进球和失球数。

1
2
3
4
5
6
7
8
9
10
11
12
let scores1 = scores.entry(team_1_name).or_insert(TeamScores {
goals_scored: 0,
goals_conceded: 0,
});
scores1.goals_scored += team_1_score;
scores1.goals_conceded += team_2_score;
let scores2 = scores.entry(team_2_name).or_insert(TeamScores {
goals_scored: 0,
goals_conceded: 0,
});
scores2.goals_scored += team_2_score;
scores2.goals_conceded += team_1_score;

学乖了,这次轮到我用 entry 了。不过还是很丑陋,看看标答:

1
2
3
4
5
6
7
8
9
10
// Insert the default with zeros if a team doesn't exist yet.
let team_1 = scores.entry(team_1_name).or_default();
// Update the values.
team_1.goals_scored += team_1_score;
team_1.goals_conceded += team_2_score;

// Similarly for the second team.
let team_2 = scores.entry(team_2_name).or_default();
team_2.goals_scored += team_2_score;
team_2.goals_conceded += team_1_score;

Derive 了 Default,然后直接 or_default() 就行了,很行很行,没注意看。

1
2
3
4
5
6
pub fn or_default(self) -> &'a mut V {
match self {
Occupied(entry) => entry.into_mut(),
Vacant(entry) => entry.insert(Default::default()),
}
}

不过问题来了,u8 的默认值是 0,这个很好理解,但真的是这样的吗,可以验证一下吗?于是我写了下面的代码:

let a: u8 = Default::default();

然后在 default 方法上 gd 一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
macro_rules! default_impl {
($t:ty, $v:expr, $doc:tt) => {
#[stable(feature = "rust1", since = "1.0.0")]
impl Default for $t {
#[inline(always)]
#[doc = $doc]
fn default() -> $t {
$v
}
}
};
}

...

default_impl! { u8, 0, "Returns the default value of `0`" }

确实如此!

此外还看到一些默认值,默认的布尔值是 falsechar'\0'ascii::Charascii::Char::Null,数字类的都是 0。

12 Options

第 1 题

1
2
3
4
5
6
7
// This function returns how much ice cream there is left in the fridge.
// If it's before 22:00 (24-hour system), then 5 scoops are left. At 22:00,
// someone eats it all, so no ice cream is left (value 0). Return `None` if
// `hour_of_day` is higher than 23.
fn maybe_ice_cream(hour_of_day: u16) -> Option<u16> {
// TODO: Complete the function body.
}

题干就不复述了,不是什么重点,直接先看我的解法:

1
2
3
4
5
6
7
if hour_of_day < 22 {
Some(5)
} else if (22..24).contains(&hour_of_day) {
Some(0)
} else {
None
}

实际上一开始还不是这样的,一开始其实是:

1
2
3
4
5
6
7
if hour_of_day < 22 {
Some(5)
} else if 22 <= hour_of_day && hour_of_day < 24 {
Some(0)
} else {
None
}

不过这样 Clippy 会警告

manual `Range::contains` implementation

可以改成 (22..24).contains(&hour_of_day),确实更清晰,我喜欢你。

不过我确实有想过这是不是可以用 match 来写,不过我没想到也懒得翻书了,就直接 if-else 后来看看标答了:

1
2
3
4
5
match hour_of_day {
0..=21 => Some(5),
22..=23 => Some(0),
_ => None,
}

果然优雅!

第 3 题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}

fn main() {
let optional_point = Some(Point { x: 100, y: 200 });

// TODO: Fix the compiler error by adding something to this match statement.
match optional_point {
Some(p) => println!("Coordinates are {},{}", p.x, p.y),
_ => panic!("No match!"),
}

println!("{optional_point:?}"); // Don't change this line.
}

编译器提示秒了:

1
2
3
4
match optional_point {
Some(ref p) => println!("Coordinates are {},{}", p.x, p.y),
_ => panic!("No match!"),
}

ref 关键字可以让我们在模式匹配中获取引用,而不是值的所有权,类似 &

正经来说,& 是一个表达式运算符,用于在代码中创建一个引用。ref 是一个模式关键字,用于在 match 模式中绑定一个引用。

不过标答还给了一种解法,将 & 加在 optional_point 上:

1
2
3
4
match &optional_point {
Some(p) => println!("Coordinates are {},{}", p.x, p.y),
_ => panic!("No match!"),
}

13 错误处理

第 4 题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#[derive(PartialEq, Debug)]
enum CreationError {
Negative,
Zero,
}

#[derive(PartialEq, Debug)]
struct PositiveNonzeroInteger(u64);

impl PositiveNonzeroInteger {
fn new(value: i64) -> Result<Self, CreationError> {
// TODO: This function shouldn't always return an `Ok`.
// Read the tests below to clarify what should be returned.
Ok(Self(value as u64))
}
}

大概就是从 i64 创建一个 PositiveNonzeroInteger(即 u64newtype),如果值是负数或者 0 就返回错误。

我的平凡朴素解法:

1
2
3
4
5
6
7
if value > 0 {
Ok(Self(value as u64))
} else if value == 0 {
Err(CreationError::Zero)
} else {
Err(CreationError::Negative)
}

你这就是写其他语言写的。一看这么多分支的 if-else 就知道不是 Rustacean。那么 match 应该怎么写呢:

1
2
3
4
5
match value.cmp(&0) {
Ordering::Less => Err(CreationError::Negative),
Ordering::Equal => Err(CreationError::Zero),
Ordering::Greater => Ok(Self(value as u64)),
}

恍然大悟,比大小返回的不会是 1, 0, -1 这种东西了,而是 Ordering 枚举,清晰明了。

14 泛型

第 1 题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// `Vec<T>` is generic over the type `T`. In most cases, the compiler is able to
// infer `T`, for example after pushing a value with a concrete type to the vector.
// But in this exercise, the compiler needs some help through a type annotation.

fn main() {
// TODO: Fix the compiler error by annotating the type of the vector
// `Vec<T>`. Choose `T` as some integer type that can be created from
// `u8` and `i8`.
let mut numbers = Vec::new();

// Don't change the lines below.
let n1: u8 = 42;
numbers.push(n1.into());
let n2: i8 = -1;
numbers.push(n2.into());

println!("{numbers:?}");
}

这个其实没什么,只是记录一下,u8i8 都可以转换成 i16

如果写的是 i8,会报错:

1
2
3
4
5
the trait bound `i8: std::convert::From<u8>` is not satisfied
the trait `From<u8>` is not implemented for `i8`
but trait `From<bool>` is implemented for it
for that trait implementation, expected `bool`, found `u8`
required for `u8` to implement `std::convert::Into<i8>`

主要是来看看具体标准库的转换实现,如下:

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
// Conversion traits for primitive integer and float types
// Conversions T -> T are covered by a blanket impl and therefore excluded
// Some conversions from and to usize/isize are not implemented due to portability concerns
macro_rules! impl_from {
...
($Small:ty => $Large:ty, #[$attr:meta], $doc:expr $(,)?) => {
#[$attr]
impl From<$Small> for $Large {
// Rustdocs on the impl block show a "[+] show undocumented items" toggle.
// Rustdocs on functions do not.
#[doc = $doc]
#[inline(always)]
fn from(small: $Small) -> Self {
small as Self
}
}
};
}

// signed integer -> signed integer
impl_from!(i8 => i16, #[stable(feature = "lossless_int_conv", since = "1.5.0")]);
...

// unsigned integer -> signed integer
impl_from!(u8 => i16, #[stable(feature = "lossless_int_conv", since = "1.5.0")]);
...

宏的定义有递归展开,只贴了关键部分。

15 Traits

第 4 题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
trait Licensed {
fn licensing_info(&self) -> String {
"Default license".to_string()
}
}

struct SomeSoftware;
struct OtherSoftware;

impl Licensed for SomeSoftware {}
impl Licensed for OtherSoftware {}

// TODO: Fix the compiler error by only changing the signature of this function.
fn compare_license_types(software1: ???, software2: ???) -> bool {
software1.licensing_info() == software2.licensing_info()
}

答案是将 ??? 改成 impl Licensed,即:

fn compare_license_types(software1: impl Licensed, software2: impl Licensed) -> bool

比较简单,不过把这个记录下来的原因是,我想与 dyn Trait 区别一下,于是问了点信息。

impl Trait 是一种语法糖,它允许你在函数参数或返回值位置指定一个 Trait,而不是一个具体的类型。但这并不是说函数会接受或返回一个「Trait 对象」。相反,它是在说:

  • 作为参数时,这个函数接受任何实现了 Trait 的 某个具体类型。编译器会为每个不同的具体类型生成一份代码(单态化)。
  • 作为返回值时,这个函数返回某个具体类型,这个类型实现了 Trait。调用者不知道具体的类型是什么,但知道它实现了 Trait。

这是「零成本抽象」,所有类型信息都在编译时确定,编译器可以直接调用正确的方法,没有运行时开销(如查找虚函数表)。性能与直接使用具体类型几乎相同。impl Trait 实际上代表的是一个具体的、但未指定的类型。

总的来说,impl Trait 是为了实现静态分发(编译时多态)

dyn Trait 代表一个 Trait 对象,它允许你在运行时处理那些实现了某个 Trait 的不同具体类型

当提到 &dyn TraitBox<dyn Trait> 时,是在表示:「有一个指针,它指向一个实现了 Trait 的值,但不知道这个值的具体类型是什么,只知道它能做 Trait 定义的事情。」

Trait 对象由两部分组成:

  • 数据指针:指向实际存储在内存中的具体数据。
  • 虚函数表指针:指向一个「虚函数表」,这个表包含了该具体类型实现 Trait 中所有方法的函数指针,以及一些元数据(如类型大小、对齐方式)。

当在 dyn Trait 上调用一个方法时,Rust 会通过虚函数表指针找到正确的方法并执行它。因此 dyn Trait 允许动态分发(运行时多态),即在运行时决定调用哪个具体类型的方法。

18 迭代器

第 2 题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// TODO: Complete the `capitalize_first` function.
// "hello" -> "Hello"
fn capitalize_first(input: &str) -> String {
let mut chars = input.chars();
match chars.next() {
None => String::new(),
Some(first) => todo!(),
}
}

// TODO: Apply the `capitalize_first` function to a slice of string slices.
// Return a vector of strings.
// ["hello", "world"] -> ["Hello", "World"]
fn capitalize_words_vector(words: &[&str]) -> Vec<String> {
// ???
}

// TODO: Apply the `capitalize_first` function again to a slice of string
// slices. Return a single string.
// ["hello", " ", "world"] -> "Hello World"
fn capitalize_words_string(words: &[&str]) -> String {
// ???
}

首先是第一个 capitalize_first,我最开始的想法就是把后面的 chars 收集起来,拼接在第一个字母后面。于是我这样写:

Some(first) => first.to_ascii_uppercase().to_string() + chars.collect(),

不过这样会报错:

1
2
a value of type `&str` cannot be built from an iterator over elements of type `char`
the trait `std::iter::FromIterator<char>` is not implemented for `&str`

原因是 chars.collect() 返回的是一个 String,而 + 操作符要求右侧是一个 &str+ 会消耗左侧的 String,并将右侧的 &str 拼接到它后面,返回一个新的 String

添加一个 & 也不够,这是因为这反而让编译器把 chars.collect() 推测成了 str

现在看来那就可以写成:

Some(first) => first.to_ascii_uppercase().to_string() + &chars.collect::<String>(),

不过我当时写的时候不太熟悉啊,拆开来手动标注了类型:

1
2
3
4
Some(first) => {
let other: String = chars.collect();
first.to_ascii_uppercase().to_string() + &other
}

再来看看标答:

Some(first) => first.to_uppercase().to_string() + chars.as_str(),

as_char 函数签名如下:

1
2
3
impl<'a> Chars<'a> {
pub fn as_str(&self) -> &'a str { ... }
}

这样不用收集了,直接拿到剩余的字符串切片。

另外下面还有两个空,我是这样做的:

1
2
3
4
5
6
7
8
9
// ["hello", "world"] -> ["Hello", "World"]
fn capitalize_words_vector(words: &[&str]) -> Vec<String> {
words.iter().map(|word| capitalize_first(word)).collect()
}

// ["hello", " ", "world"] -> "Hello World"
fn capitalize_words_string(words: &[&str]) -> String {
capitalize_words_vector(words).join("")
}

似乎是很平凡的做法。

不过标答还是更高一个档次,标答两个空都是第一个空的解法:

words.iter().map(|word| capitalize_first(word)).collect()

原因是 collect 可以根据返回值类型推断出需要收集成什么类型的集合,非常妙。

第 3 题

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
#[derive(Debug, PartialEq, Eq)]
enum DivisionError {
// Example: 42 / 0
DivideByZero,
// Only case for `i64`: `i64::MIN / -1` because the result is `i64::MAX + 1`
IntegerOverflow,
// Example: 5 / 2 = 2.5
NotDivisible,
}

// TODO: Calculate `a` divided by `b` if `a` is evenly divisible by `b`.
// Otherwise, return a suitable error.
fn divide(a: i64, b: i64) -> Result<i64, DivisionError> {
todo!();
}

// TODO: Add the correct return type and complete the function body.
// Desired output: `Ok([1, 11, 1426, 3])`
fn result_with_list() {
let numbers = [27, 297, 38502, 81];
let division_results = numbers.into_iter().map(|n| divide(n, 27));
}

// TODO: Add the correct return type and complete the function body.
// Desired output: `[Ok(1), Ok(11), Ok(1426), Ok(3)]`
fn list_of_results() {
let numbers = [27, 297, 38502, 81];
let division_results = numbers.into_iter().map(|n| divide(n, 27));
}

这个跟上面一样,展现了类型推断的实用之处。不过我一开始没领会到,于是这样写:

1
2
3
4
5
6
7
8
9
10
11
fn result_with_list() -> Result<Vec<i64>, DivisionError> {
let numbers = [27, 297, 38502, 81];
let division_results: Result<_, _> = numbers.into_iter().map(|n| divide(n, 27)).collect();
division_results
}

fn list_of_results() -> Vec<Result<i64, DivisionError>> {
let numbers = [27, 297, 38502, 81];
let division_results: Vec<_> = numbers.into_iter().map(|n| divide(n, 27)).collect();
division_results
}

指定了 collect 应该是收集成 Result 的 Vec 还是 Vec 的 Result,似乎是因为一开始忘了标注返回值类型?

标答:

1
2
3
4
5
6
7
8
9
10
11
fn result_with_list() -> Result<Vec<i64>, DivisionError> {
let numbers = [27, 297, 38502, 81];
let division_results = numbers.into_iter().map(|n| divide(n, 27));
division_results.collect()
}

fn list_of_results() -> Vec<Result<i64, DivisionError>> {
let numbers = [27, 297, 38502, 81];
let division_results = numbers.into_iter().map(|n| divide(n, 27));
division_results.collect()
}

当然其实也用不着中间变量,只不过一开始给了。

第 4 题

1
2
3
4
5
6
7
8
9
10
11
12
13
fn factorial(num: u64) -> u64 {
// TODO: Complete this function to return the factorial of `num` which is
// defined as `1 * 2 * 3 * … * num`.
// https://en.wikipedia.org/wiki/Factorial
//
// Do not use:
// - early returns (using the `return` keyword explicitly)
// Try not to use:
// - imperative style loops (for/while)
// - additional variables
// For an extra challenge, don't use:
// - recursion
}

即不用 return, for, while,也不用额外变量和递归来计算阶乘。

看到阶乘其实我第一想法就是递归,不过看到额外说了不能用递归。于是我想了一下,写了下面的代码:

(2..=num).reduce(|x, y| x * y).unwrap_or(1)

现学现用,我也会用 Range 了。感觉这一行这么短无敌了,估计就是标答了吧。

不过一开始是 (1..=num),然后对 num 也有 if-else 判断。但后面看了看原来可以直接返回空 Range 的,所以就改成了 unwrap_or(1),这样就可以一行完成了。

结果标答给了两个答案:

1
2
3
4
5
6
// fold
#[allow(clippy::unnecessary_fold)]
(2..=num).fold(1, |acc, x| acc * x)

// product
(2..=num).product()

先不提内置的方法 product,先来看看 reduce 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
pub trait Iterator {
/// Reduces the elements to a single one, by repeatedly applying a reducing
/// operation.
fn reduce<F>(mut self, f: F) -> Option<Self::Item>
where
Self: Sized,
F: FnMut(Self::Item, Self::Item) -> Self::Item,
{
let first = self.next()?;
Some(self.fold(first, f))
}
}

reduce 会返回一个 Option。可以看到其实里面用的就是 fold,因此再来看看 fold 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pub trait Iterator {
/// Folds every element into an accumulator by applying an operation,
/// returning the final result.
fn fold<B, F>(mut self, init: B, mut f: F) -> B
where
Self: Sized,
F: FnMut(B, Self::Item) -> B,
{
let mut accum = init;
while let Some(x) = self.next() {
accum = f(accum, x);
}
accum
}
}

fold 需要一个初值,然后对迭代器的每个元素应用闭包 f,将当前累积值和当前元素传递给 f,并将返回值作为新的累积值,最终直接返回累积值。同时不要求累加器的类型与迭代器的元素类型相同(FnMut(B, Self::Item) -> B)。

reduce 不需要初值,并要求累加器类型与元素类型相同(FnMut(Self::Item, Self::Item) -> Self::Item)。如果迭代器为空,reduce 会返回 None,否则返回 Some 包含计算结果。

再看回 fold 解法,上面有一个 #[allow(clippy::unnecessary_fold)],这是因为 Clippy 会警告说用 fold 有点多余,因为可以直接用 product

this `.fold` can be written more succinctly using another method

不过 reduce 就没有这个待遇,我查了一下甚至没查到一个规则名带有 reduce。

那么再来看看 product

1
2
3
4
5
6
7
8
9
10
pub trait Iterator {
/// Iterates over the entire iterator, multiplying all the elements
fn product<P>(self) -> P
where
Self: Sized,
P: Product<Self::Item>,
{
Product::product(self)
}
}

要求实现了 Product trait,然后调用 Product::product(self) 来计算乘积。

1
2
3
4
5
pub trait Product<A = Self>: Sized {
/// Takes an iterator and generates `Self` from the elements by multiplying
/// the items.
fn product<I: Iterator<Item = A>>(iter: I) -> Self;
}

然后是具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
macro_rules! integer_sum_product {
(@impls $zero:expr, $one:expr, #[$attr:meta], $($a:ty)*) => ($(
...

#[$attr]
impl Product for $a {
fn product<I: Iterator<Item=Self>>(iter: I) -> Self {
iter.fold(
$one,
#[rustc_inherit_overflow_checks]
|a, b| a * b,
)
}
}

...
)*);
}

...

integer_sum_product! { i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize }

另外闭包我老是多加一个 ->,即 |x, y| -> x * y,鉴定为写 TypeScript 写的。-> 用来表示返回值类型,这样写逻辑上也不对,只是看着更顺眼罢了。

第 5 题

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
#[derive(Clone, Copy, PartialEq, Eq)]
enum Progress {
None,
Some,
Complete,
}

fn count_for(map: &HashMap<String, Progress>, value: Progress) -> usize {
let mut count = 0;
for val in map.values() {
if *val == value {
count += 1;
}
}
count
}

// TODO: Implement the functionality of `count_for` but with an iterator instead
// of a `for` loop.
fn count_iterator(map: &HashMap<String, Progress>, value: Progress) -> usize {
// `map` is a hash map with `String` keys and `Progress` values.
// map = { "variables1": Complete, "from_str": None, … }
}

fn count_collection_for(collection: &[HashMap<String, Progress>], value: Progress) -> usize {
let mut count = 0;
for map in collection {
for val in map.values() {
if *val == value {
count += 1;
}
}
}
count
}

// TODO: Implement the functionality of `count_collection_for` but with an
// iterator instead of a `for` loop.
fn count_collection_iterator(collection: &[HashMap<String, Progress>], value: Progress) -> usize {
// `collection` is a slice of hash maps.
// collection = [{ "variables1": Complete, "from_str": None, … },
// { "variables2": Complete, … }, … ]
}

第一个 TODO 标答如下:

map.values().filter(|val| **val == value).count()

我一开始也是这么写的,不过后面改成了下面的写法,只是为了少一个 *

map.iter().filter(|p| *p.1 == value).count()

map.values() 返回的是 &Progress 的迭代器,因此 filter 的闭包参数是 &&Progress,所以需要两次解引用:

1
2
3
4
5
6
7
8
9
pub trait Iterator {
fn filter<P>(self, predicate: P) -> Filter<Self, P>
where
Self: Sized,
P: FnMut(&Self::Item) -> bool,
{
...
}
}

map.iter() 返回的是 (&String, &Progress) 的迭代器,因此闭包参数是 &(&String, &Progress),而 .1 会自动解引用一次,所以只需要内部再一次解引用就够了。

从语义上,应该还是标答的第一种做法更好,表达了只关心值的意图。

第二个没啥好的想法,就对每一个元素 map 调用第一个函数:

collection.iter().map(|m| count_iterator(m, value)).sum()

这也是标答的做法。不过标答还给了一种展平的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 标答
collection
.iter()
.flat_map(HashMap::values) // or just `.flatten()` when wanting the default iterator (`HashMap::iter`)
.filter(|val| **val == value)
.count()

// 按照注释的写法
collection
.iter()
.flatten()
.filter(|p| *p.1 == value)
.count()

先来看看 flat_map

1
2
3
4
5
6
7
8
9
10
11
pub trait Iterator {
/// Creates an iterator that works like map, but flattens nested structure.
fn flat_map<U, F>(self, f: F) -> FlatMap<Self, U, F>
where
Self: Sized,
U: IntoIterator,
F: FnMut(Self::Item) -> U,
{
FlatMap::new(self, f)
}
}

然后:

1
2
3
4
5
6
7
impl<I: Iterator, U: IntoIterator, F: FnMut(I::Item) -> U> FlatMap<I, U, F> {
pub(in crate::iter) fn new(iter: I, f: F) -> FlatMap<I, U, F> {
FlatMap { inner: FlattenCompat::new(iter.map(f)) }
}

...
}

下面就不深究了,看一下 flatten

1
2
3
4
5
6
7
8
9
10
pub trait Iterator {
/// Creates an iterator that flattens nested structure.
fn flatten(self) -> Flatten<Self>
where
Self: Sized,
Self::Item: IntoIterator,
{
Flatten::new(self)
}
}

再继续:

1
2
3
4
5
impl<I: Iterator<Item: IntoIterator>> Flatten<I> {
pub(in super::super) fn new(iter: I) -> Flatten<I> {
Flatten { inner: FlattenCompat::new(iter) }
}
}

殊途同归。

19 智能指针

注意题目没有编号,要按照 Rustlings 给的顺序做。否则就会改 arc1.rs 改完后,Rustlings 毫无变化了。

20 线程

第 2 题

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
struct JobStatus {
jobs_done: u32,
}

fn main() {
// TODO: `Arc` isn't enough if you want a **mutable** shared state.
let status = Arc::new(JobStatus { jobs_done: 0 });

let mut handles = Vec::new();
for _ in 0..10 {
let status_shared = Arc::clone(&status);
let handle = thread::spawn(move || {
thread::sleep(Duration::from_millis(250));

// TODO: You must take an action before you update a shared value.
status_shared.jobs_done += 1;
});
handles.push(handle);
}

// Waiting for all jobs to complete.
for handle in handles {
handle.join().unwrap();
}

// TODO: Print the value of `JobStatus.jobs_done`.
println!("Jobs done: {}", todo!());
}

猜到了标答大概用的是互斥锁,不过我用的是读写锁:

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
fn main() {
let status = Arc::new(RwLock::new(JobStatus { jobs_done: 0 }));
// ^^^^^^^^^^^ ^

let mut handles = Vec::new();
for _ in 0..10 {
let status_shared = Arc::clone(&status);
let handle = thread::spawn(move || {
thread::sleep(Duration::from_millis(250));

let mut status_write_guard = status_shared.write().unwrap();
// ^^^^^^^^^^^^^^^^^
status_write_guard.jobs_done += 1;
});
handles.push(handle);
}

for handle in handles {
handle.join().unwrap();
}

let status_read_guard = status.read().unwrap();
// ^^^^^^^^^^^^^^^^
println!("Jobs done: {}", status_read_guard.jobs_done);
}

不记得读写锁写法还去找了一下之前写的。不过其实应该用不到读写锁,因为没有读多写少的场景,直接用互斥锁就行了。

标答:

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
fn main() {
// `Arc` isn't enough if you want a **mutable** shared state.
// We need to wrap the value with a `Mutex`.
let status = Arc::new(Mutex::new(JobStatus { jobs_done: 0 }));
// ^^^^^^^^^^^ ^

let mut handles = Vec::new();
for _ in 0..10 {
let status_shared = Arc::clone(&status);
let handle = thread::spawn(move || {
thread::sleep(Duration::from_millis(250));

// Lock before you update a shared value.
status_shared.lock().unwrap().jobs_done += 1;
// ^^^^^^^^^^^^^^^^
});
handles.push(handle);
}

// Waiting for all jobs to complete.
for handle in handles {
handle.join().unwrap();
}

println!("Jobs done: {}", status.lock().unwrap().jobs_done);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
}

这样看我的读写锁写法似乎有点别扭,中间的 guard 是不是多余了,直接链式调用不行吗:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fn main() {
let status = Arc::new(RwLock::new(JobStatus { jobs_done: 0 }));
// ^^^^^^^^^^^ ^

let mut handles = Vec::new();
for _ in 0..10 {
let status_shared = Arc::clone(&status);
let handle = thread::spawn(move || {
thread::sleep(Duration::from_millis(250));

status_shared.write().unwrap().jobs_done += 1;
// ^^^^^^^^^^^^^^^^^
});
handles.push(handle);
}

for handle in handles {
handle.join().unwrap();
}

println!("Jobs done: {}", status.read().unwrap().jobs_done);
// ^^^^^^^^^^^^^^^^
}

试了一下,还真行……

我记错了,是另一处不行:

1
2
3
4
5
6
7
8
9
10
11
12
13
pub type SharedConfig = RwLock<AppConfig>;

...

let config = self.app_handle.state::<SharedConfig>();
let config_guard = config.read().await;
self.calculate_next_event(&config_guard)

...

let config = self.app_handle.state::<SharedConfig>();
let mut config_guard = config.write().await;
*config_guard = new_config;

这里用的是 Tokio 异步锁,这些不能链式调用写成:

1
2
3
4
5
6
7
let config = self.app_handle.state::<SharedConfig>().read().await;
self.calculate_next_event(&config)

...

let config = self.app_handle.state::<SharedConfig>().write().await;
*config = new_config;

这是因为 self.app_handle.state::<SharedConfig>() 返回的是一个临时值,作为一个智能指针拥有对实际数据的引用。

read().await 完成并赋值给 config_guard 后,这个临时值就被认为不再需要了,因此它会立即被丢弃。

但是,config_guard 内部却借用了被这个临时值所拥有的 RwLock。结果就是 config_guard 还在借用,但它借用的对象(临时值)已经被丢弃了,这就造成了悬垂引用,从而报错:

1
2
temporary value dropped while borrowed
consider using a let binding to create a longer lived value

解决方法正如编译器提示所说的,可以使用 let 绑定将临时值存储在一个变量中,从而延长其生命周期,即上面的写法。

下面可以先来看看互斥锁的一部分实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pub struct Mutex<T: ?Sized> {
inner: sys::Mutex,
poison: poison::Flag,
data: UnsafeCell<T>,
}

...

pub(crate) struct Flag {
#[cfg(panic = "unwind")]
failed: Atomic<bool>,
}

...

pub struct UnsafeCell<T: ?Sized> {
value: T,
}

sys::Mutex 待会具体涉及,是具体平台相关的互斥锁实现。

poison::Flag 是用来标记互斥锁是否被「污染」了,即在持有锁的线程 panic 了。#[cfg(panic = "unwind")] 属性表示,只有当 Rust 编译器被配置为使用「栈展开」(unwind)策略来处理 panic 时,下面的代码才会被编译。即如果当前的 Rust 编译配置是使用 unwind 策略来处理 panic,那么就在 Flag 结构体中包含一个名为 failedAtomic<bool> 字段。

UnsafeCell<T> 是一个允许在 Rust 的安全代码中进行内部可变性的容器。是 Rust 中唯一一个可以安全地在不可变引用 &T 后修改其内部值的类型。

然后是 lock() 方法:

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
impl<T: ?Sized> Mutex<T> {
/// Acquires a mutex, blocking the current thread until it is able to do so.
pub fn lock(&self) -> LockResult<MutexGuard<'_, T>> {
unsafe {
self.inner.lock();
MutexGuard::new(self)
}
}

...
}

...

/// A type alias for the result of a lock method which can be poisoned.
pub type LockResult<T> = Result<T, PoisonError<T>>;

...

/// A type of error which can be returned whenever a lock is acquired.
pub struct PoisonError<T> {
data: T,
#[cfg(not(panic = "unwind"))]
_never: !,
}

这里涉及了 MutexGuard,下面还有 Drop 的实现,以实现解锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// An RAII implementation of a "scoped lock" of a mutex. When this structure is
/// dropped (falls out of scope), the lock will be unlocked.
pub struct MutexGuard<'a, T: ?Sized + 'a> {
lock: &'a Mutex<T>,
poison: poison::Guard,
}

impl<T: ?Sized> Drop for MutexGuard<'_, T> {
#[inline]
fn drop(&mut self) {
unsafe {
self.lock.poison.done(&self.poison);
self.lock.inner.unlock();
}
}
}

接下来是深入 sys::Mutex,以 Windows 为例(看了一下,Linux 也差不多,看到的其中一个区别是 State Windows 上是 u8,Linux 上是 u32),为了简洁删掉了部分上下文相关性较弱的属性、方法与代码:

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
63
64
65
66
type Futex = sys::futex::SmallFutex;
type State = sys::futex::SmallPrimitive;

pub struct Mutex {
futex: Futex,
}

const UNLOCKED: State = 0;
const LOCKED: State = 1; // locked, no other threads waiting
const CONTENDED: State = 2; // locked, and other threads waiting (contended)

impl Mutex {
pub const fn new() -> Self {
Self { futex: Futex::new(UNLOCKED) }
}

pub fn lock(&self) {
if self.futex.compare_exchange(UNLOCKED, LOCKED, Acquire, Relaxed).is_err() {
self.lock_contended();
}
}

fn lock_contended(&self) {
// Spin first to speed things up if the lock is released quickly.
let mut state = self.spin();

// If it's unlocked now, attempt to take the lock
// without marking it as contended.
if state == UNLOCKED {
match self.futex.compare_exchange(UNLOCKED, LOCKED, Acquire, Relaxed) {
Ok(_) => return, // Locked!
Err(s) => state = s,
}
}

loop {
// Put the lock in contended state.
// We avoid an unnecessary write if it as already set to CONTENDED,
// to be friendlier for the caches.
if state != CONTENDED && self.futex.swap(CONTENDED, Acquire) == UNLOCKED {
// We changed it from UNLOCKED to CONTENDED, so we just successfully locked it.
return;
}

// Wait for the futex to change state, assuming it is still CONTENDED.
futex_wait(&self.futex, CONTENDED, None);

// Spin again after waking up.
state = self.spin();
}
}

pub unsafe fn unlock(&self) {
if self.futex.swap(UNLOCKED, Release) == CONTENDED {
// We only wake up one thread. When that thread locks the mutex, it
// will mark the mutex as CONTENDED (see lock_contended above),
// which makes sure that any other waiting threads will also be
// woken up eventually.
self.wake();
}
}

fn wake(&self) {
futex_wake(&self.futex);
}
}

看着有点熟悉,下面是上学期学的 C 版本互斥锁实现

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
#define UNLOCK 0   // 锁空闲
#define ONE_HOLD 1 // 锁被单个线程持有
#define WAITERS 2 // 锁被持有,(可能)有线程在等待

void mutex_lock(spinlock_t *lk) {
// 尝试获取锁
// 如果 lk 的值是 UNLOCK,则将其设为 ONE_HOLD,返回原来的值
int c = cmpxchg(lk, UNLOCK, ONE_HOLD);
if (c != UNLOCK) {
// 锁已被持有
do {
if (c == WAITERS || cmpxchg(lk, ONE_HOLD, WAITERS) != UNLOCK) {
// 等待锁
futex_wait(lk, WAITERS);
}
} while ((c = cmpxchg(lk, UNLOCK, WAITERS)) != UNLOCK);
}
}

void mutex_unlock(spinlock_t *lk) {
// 减少等待计数
if (atomic_dec(lk) != ONE_HOLD) {
// 有等待者
*lk = UNLOCK;
futex_wake(lk);
}
}

回到上面的标准库实现,先看 unlock,可以看出来两个版本是一样的。标准库版本尝试将状态设置为 UNLOCKED,如果之前的状态是 CONTENDED,则调用 wake 唤醒一个等待线程。而 C 版本则是先将状态减一,如果结果不是 ONE_HOLD,说明之前是 WAITERS,则将状态设为 UNLOCK 并调用 futex_wake

lock 中首先尝试通过 compare_exchange 将状态从 UNLOCKED 改为 LOCKED,如果成功则直接返回,否则调用 lock_contended 处理竞争。这跟上面的 C 版本类似,先尝试将 lkUNLOCK 改为 ONE_HOLD,成功则直接返回,否则进入 do-while 循环。

在标准库的实现中,为了优化锁快速释放的情形,lock_contended 先调用 spin 进行自旋,具体的 spin 实现就不贴出来了。根据代码实现,自旋次数是 100,然后用一个 loop 来检查状态是否不再是 LOCKED,如果是就继续自旋,否则返回当前状态。

与 C 版本不同的是,标准库版本在自旋后,如果状态变成了 UNLOCKED,会再次尝试在不标记为竞争的情况下获取锁。

标准库版本将成功获取锁放在了 if 内,而 C 版本则放在了 do-while 循环和 if 的条件中。

感觉这个标准库的版本更好,C 版本的尝试将锁标记为竞争状态用了两个 cmpxchg,而标准库版本只用了一次 swap,只是尝试将锁标记为竞争状态,并检查之前是否锁已经被释放,更清晰就是了。

读写锁就不贴了,看了几眼比较陌生。

21 宏

没怎么考宏定义,因此比较简单。

第 3 题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// TODO: Fix the compiler error without taking the macro definition out of this
// module.
mod macros {
#[macro_export]
macro_rules! my_macro {
() => {
println!("Check out my macro!");
};
}
}

fn main() {
my_macro!();
}

导出宏用 #[macro_export] 属性,之前正好见过,不然我也不知道咋整。

第 4 题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// TODO: Fix the compiler error by adding one or two characters.
#[rustfmt::skip]
macro_rules! my_macro {
() => {
println!("Check out my macro!");
};
($val:expr) => {
println!("Look at this other macro: {}", $val);
}
}

fn main() {
my_macro!();
my_macro!(7777);
}

不同模式用 ; 分隔(第二个后面可加可不加)。这个是看了眼 todo! 的定义:

1
2
3
4
5
6
7
8
macro_rules! todo {
() => {
$crate::panicking::panic("not yet implemented")
};
($($arg:tt)+) => {
$crate::panic!("not yet implemented: {}", $crate::format_args!($($arg)+))
};
}

22 Clippy

第 2 题

1
2
3
4
5
6
7
8
9
10
fn main() {
let mut res = 42;
let option = Some(12);
// TODO: Fix the Clippy lint.
for x in option {
res += x;
}

println!("{res}");
}

改成 if-let:

1
2
3
if let Some(x) = option {
res += x;
}

看到 for 我第一反应是用 while-let,毕竟都是循环关键词。

为什么 Option 可以用 for 呢?因为 Option 实现了 IntoIterator,转换成迭代器后,Some 会变成一个元素的迭代器,None 会变成空迭代器。

用 while-let 的话,会导致死循环,因为一直匹配。除非写成:

1
2
3
4
5
let mut option = Some(12);
while let Some(x) = option {
res += x;
option = None;
}

第 3 题

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
// Here are some more easy Clippy fixes so you can see its utility 📎
// TODO: Fix all the Clippy lints.

#[rustfmt::skip]
#[allow(unused_variables, unused_assignments)]
fn main() {
let my_option: Option<&str> = None;
// Assume that you don't know the value of `my_option`.
// In the case of `Some`, we want to print its value.
if my_option.is_none() {
println!("{}", my_option.unwrap());
}

let my_arr = &[
-1, -2, -3
-4, -5, -6
];
println!("My array! Here it is: {my_arr:?}");

let my_empty_vec = vec![1, 2, 3, 4, 5].resize(0, 5);
println!("This Vec is empty, see? {my_empty_vec:?}");

let mut value_a = 45;
let mut value_b = 66;
// Let's swap these two!
value_a = value_b;
value_b = value_a;
println!("value a: {value_a}; value b: {value_b}");
}

标答如下:

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
use std::mem;

#[rustfmt::skip]
#[allow(unused_variables, unused_assignments)]
fn main() {
let my_option: Option<&str> = None;
// `unwrap` of an `Option` after checking if it is `None` will panic.
// Use `if-let` instead.
if let Some(value) = my_option {
println!("{value}");
}

// A comma was missing.
let my_arr = &[
-1, -2, -3,
-4, -5, -6,
];
println!("My array! Here it is: {my_arr:?}");

let mut my_empty_vec = vec![1, 2, 3, 4, 5];
// `resize` mutates a vector instead of returning a new one.
// `resize(0, …)` clears a vector, so it is better to use `clear`.
my_empty_vec.clear();
println!("This Vec is empty, see? {my_empty_vec:?}");

let mut value_a = 45;
let mut value_b = 66;
// Use `mem::swap` to correctly swap two values.
mem::swap(&mut value_a, &mut value_b);
println!("value a: {value_a}; value b: {value_b}");
}

分别是 if-let,.clear()mem::swap

第一个我有点晕,写成了:

1
2
3
if my_option.is_some() {
println!("{:?}", my_option);
}

笨啊!

23 转换

这个有跟智能指针一样的问题。

from into

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#[derive(Debug)]
struct Person {
name: String,
age: u8,
}

// We implement the Default trait to use it as a fallback when the provided
// string is not convertible into a `Person` object.
impl Default for Person {
fn default() -> Self {
Self {
name: String::from("John"),
age: 30,
}
}
}

// TODO: Complete this `From` implementation to be able to parse a `Person`
// out of a string in the form of "Mark,20".
impl From<&str> for Person {
fn from(s: &str) -> Self {}
}

我的超绝丑陋解法:

1
2
3
4
5
6
7
8
9
10
11
12
let result: Vec<_> = s.split(",").collect();
if result.len() != 2
|| result.first().unwrap().is_empty()
|| result.get(1).unwrap().parse::<u8>().is_err()
{
Person::default()
} else {
Person {
name: result.first().unwrap().to_string(),
age: result.get(1).unwrap().parse::<u8>().unwrap(),
}
}

主要是为了将默认分支合并了,这会导致非常丑陋的重复调用。另外 .get(0) 根据 Clippy 提示改成了 .first()

看了眼 first() 方法定义,有点惊讶:

1
2
3
4
5
6
impl<T> [T] {
/// Returns the first element of the slice, or `None` if it is empty.
pub const fn first(&self) -> Option<&T> {
if let [first, ..] = self { Some(first) } else { None }
}
}

居然不是用 self.get(0) 实现的,而是用模式匹配。

标答用了模式匹配,有点聪明啊,我怎么想不到呢:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let mut split = s.split(',');
let (Some(name), Some(age), None) = (split.next(), split.next(), split.next()) else {
// ^^^^ there should be no third element
return Self::default();
};

if name.is_empty() {
return Self::default();
}

let Ok(age) = age.parse() else {
return Self::default();
};

Self {
name: name.into(),
age,
}

try from into

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
#[derive(Debug, PartialEq)]
struct Color {
red: u8,
green: u8,
blue: u8,
}

// We will use this error type for the `TryFrom` conversions.
#[derive(Debug, PartialEq)]
enum IntoColorError {
// Incorrect length of slice
BadLen,
// Integer conversion error
IntConversion,
}

// TODO: Tuple implementation.
// Correct RGB color values must be integers in the 0..=255 range.
impl TryFrom<(i16, i16, i16)> for Color {
type Error = IntoColorError;

fn try_from(tuple: (i16, i16, i16)) -> Result<Self, Self::Error> {}
}

// TODO: Array implementation.
impl TryFrom<[i16; 3]> for Color {
type Error = IntoColorError;

fn try_from(arr: [i16; 3]) -> Result<Self, Self::Error> {}
}

// TODO: Slice implementation.
// This implementation needs to check the slice length.
impl TryFrom<&[i16]> for Color {
type Error = IntoColorError;

fn try_from(slice: &[i16]) -> Result<Self, Self::Error> {}
}

这个写得更是一坨:

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
fn try_from(tuple: (i16, i16, i16)) -> Result<Self, Self::Error> {
if (0..=255).contains(&tuple.0)
&& (0..=255).contains(&tuple.1)
&& (0..=255).contains(&tuple.2)
{
Ok(Self {
red: tuple.0 as u8,
green: tuple.1 as u8,
blue: tuple.2 as u8,
})
} else {
Err(IntoColorError::IntConversion)
}
}

...

fn try_from(arr: [i16; 3]) -> Result<Self, Self::Error> {
if arr.iter().all(|x| (0..=255).contains(x)) {
Ok(Self {
red: *arr.first().unwrap() as u8,
green: *arr.get(1).unwrap() as u8,
blue: *arr.get(2).unwrap() as u8,
})
} else {
Err(IntoColorError::IntConversion)
}
}

...

fn try_from(slice: &[i16]) -> Result<Self, Self::Error> {
if slice.len() != 3 {
Err(IntoColorError::BadLen)
} else if slice.iter().all(|x| (0..=255).contains(x)) {
Ok(Self {
red: *slice.first().unwrap() as u8,
green: *slice.get(1).unwrap() as u8,
blue: *slice.get(2).unwrap() as u8,
})
} else {
Err(IntoColorError::IntConversion)
}
}

好歹记得 Range ,表扬一下吧……

标答用模式匹配用得好,我好像顺着思维,没想到用 u8::try_from 来做转换:

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
fn try_from(tuple: (i16, i16, i16)) -> Result<Self, Self::Error> {
let (Ok(red), Ok(green), Ok(blue)) = (
u8::try_from(tuple.0),
u8::try_from(tuple.1),
u8::try_from(tuple.2),
) else {
return Err(IntoColorError::IntConversion);
};

Ok(Self { red, green, blue })
}

...

fn try_from(arr: [i16; 3]) -> Result<Self, Self::Error> {
Self::try_from((arr[0], arr[1], arr[2]))
}

...

fn try_from(slice: &[i16]) -> Result<Self, Self::Error> {
if slice.len() != 3 {
return Err(IntoColorError::BadLen);
}
Self::try_from((slice[0], slice[1], slice[2]))
}

还有使用了已经实现的 TryFrom 来简化代码,我是笨蛋……感觉可能受到了迭代器第 2 题的影响,于是就没有复用,直接造轮子,还是很丑的轮子。

as ref mut

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Obtain the number of bytes (not characters) in the given argument
// (`.len()` returns the number of bytes in a string).
// TODO: Add the `AsRef` trait appropriately as a trait bound.
fn byte_counter<T>(arg: T) -> usize {
arg.as_ref().len()
}

// Obtain the number of characters (not bytes) in the given argument.
// TODO: Add the `AsRef` trait appropriately as a trait bound.
fn char_counter<T>(arg: T) -> usize {
arg.as_ref().chars().count()
}

// Squares a number using `as_mut()`.
// TODO: Add the appropriate trait bound.
fn num_sq<T>(arg: &mut T) {
// TODO: Implement the function body.
}

这题最后一个 TODO,因为不太了解,一直照着编译器的改,于是越改越复杂,越改越错。最终答案如下,懒得讲了:

1
2
3
4
5
6
7
8
9
10
11
12
fn byte_counter<T: AsRef<str>>(arg: T) -> usize {
arg.as_ref().len()
}

fn char_counter<T: AsRef<str>>(arg: T) -> usize {
arg.as_ref().chars().count()
}

fn num_sq<T: AsMut<u32>>(arg: &mut T) {
let mut_arg = arg.as_mut();
*mut_arg *= *mut_arg;
}

最终就是做完了,好!