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`" } |
确实如此!
此外还看到一些默认值,默认的布尔值是 false,char 是 '\0',ascii::Char 是 ascii::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(即 u64 的 newtype),如果值是负数或者 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:?}"); } |
这个其实没什么,只是记录一下,u8 跟 i8 都可以转换成 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 Trait 或 Box<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 结构体中包含一个名为 failed 的 Atomic<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 版本类似,先尝试将 lk 从 UNLOCK 改为 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; } |
最终就是做完了,好!
