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,有非常多的方式,这里使用最简单的,可以直接插入字符串。

fn main() {
    println!("Hello {}!", "QRZ");
}

variables1

难点是 x 赋值,用 let 赋值即可。

variables2

本以为是没有给变量类型,后来发现是没有初始化。

variables3

变量没有添加 mut 可变字段

variables4

虽然给了变量类型,但是没有初始化。

variables5

开始的时候给参数的类型是 str,下面变成了 int,我应该改名字吗?

不用,参考 https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#shadowing,给下面的变量加上 let 关键字就可以复用变量名称了。

variables6

报错信息提示很明显,给 const 的值要加变量类型。

functions1

函数没定义,我们定义一个空函数 call_me() 即可。

functions2

函数参数需要指定类型。

functions3

调用带参数的函数需要给出参数值,不能为空。

functions4

需要定义函数的返回值。

functions5

返回值要么加上 return 关键字,要么就被加分号。否则的话类型是 ()

if1

实现一个比大小函数。

    if a > b {
        a
    }
    else {
        b
    }

我这代码写得磕磕绊绊(笑

if2

第一个报错的原因是返回值不一样,接下来实现 test 的函数即可。

quiz1

一个简单的函数

fn calculate_apple_price (num: i32) -> i32 {
    if num > 40 {
        num
    } else {
        num * 2
    }
}

move_semantics1

尽管 vec0 不是 mut 的,但是在 fill_vec 函数中控制权被转移了。返回的值是 mut 的,因此 vec1 需要是 mut 的。

move_semantics2

该问题出现在 vec0 的所有权被转移到 vec1 了。hint 给了三种方法来修复:

  1. Make another, separate version of the data that’s in vec0 and pass that to fill_vec instead. 意思是说将 vec0 传给别的值(比如 vec2)再传给 fill_vec 函数。由于 Vec 没有实现 Copy trait,因此直接 let vec2=vec0 的话仍然会导致所有权的转移。想要实现完整的深复制,需要:
    let vec2 = (&vec0).to_vec();
    let mut vec1 = fill_vec(vec2);
  2. Make fill_vec borrow its argument instead of taking ownership of it, and then copy the data within the function in order to return an owned Vec<i32>. 意思时说让 fill_vec 函数借用这个参数而不是改变所有权,在这个函数中进行复制并返回带有所有权的 Vec<i32>,做如下修改:
    	let mut vec1 = fill_vec(&vec0);
    	......
     
    fn fill_vec(vec: &Vec<i32>) -> Vec<i32> {
        let mut vec = vec.to_vec();
  3. Make fill_vec mutably borrow its argument (which will need to be mutable), modify it directly, then not return anything. Then you can get rid of vec1 entirely — note that this will change what gets printed by the first println!. 意思是说让 fill_vec 函数 mutably 借用这个参数,但是这样的话 vec0 也需要是 mutable 的。 改的地方相比 2 来说并不多,但是为没理解,这是用在什么时候的 feature 呢
        let mut vec0 = Vec::new();
     
        let mut vec1 = fill_vec(&mut vec0);
     
        ......
     
    fn fill_vec(vec: &mut Vec<i32>) -> Vec<i32> {
        let mut vec = vec.to_vec();

move_semantics3

要求是不添加新行,在已有的行上修改。报错主要是参数不是 mutable 的。

这里只用了 vec1,我们不需要管 vec0 的所有权

还是没搞懂,看一下 hint,说的是

这个和 move_semantics2 的区别是 fill_vec 函数的第一行,上一个的第一行是一个转换: ` rust let mut vec = vec. to_vec (); 它将控制权转移并且使其可变。 而这道题目是没有的。我们可以把这一行加回来,也可以给某个位置加上mut` 关键字,修改当前存在的一个绑定,使其变成 mutable 的绑定。

那么加在哪里呢

忽然发现自己犯蠢了,直接加在函数参数里就可以了:

fn fill_vec(mut vec: Vec<i32>) -> Vec<i32> {

之前的参数是 immutable 的,我们可以加上 mut 关键字(而不必借用)。

move_semantics4

它直接把 fill_vec 函数的参数去掉了,还说:fill_vec() no longer takes vec: Vec<i32> as argument。有点没看懂,先看一下注释

重构 (Refactor) 这段代码,我们不再使用 vec0 并在 fn main 中创建向量,而是在 fn fill_vec 中创建它,并将新创建的向量从 fill_vec 传输给它的调用者。执行 rustlings hint move_semantics4 以获得提示!

咳咳,还是没看懂,看看 hint 吧

只要你觉得你有足够的方向,就停止阅读:) 或者尝试做一个步骤,然后修复导致的编译器错误!

只做一步?难道是把某个变量变成全局变量吗

哦。。行吧,我们在 fill_vec() 中使用 Vec::new() 创建 vec,然后删去 vec0 即可。

move_semantics5

    let mut x = 100;
    let y = &mut x;
    let z = &mut x;
    *y += 100;
    *z += 1000;
    assert_eq!(x, 1200);

参考 Mutable References,这里的问题和它类似,但是要求我们仅通过换行使编译通过。这里其实用到了 Rust 的性质,我们把 z 借用这一行和 *y 赋值的行交换即可。因为给 y 赋值之后不会再使用 y 了,此时就可以重新 mutable 借用 x 了。

move_semantics6

本题要求只改变引用,不修改其他值。报错原因是 get_char 函数改变的 data 的所有权,导致下面调用 string_uppercase 函数时出错。那么怎样才能在调用 get_char 时不改变 data 的所有权呢?借用。要做的事情很简单。我们首先看一下原始代码:

fn main() {
    let data = "Rust is great!".to_string();
 
    get_char(data);
 
    string_uppercase(&data);
}
 
// Should not take ownership
fn get_char(data: String) -> char {
    data.chars().last().unwrap()
}
 
// Should take ownership
fn string_uppercase(mut data: &String) {
    data = &data.to_uppercase();
 
    println!("{}", data);
}

我们要做的其实是和注释中说的一致:get_char 不应当获取所有权,我们将其修改为借用的版本,而 string_uppercase 需要获得所有权,我们将其改成直接传入的版本:

fn main() {
    let data = "Rust is great!".to_string();
 
    get_char(&data);
 
    string_uppercase(data);
}
 
// Should not take ownership
fn get_char(data: &String) -> char {
    data.chars().last().unwrap()
}
 
// Should take ownership
fn string_uppercase(mut data: String) {
    data = data.to_uppercase();
 
    println!("{}", data);
}

为什么 string_uppercase 需要获取所有权而不是借用呢?如果我们借用的话会怎样呢?恢复一下:

    string_uppercase(&data);
}
...
// Should take ownership
fn string_uppercase(mut data: &String) {
    data = &data.to_uppercase();
 
    println!("{}", data);
}

报错:

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(),自然会报错。那么我们不借用呢?也就是改成:

    string_uppercase(&data);
}
...
// Should take ownership
fn string_uppercase(mut data: &String) {
    data = data.to_uppercase();
 
    println!("{}", data);
}

新报错为

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 = &data.to_uppercase();

中,data 的类型是 &String,我们必须通过引用保持正确的参数。如果我们修改成

	let data = &data.to_uppercase();

	let data = data.to_uppercase();

都可以通过编译。如果我们打印一下此时 data 的类型:

fn print_type_of<T>(_: &T) {
    println!("{}", std::any::type_name::<T>())
}
 
// Should take ownership
fn string_uppercase(mut data: &String) {
    print_type_of(&data);
    let data = data.to_uppercase();
    print_type_of(&data);
    println!("{}", data);
}

会发现 let 前后 data 的类型分别为

&alloc::string::String
alloc::string::String

而借用版本的则为

fn string_uppercase(mut data: &String) {
    print_type_of(&data);
    let data = &data.to_uppercase();
    print_type_of(&data);
    ...
// &alloc::string::String
// &alloc::string::String

这个其实有点意思,为什么和上面的不大一样,是因为如果没有 let 的话,data.to_uppercase() 的生命周期到重新赋值给 data 为止。而 let 之后它的生命周期独立,到函数结束为止,因此能够正常编译。

primitive_types1

补充变量即可,这里学到的是 Bool 类型

primitive_types2

同上,这里学到的是 Char 类型,且对于 char,有几个很方便判断的函数:

ch.is_alphabetic();

会判断是否在字母表中

ch.is_numeric();

会判断是否在数字中

primitive_types3

这里是创建数组的方法,我们可以通过

let a = ["100"; 100];

创建长度为 100 的字符串数组

primitive_types4

这道题会学习切片。

let nice_slice = &a[1..4];

会对 a 切片,从 1 开始,到 3 截止,共 3 个元素。

primitive_types5

这道题会学习元组。

fn main() {
    let cat = ("Furry McFurson", 3.5);
    let (name, age) = cat;
 
    println!("{} is {} years old.", name, age);
}

这个用法很符合直觉。

primitive_types6

这道题学习了元组中元素的 index

fn indexing_tuple() {
    let numbers = (1, 2, 3);
    // Replace below ??? with the tuple indexing syntax.
    let second = numbers.1;
 
    assert_eq!(2, second,
        "This is not the 2nd number in the tuple!")
}

嗯,用法很新颖。

structs1

学习结构体的构造和使用。有传统的类似 C 的构造和元组方式的构造,还有一种被称为 Unit-Like 结构体,它可以没有任何结构,我们可以直接调用。

structs2

这里学习使用 update 语法。相对于 order_template,只有 name 和 count 字段有变化,因此可以简写为

        let your_order = Order {
            name: String::from("Hacker in Rust"),
            count: 1,
            ..order_template
        };

structs3

学习使用 panic! 宏,以及一些简单的实现

enums1

学习 enum 的定义

enums2

学习 enums 中不同变量的定义,参考 Enum Values

enums3

学习 match 的用法,参考 The match Control Flow Construct

    fn process(&mut self, message: Message) {
        // TODO: create a match expression to process the different message variants
        match message {
            Message::ChangeColor(color) => self.change_color(color),
            Message::Echo(s) => self.echo(s),
            Message::Move(p) => self.move_position(p),
            Message::Quit => self.quit()
        }
    }

这个参数的写法我之前还真没注意。

modules1

Rust 默认所有的结构体中的函数/模块都是 private 的,我们可以通过 pub 关键字使其 public,参考 Making Structs and Enums Public

modules2

这道题目目的是学会 use ... as ... 的使用,默认也是 private 的,我们可以添加 pub 关键字使其 public。

    // TODO: Fix these use statements
    pub use self::fruits::PEAR as fruit;
    pub use self::veggies::CUCUMBER as veggie;

modules3

注释告诉我们,可以用 use 关键字从任何地方导入 modules,尤其是 Rust 标准库到我们项目的区域(scope)中,这里要求我们导入 SystemTimeUNIX_EPOCH。查询文档 Constant std::time::UNIX_EPOCH 可以直接找到导入方法:

use std::time::{SystemTime, UNIX_EPOCH};

我没看懂为什么 Err 的话报错 "SystemTime before UNIX EPOCH!"

vec1

要求我们用 vec! 宏初始化数组

let v = vec![10, 20, 30, 40]

看了一下 hint,第二种方法是先 Vec::new() 创建一个 vector 然后 push 填充。肯定是比宏麻烦的,宏的实现是否就是第二种方法的简写呢?看一下 vec! 宏的实现。

vec2

看一下 test 都干了啥:

首先定义 v:

let v: Vec<i32> = (1..).filter(|x| x % 2 == 0).take(5).collect();

这代码写得。。根本看不懂。简单解析一下应该是从 1 开始取数字,需要满足要求(filter)x 能整除 2,然后取(take)5 个,并 collect 成数组。有关 collect 的用法见 method.collect。里面的介绍是将迭代器转换为 collection。这道题实际上是要求改变数组的值。对于上面的循环:

    for i in v.iter_mut() {
        // TODO: Fill this up so that each element in the Vec `v` is
        // multiplied by 2.
    }

i 指向的是 v 的可变引用(iter_mut() 会返回 &mut v),我们可以使用该迭代器修改当前的值。

    for i in v.iter_mut() {
        // TODO: Fill this up so that each element in the Vec `v` is
        // multiplied by 2.
        *i = *i*2;
    }

hashmap1

学习 Rust 中哈希表的使用。我们需要

use std::collections::HashMap;            // 引入
let mut basket = HashMap::new();          // 初始化
basket.insert(String::from("banana"), 2); // 插入

而对于它的键计数可以通过

basket.len();

对于它的值求和可以通过

basket.values().sum::<u32>()

hashmap2

这道题实际上是考察了 match 的用法,因为我们只需要匹配 Banana 和 Pineapple,因此其他的都不需要 match,可以用 _ => None 来放弃匹配。

    for fruit in fruit_kinds {
        // TODO: Put new fruits if not already present. Note that you
        // are not allowed to put any type of fruit that's already
        // present!
        match fruit {
            Fruit::Banana => basket.insert(Fruit::Banana, 6),
            Fruit::Pineapple => basket.insert(Fruit::Pineapple, 7),
            _ => None,
        };
    }

strings1

在不改变函数签名的条件下编译通过,编译错误报告给出了很明显的提示,返回值用 to_string() 转换即可。看了一下 hint,解释是我们返回的字符串生命周期和程序一样长,但是它是 &str,我们需要的是 String。我们还可以通过 String::from 创建字符串。

strings2

这里考察了 String 转换为 &str(string slice)的方法,很简单。

quiz2

这道题要求我们判断字符串的类型,要么是 String,要么是 &str

一些我不了解的函数:

errors1

Rust 可以通过 Option 结构描述错误信息

报错希望 Option<String>,但是实际上是 Result

看一下 hint。

OKErr 都是 Result 的变体。因此需要我们修改 generate_text 的返回值为 Result 而不是 Option。 为了完成修改,我们需要

  • 更新函数签名的返回值类型为 Result<String, String>,使得返回值可以为 OK(String)Err(String)
  • 修改函数体以返回相应的值

即可。

errors2

看一下注释:

一个小游戏,目前它的问题是: 完全没有处理错误(也没有处理胜利的情况) 我们需要做的是: 如果我们调用 parse 函数时参数字符串不是数字,就返回 ParseIntError,在这种情况下,我们立即从函数中返回错误并不会尝试乘或加 有两种实现方式,但是其中一种更短。

参考 Early returns,我们只需要 match 一下是否成功,在该返回的时候返回就行了:

    let qty = match qty {
        Ok(qty) => qty,
        Err(e) => return Err(e)
    };

如果不想针对错误进行匹配,就上上面那样的话,还有一种更简单的方法:参考 Introducing ?。只需要加一个 ? 符号就行。

    let qty = item_quantity.parse::<i32>()?;

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 函数的返回值是 (),而 ? 需求返回值是 ResultOption。我们可以修改函数返回值,但是对于 main 而言显然是有限制的。所幸 Rust 的实现中,main 可以返回 Result<(), E>。我们可以修改 main 的返回值并在最后加上 Ok(())

fn main() -> Result<(), ParseIntError> {
    let mut tokens = 100;
    let pretend_user_input = "8";
 
    let cost = total_cost(pretend_user_input)?;
 
    if cost > tokens {
        println!("You can't afford that many!");
    } else {
        tokens -= cost;
        println!("You now have {} tokens.", tokens);
    }
 
    Ok(())
}

errors4

有点没看懂要干啥。。看看 hint:

PositiveNonzeroInteger::new 总会创建一个新的实例(instance)并返回结果 Ok,它应当做一些检查,在检查失败时返回 Err,仅当检查确定一切正常时返回 Ok

所以我们在 new 函数中实现检查,小于 0 是报错负数,等于 0 时报错 0。

impl PositiveNonzeroInteger {
    fn new(value: i64) -> Result<PositiveNonzeroInteger, CreationError> {
        if value < 0 {
            Err(CreationError::Negative)
 
        } else if value == 0 {
            Err(CreationError::Zero)
        }
        else {
            Ok(PositiveNonzeroInteger(value as u64))
        }
    }
}

除了 if 外,参考 errors5 也可以使用 match 的版本:

impl PositiveNonzeroInteger {
    fn new(value: i64) -> Result<PositiveNonzeroInteger, CreationError> {
        match value {
            x if x < 0 => Err(CreationError::Negative),
            x if x == 0 => Err(CreationError::Zero),
            _ => Ok(PositiveNonzeroInteger(value as u64))
        }
    }
}

errors5

看 TODO 要求我们更新 main 的返回值就能使得这段代码能通过编译了。

看一下报错

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:

Tip

main 中会产生两种错误类型(ParseIntErrorCreationError),它们都通过 ? 传递。我们怎样在 main 中声明同时允许两种错误返回的情况呢? 还是参考 Where The ? Operator Can Be Used,在底层实现中,? 对错误值调用 From::from 将其转换成一个装箱(boxed)的 trait 对象,也就是 Box<dyn error::Error>,它是多态的,意味着很多不同类型的错误可以从同一个函数返回。所有的错误行为都是一样的,因为它们都实现了 error::Error trait(特征)。

这样我们可以通过修改返回值简单地实现:

fn main() -> Result<(), Box<dyn error::Error>>

更多细节可以看 Using Trait Objects That Allow for Values of Different Types,我觉得以后还会遇到,到时候再回顾吧。

errors6

首先慢慢地看注释:

使用类似 Box<dyn error::Error> 捕获所有错误类型并不推荐用于库文件代码,因为调用者(caller)可以希望根据错误类型做决定,而不是打印或进一步传播(propagate)。这里,我们定义一个通用的(custom)错误类型,让 caller 决定当我们的函数发生错误时干什么。

接下来看 Don't change anything below this line. 下面的内容

首先是一些基本定义,和前几道题目类似。

#[derive(PartialEq, Debug)]
struct PositiveNonzeroInteger(u64);
 
#[derive(PartialEq, Debug)]
enum CreationError {
    Negative,
    Zero,
}
 
impl PositiveNonzeroInteger {
    fn new(value: i64) -> Result<PositiveNonzeroInteger, CreationError> {
        match value {
            x if x < 0 => Err(CreationError::Negative),
            x if x == 0 => Err(CreationError::Zero),
            x => Ok(PositiveNonzeroInteger(x as u64))
        }
    }
}

看一看报错:

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 的源码和文档:

    #[inline]
    #[stable(feature = "rust1", since = "1.0.0")]
    pub fn map_err<
        F, 
        O: FnOnce(E) -> F
        >(self, op: O) -> Result<T, F> 
        {
        match self {
            Ok(t) => Ok(t),
            Err(e) => Err(op(e)),
        }
    }

在正确的时候直接传出不论,在错误的时候会调用参数,也就是函数 op,在这里就是 from_creation,用它来处理 error。看一下 error 的参数是 CreationError,那么 from_creation 的参数就是 fn from_creation(e: CreationError)

接下来通过 test 看返回值,以其中第二个 test 为例

    #[test]
    fn test_negative() {
        assert_eq!(
            parse_pos_nonzero("-555"),
            Err(ParsePosNonzeroError::Creation(CreationError::Negative))
        );
    }

Err 内部是 ParsePosNonzeroError::Creation(CreationError::Negative),也就是 ParsePosNonzeroError。综上,函数定义为

fn from_creation(e: CreationError) -> ParsePosNonzeroError

这样我们就能实现好函数了:

impl ParsePosNonzeroError {
    // TODO: add another error conversion function here.
    fn from_creation(e: CreationError) -> ParsePosNonzeroError {
        match e {
            CreationError::Negative => ParsePosNonzeroError::Creation(CreationError::Negative),
            CreationError::Zero => ParsePosNonzeroError::Creation(CreationError::Zero),
        }
    }
}

可以通过三个样例,没有通过的是

    #[test]
    fn test_parse_error() {
        // We can't construct a ParseIntError, so we have to pattern match.
        assert!(matches!(
            parse_pos_nonzero("not a number"),
            Err(ParsePosNonzeroError::ParseInt(_))
        ));
    }

我们可以在 x 执行完 parse 之后 match 一下。

首先写一个符合为自己直觉的版本:

    let x: i64 = s.parse().unwrap();
    let x = match x {
        Ok(x) => x,
        Err(e) => return Err(ParsePosNonzeroError::ParseInt(e))
    };

然而报错:

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 的代码,魔改了一番:

    let x = s.parse::<i64>();
    let x = match x {
        Ok(x) => x,
        Err(e) => return Err(ParsePosNonzeroError::ParseInt(e))
    };

在这种情况下,编译通过了。但是这显然不符合我们的需求:对比一下之前的 x:

let x: i64 = s.parse().unwrap();

首先看一下 parse 的文档parse 的定义是

pub fn parse<F>(&self) -> Result<F, <F as FromStr>::Err>

它还有一种可以自行推断类型的语法 ::<type>,这么说我们用 let x: i64 = s.parse()let x = s.parse::<i64>() 应该是相同的。那为什么上面还会报错呢?

看一下 hint 吧。。

TODO 要求更改的行的下方,有使用 Result 上的 map_err() 方法将一种类型的错误转换为另一种类型的示例,尝试在 parse()Result 上使用类似的东西。可以使用 ? 运算符从函数中提前返回,或者使用 match 表达式,或是其他方法。

哦。。我又实现了一个 from_parse 函数:

    fn from_parse(e: ParseIntError) -> ParsePosNonzeroError {
        match e {
            e => return ParsePosNonzeroError::ParseInt(e)
        }
    }

之后的调用如下:

    let x: i64 = s.parse()
        .map_err(ParsePosNonzeroError::from_parse)
        .unwrap();

此时可以编译通过,但是还是无法完成 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 方法了。

最终的代码为

    let x: i64 = s.parse::<i64>()
        .map_err(ParsePosNonzeroError::from_parse)?;

或者上面的

    let x = s.parse::<i64>();
    let x = match x {
        Ok(x) => x,
        Err(e) => return Err(ParsePosNonzeroError::ParseInt(e))
    };

都是正确的。

generics1

首先是 Vec 的类型为 String,那么下面的字符串就需要 .to_string(),还可以设置 Vec 的类型为 &str,就不需要修改下面的 push 了。

generics2

看一下注释:

强大的 wrapper 提供了存储正整数的能力,将它重写以支持 wrapping 任意值

嗯,还是没搞懂咋写,看看 hint 吧。

目前我们只能 wrap u32,也许我们可以以某种方式更新对该数据类型的显式引用? 如果还卡住的话,参考 https://doc.rust-lang.org/stable/book/ch10-01-syntax.html#in-method-definitions

哦,是 Rust 中的泛型。原来的代码是

struct Wrapper {
    value: u32,
}
 
impl Wrapper {
    pub fn new(value: u32) -> Self {
        Wrapper { value }
    }
}

修改后的代码是:

struct Wrapper<T> {
    value: T,
}
 
impl<T> Wrapper<T> {
    pub fn new(value: T) -> Self {
        Wrapper { value }
    }
}

感觉和 C++ 差不多?

generics3

首先简单地参考 generics2 实现一下泛型:

pub struct ReportCard<T> {
    pub grade: T,
    pub student_name: String,
    pub student_age: u8,
}
 
impl<T> ReportCard<T> {
    pub fn print(&self) -> String {
        format!("{} ({}) - achieved a grade of {}",
            &self.student_name, &self.student_age, &self.grade)
    }
}

接下来报错:

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 中添加约束即可:

use std::fmt::Display;
 
pub struct ReportCard<T> {
    pub grade: T,
    pub student_name: String,
    pub student_age: u8,
}
 
impl<T: Display> ReportCard<T> {
    pub fn print(&self) -> String {
        format!("{} ({}) - achieved a grade of {}",
            &self.student_name, &self.student_age, &self.grade)
    }
}

option1

看一下注释,我们可以修改任何位置,除了 print_number 的函数签名。该函数定义如下:

// you can modify anything EXCEPT for this function's signature
fn print_number(maybe_number: Option<u16>) {
    println!("printing: {}", maybe_number.unwrap());
}

第一处错误是类型不匹配,给数字添加 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> 会创建:

enum Option_u16 {
    Some(u16),
    None,
}

而初始化数组的操作 let mut numbers: [Option<u16>; 5]; 是否意味着值不确定而没有被初始化呢?看了一下 hint,确实如此

Tip

数组的值没有合理的默认值;使用前需要填写值。

option2

第一处参考 Concise Control Flow with if let,学习了 if let 表达式替代 match 的用法。

第二处参考 while let,注意到 vectorpop 会加一层 Option<T>,因此用了 Some(Some(integer))

option3

问题出现在所有权转移给了 p,之后再调用 y 就会出错了。

那么 match 可不可以借用呢?当然可以。我们有两种改法:

fn main() {
    let y: &Option<Point> = &Some(Point { x: 100, y: 200 });
 
    match y {
        Some(p) => println!("Co-ordinates are {},{} ", p.x, p.y),
        _ => println!("no match"),
    }
    y; // Fix without deleting this line.
}

fn main() {
    let y: Option<Point> = Some(Point { x: 100, y: 200 });
 
    match &y {
        Some(p) => println!("Co-ordinates are {},{} ", p.x, p.y),
        _ => println!("no match"),
    }
    y; // Fix without deleting this line.
}

第一种改法让 y 的类型变成 &Option<Point>,我们创建的 Some(Point { x: 100, y: 200 }) 的生命周期和 main 函数一致,借用自然也可以被借用。

第二种改法让我们匹配 y 的时候借用,借用自然不会改变 y 的所有权。

看了一下 hint,这其实是考察 ref 关键字,不过自从 Rust 2018 开始我们就可以使用 & 来引用了,用 & 的写法还是很符合直觉的。第三种改法如下所示:

fn main() {
    let y: Option<Point> = Some(Point { x: 100, y: 200 });
 
    match y {
        Some(ref p) => println!("Co-ordinates are {},{} ", p.x, p.y),
        _ => println!("no match"),
    }
    y; // Fix without deleting this line.
}

另外 &ref 还是有区别的:

  • & 表示 pattern 需要一个对对象的引用。因此,& 是上述 pattern 的一部分,&FooFoo 匹配的对象不同。
  • ref 表示想要引用一个未打包(unpacked)的值。它不被匹配(It is not matched against):Foo(ref foo)Foo(foo) 匹配相同的对象。

traits1

参考 Implementing a Trait on a Type 实现即可

traits2

我们需要为 Vector<String> 实现 traint。

实现方法和 trait1 类似(感谢编译器帮了大忙)

impl AppendBar for Vec<String> {
    fn append_bar(mut self) -> Self {
        self.push(String::from("Bar"));
        self
    }
}

看一下 hint 自己有没有遗漏的地方

注意到 trait 获取了 self 的所有权,返回了 Self

tests1

学习 assert!() 宏的使用。

test2

学习 assert_eq!() 宏的使用。

test3

简单测试,我们可以在 assert() 宏中使用单目运算符 !

quiz3

自己编写 test 的小 quiz。

Box1

首先看注释:

在编译时,Rust 需要知道一个类型会占用多少空间。这对于递归类型(recursive type)是有问题的,递归类型是可以将另一个相同类型的值作为自身一部分的类型。为了解决这个问题,我们可以使用 Box,它是一个用于堆上存储数据的智能指针,它也允许我们打包一个递归类型。

本实验值要实现的递归类型是 cons list,它是函数式编程语言中的一种常见(frequently)数据结构。cons list 中的每一项有两个元素:当前项的值和下一个项的值。最后一项的值为 Nil

  • 步骤一:在 enum 中使用 Box 让代码可以通过编译
  • 步骤二:通过移除 unimplemented!() 创建空的和非空的 cons list

感谢报错,它提示了我们接下来应该怎么做:

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_listnon_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> 引用计数智能指针 的一种重构,使得它可以安全的用于并发环境。

写完之后来理一下主要逻辑:

fn main() {
    let numbers: Vec<_> = (0..100u32).collect();
    let shared_numbers = Arc::new(numbers);// TODO
    let mut joinhandles = Vec::new();
 
    for offset in 0..8 {
        let child_numbers = Arc::clone(&shared_numbers); // TODO
        joinhandles.push(thread::spawn(move || {
            let sum: u32 = child_numbers.iter().filter(|n| *n % 8 == offset).sum();
            println!("Sum of offset {} is {}", offset, sum);
        }));
    }
    for handle in joinhandles.into_iter() {
        handle.join().unwrap();
    }
}

shared_numbers 通过 Arc 创建引用计数的 numbers,在下面的循环中,child_numbers 每通过 Arc clone 一次,shared_numbers 的引用计数就会加一。

在计算 sum 时,filter 会获取满足 *n % 8 == offset 的数字并求和,最终输出。为了避免创建 numbers 的 copy,我们需要在 main 线程中创建 child_numbers,而不是在子线程中。

iterators1

迭代器的基本知识。参考使用迭代器处理元素序列Trait std::iter::Iterator

我们可用 .iter() 生成迭代器并通过 .next() 使用,迭代器的结束是 None

iterators2

Step 1

对于 iters,如果我们消费了一个,那么再次使用的时候不会使用第一个了。参考下面这个例子:

#![allow(unused)]
pub fn capitalize_first(input: &str) {
    let mut c = input.chars();
    println!("{:?}", &c);
    match c.next() {
        None => String::new(),
        Some(first) => {
            println!("{:?}", &c);
        }
    };
}
 
fn main() {
    let s = "hello";
    capitalize_first(&s);
}

输出为

Chars(['h', 'e', 'l', 'l', 'o'])
Chars(['e', 'l', 'l', 'o'])

Step 2

参考 collect,比较简单。

Step 3

本来我是想参考 flat_map 的,但是有点没搞明白为什么怎么写都有问题。突发奇想按照 Step 2 的写,居然成功通过了。。看一下 hint,对此的解释是:

Tip

它和之前的解答惊人地相似。Collect 非常强大和通用,Rust 仅需要知道希望的类型。

等全做完之后可用整理一下为什么 flat_map 不能通过吧。

iterators3

Tip

这个练习比之前所有的都要大,相信你可以完成它。

接下来是任务

  1. 完成除法功能以通过前四个测试
  2. 完成 result_with_listlist_of_results 函数以通过剩余的测试

除法功能咋用 match_error 实现啊,我用 if else 实现了。

接下来看 result_with_list 的实现。原始实现为

// Complete the function and return a value of the correct type so the test passes.
// Desired output: Ok([1, 11, 1426, 3])
fn result_with_list() -> () {
    let numbers = vec![27, 297, 38502, 81];
    let division_results = numbers.into_iter().map(|n| divide(n, 27));
}

测试为

    #[test]
    fn test_result_with_list() {
        assert_eq!(format!("{:?}", result_with_list()), "Ok([1, 11, 1426, 3])");
    }

分析一下 division_results 的值为 [1, 11, 1426, 3],我们希望返回的值为 Ok([1, 11, 1426, 3])

哦,强大的 collect trait,两个函数通过简单的 collect 就都解决了。等有时间看一下 collect 到底是怎么实现的。

iterators4

要求我们实现一个阶乘(factorial)函数。不需要使用 return、循环和其他变量。Rust 提供了非常强的 API:

pub fn factorial(num: u64) -> u64 {
    (1..num+1).product()
}

仅需要一行就能实现。product() trait 实现了连乘的功能。

iterators5

看看注释:

Tip

让我们定义一个简单的模型以跟踪 Rustlings 练习的进程。这个进程会用哈希表建模,它的键是练习的名字,值是练习的进程。创建了两个计数函数以计算给定进程的练习数量。这些计数函数使用了至关重要的(imperative)循环风格。使用函数式的 iterators 重建这些计数。只需要修改 count_iteratorcount_collection_iterator 这两个迭代器方法。

边看文档边做题:Trait std::iter::Iterator,文档还是很有用的。

第一个是遍历 map 的 iter 并对满足要求的值计数。我用 filter 看是否满足要求,用 count 计数。

fn count_iterator(map: &HashMap<String, Progress>, value: Progress) -> usize {
    // map is a hashmap with String keys and Progress values.
    // map = { "variables1": Complete, "from_str": None, ... }
    map.values().filter(|val| *val == &value).count()
}

第二个的 map 夹在了 Vector slice 里,遍历到还简单,用 iter() + map() 计算每个 iter 中的值,最后用 sum 计数。map 实现的闭包直接拿前一个填充了。

fn count_collection_iterator(collection: &[HashMap<String, Progress>], value: Progress) -> usize {
    // collection is a slice of hashmaps.
    // collection = [{ "variables1": Complete, "from_str": None, ... },
    //     { "variables2": Complete, ... }, ... ]
    collection
        .iter()
        .map(|vals| vals.values().filter(|val| *val == &value).count())
        .sum()
}

threads1

看看注释:

Note

这道题目的想法是在第 22 行产生(spawned)的线程完成作业,而主线程会监视作业直到完成 10 个作业。因为产生的线程的睡眠时间和等待线程睡眠时间的区别,你会看到 6 行 “waiting…” 且程序没有运行到超时就终止了。

唔,不管怎么样,先看一下程序的逻辑吧。

fn main() {
    let status = Arc::new(JobStatus { jobs_completed: 0 });
    let status_shared = status.clone();
    thread::spawn(move || {
        for _ in 0..10 {
            thread::sleep(Duration::from_millis(250));
            status_shared.jobs_completed += 1;
        }
    });
    while status.jobs_completed < 10 {
        println!("waiting... ");
        thread::sleep(Duration::from_millis(500));
    }
}

status 是用 Arc 创建的,它支持原子操作。Spawn 参考 Function std::thread::spawn,它会产生一个新线程,并为其返回一个 JoinHandle

看看它的原始实现

#[stable(feature = "rust1", since = "1.0.0")]
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
    F: FnOnce() -> T,
    F: Send + 'static,
    T: Send + 'static,
{
    Builder::new().spawn(f).expect("failed to spawn thread")
}

spawn 函数的闭包和返回值都有约束:

  • 'static 意味着闭包和返回值必须具有整个程序执行的生命周期,因为线程的生命周期可能超过它们被创建的生命周期。

    如果线程及其返回值比它们的调用者存活时间更长,我们需要确保它们在之后也是有效的。由于我们不知道它们什么时候返回,因此我们需要让它尽可能长时间地有效,也就是直到程序结束,因此是 'static 的生命周期。

  • Send 约束是因为闭包需要从产生它的线程按值传递给新线程。它的返回值需要从新线程传递到它加入的线程。注意,Send trait 表示从线程传递到线程是安全的。 Sync 表示在线程之间传递引用是安全的。

在该线程中会执行十次 sleep(250ms),每次会对某个计数器加一。最开始的错误也出现在这里:

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:

Tip

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

这里考察了宏的性质:

Warning

宏和函数的最后一个重要的区别是:在一个文件里调用宏 之前 必须定义它,或将其引入作用域,而函数则可以在任何地方定义和调用。

macros3

这个练习考察了宏的作用域。可用参考 Macros By Example 中的 The macro_use attribute 通过添加 macro_use 属性解决。它有两个用处:

  1. 首先,它可以用于使模块的宏范围在模块关闭时不会结束,方法是将其应用于模块:
  2. 其次,它可用于从另一个 crate 导入宏,方法是将其附加到 crate 根模块中出现的 extern crate 声明。

macros4

看上去缺个分号,补充上去果然编译通过了。主要考察的是宏的格式。

macro_rules! my_macro {
    () => {
        println!("Check out my macro!");
    }
    ($val:expr) => {
        println!("Look at this other macro: {}", $val);
    }
}

宏的定义为

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 会检查是否对 OptionResult 值进行了循环(可能是因为没必要?)

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。

首先实现了一个正常版本的

        if s.len() <= 0 {
            Person::default()
        } else {
            let sp: Vec<_> = s.split(",").collect();
            if sp.len() != 2 || sp[0].len() <= 0 {
                Person::default()
            } else {
                let age = sp[1].parse::<usize>();
                Person {
                    name: sp[0].to_string(),
                    age: match age {
                        Ok(age) => age,
                        _ => return Person::default(),
                    }
                }
            }
        }

然后是稍微不正常版本的:

        match s.len() {
            x if x <= 0 => Person::default(),
            _ => {
                let sp: Vec<_> = s.split(",").collect();
                if sp.len() != 2 || sp[0].len() <= 0 {
                    Person::default()
                } else {
                    let age = sp[1].parse::<usize>();
                    Person {
                        name: sp[0].to_string(),
                        age: match age {
                            Ok(age) => age,
                            _ => return Person::default(),
                        },
                    }
                }
            }
        }

最后是我写完之后觉得醍醐灌顶的:

        match s.len() {
            x if x <= 0 => Person::default(),
            _ => {
                match s.split(",").collect::<Vec<_>>() {
                    sp if sp.len() != 2 || sp[0].len() <= 0 => Person::default(),
                    sp => {
                        match sp[1].parse::<usize>() {
                            Ok(age) => Person {
                                name: sp[0].to_string(),
                                age: age,
                            },
                            _ => Person::default()
                        }
                    }
                }
            }
        }

from_str

Tip

它和 from_into 类似,但是在这里我们会实现 FromStr 并返回 errors 而不是返回一个默认值。另外,在实现 FromStr 时,你可以在 strings 上使用 parse 方法以生成实现者类型的对象。

不得不说 match 是真的好用。如果我们希望用其他方法的化,hint 也给了提示:

Tip

我们可以将 Resultmap_err 方法与函数或闭包一起使用,以包装 parse::<usize> 的错误

Tip

如果希望用 ? 传播错误,可以参考 https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reenter_question_mark.html

try_from_into

Tip

TryFrom 是简单且安全的类型转换,它会在某些情况下以可控的方式失败。

在通常状况下,它和 From 是一致的。主要的区别是它会返回一个 Result 类型而不是目标类型

用一种非常鱼唇的方法解决了:

// Tuple implementation
impl TryFrom<(i16, i16, i16)> for Color {
    type Error = IntoColorError;
    fn try_from(tuple: (i16, i16, i16)) -> Result<Self, Self::Error> {
        if tuple.0 >= 0
            && tuple.0 < 256
            && tuple.1 >= 0
            && tuple.1 < 256
            && tuple.2 >= 0
            && tuple.2 < 256
        {
            Ok(Self {
                red: tuple.0 as u8,
                green: tuple.1 as u8,
                blue: tuple.2 as u8,
            })
        } else {
            Err(IntoColorError::IntConversion)
        }
    }
}
 
// Array implementation
impl TryFrom<[i16; 3]> for Color {
    type Error = IntoColorError;
    fn try_from(arr: [i16; 3]) -> Result<Self, Self::Error> {
        if arr[0] >= 0 && arr[0] < 256 && arr[1] >= 0 && arr[1] < 256 && arr[2] >= 0 && arr[2] < 256
        {
            Ok(Self {
                red: arr[0] as u8,
                green: arr[1] as u8,
                blue: arr[2] as u8,
            })
        } else {
            Err(IntoColorError::IntConversion)
        }
    }
}
 
// Slice implementation
impl TryFrom<&[i16]> for Color {
    type Error = IntoColorError;
    fn try_from(slice: &[i16]) -> Result<Self, Self::Error> {
        if slice.len() != 3 {
            Err(IntoColorError::BadLen)
        } else if slice[0] >= 0
            && slice[0] < 256
            && slice[1] >= 0
            && slice[1] < 256
            && slice[2] >= 0
            && slice[2] < 256
        {
            Ok(Self {
                red: slice[0] as u8,
                green: slice[1] as u8,
                blue: slice[2] as u8,
            })
        } else {
            Err(IntoColorError::IntConversion)
        }
    }
}

看了一眼 hint,我这确实是比较麻烦的写法:

Tip

看官方文档的示例,https://doc.rust-lang.org/std/convert/trait.TryFrom.html

TryFrom 标准库中是否有一种实现,即可以做数字转换又能够检查输出的范围?

你可以用 Resultmap_error 方法转换错误。

如果希望用 ? 传播错误,可以参考 https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reenter_question_mark.html

挑战:你是否可以让 TryFrom 在许多整数类型上通用?

as_ref_mut

[! tip]

AsRefAsMut 允许廉价的引用到引用的转换。更多内容参考 https://doc.rust-lang.org/std/convert/trait.AsRef.htmlhttps://doc.rust-lang.org/std/convert/trait.AsMut.html

本练习是学习了 AsRef 的使用。正如参考内容的 examples 中写的那样:

Quote

通过使用特征边界(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 函数实现。

fn main() -> Result<(), Box<dyn Error>> {
    println!("{:?}", "Hong Kong,1999,25.7".parse::<Climate>()?);
    println!("{:?}", "".parse::<Climate>()?);
    Ok(())
}

它将字符串通过 from_str(在 from_str 中有过介绍)转换成 Climate 类型,然后通过 ? 处理错误。这里只实现了一个相关的 From trait:

// This `From` implementation allows the `?` operator to work on
// `ParseIntError` values.
impl From<ParseIntError> for ParseClimateError {
    fn from(e: ParseIntError) -> Self {
        Self::ParseInt(e)
    }
}

这段代码为 ParseClimateError 实现了 From<ParseIntError>。我们可以仿照这个例子实现下面的 From<ParseFloatError>

不过这和 main 函数无法通过编译无关,main 函数的报错为:

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,让其他错误可以包裹这个错误类型。

impl Error for ParseClimateError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        None
    }
}

看看接下来的报错:

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 中 fmtmatch cover 相应的 pattern 即可。这样 main 函数就通过编译了,而有两个测试尚未通过。这里需要我们去完成 Climate 中的 from_str

impl FromStr for Climate {
    type Err = ParseClimateError;
    // TODO: Complete this function by making it handle the missing error
    // cases.
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let v: Vec<_> = s.split(',').collect();
        let (city, year, temp) = match &v[..] {
            [city, year, temp] => match city.len() {
                0 => return Err(ParseClimateError::NoCity),
                _ => (city.to_string(), year, temp),
            },
            _ => match v.len() {
                1 => return Err(ParseClimateError::Empty),
                _ => return Err(ParseClimateError::BadLen),
            } 
        };
        let year: u32 = year.parse()?;
        let temp: f32 = temp.parse()?;
        Ok(Climate { city, year, temp })
    }
}

完成之后,4.7.1 版本的 rustlings 就告一段落了。