100 Exercises To Learn Rust 挑战!if・Panic・演练

之前的文章

【0】准备
【1】构文・整数・变量 ← 上回
【2】 if・Panic・演练 ← 本次

这是" 100 Exercise To Learn Rust"的第2次练习!本次的主题包括 if 表达式、panic 机制,以及对前面内容的总结练习。

本次相关的页面如下:

[02_basic_calculator/03_if_else](https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/02_basic_calculator/03_if_else/src/lib.rs "02_basic_calculator/03_if_else") if式

问题如下:

rust 复制代码
/// Return `true` if `n` is even, `false` otherwise.
fn is_even(n: u32) -> bool {
    todo!()
}

#[cfg(test)]
mod tests {
    use crate::is_even;

    #[test]
    fn one() {
        assert!(!is_even(1));
    }

    #[test]
    fn two() {
        assert!(is_even(2));
    }

    #[test]
    fn high() {
        assert!(!is_even(231));
    }
}

要求返回 n 是偶数时为 true,n 是奇数时为 false。

解释

如果忘记这是在用 Rust,而是直接使用 if 来编写的话,可能会像这样写吧?

rust 复制代码
fn is_even(n: u32) -> bool {
    let result;

    if n % 2 == 0 {
        result = true;
    } else {
        result = false;
    }

    return result;
}

不过,这段源码实在让人忍不住想吐槽很多地方。正好借此机会,我们可以尝试使用一个叫 Clippy 的 linter 工具。

rust 复制代码
cargo clippy
rust 复制代码
    Checking if_else v0.1.0 (/path/to/100-exercises-to-learn-rust/exercises/02_basic_calculator/03_if_else)
warning: function `is_even` is never used
 --> exercises/02_basic_calculator/03_if_else/src/lib.rs:2:4
  |
2 | fn is_even(n: u32) -> bool {
  |    ^^^^^^^
  |
  = note: `#[warn(dead_code)]` on by default

warning: unneeded `return` statement
  --> exercises/02_basic_calculator/03_if_else/src/lib.rs:11:5
   |
11 |     return result;
   |     ^^^^^^^^^^^^^
   |
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_return
   = note: `#[warn(clippy::needless_return)]` on by default
help: remove `return`
   |
11 -     return result;
11 +     result
   |

warning: unneeded late initialization
 --> exercises/02_basic_calculator/03_if_else/src/lib.rs:3:5
  |
3 |     let result;
  |     ^^^^^^^^^^^
  |
  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_late_init
  = note: `#[warn(clippy::needless_late_init)]` on by default
help: declare `result` here
  |
5 |     let result = if n % 2 == 0 {
  |     ++++++++++++
help: remove the assignments from the branches
  |
6 ~         true
7 |     } else {
8 ~         false
  |
help: add a semicolon after the `if` expression
  |
9 |     };
  |      +

warning: this if-then-else expression assigns a bool literal
 --> exercises/02_basic_calculator/03_if_else/src/lib.rs:5:5
  |
5 | /     if n % 2 == 0 {
6 | |         result = true;
7 | |     } else {
8 | |         result = false;
9 | |     }
  | |_____^ help: you can reduce it to: `result = n % 2 == 0;`
  |
  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_bool_assign
  = note: `#[warn(clippy::needless_bool_assign)]` on by default

warning: `if_else` (lib) generated 4 warnings (run `cargo clippy --fix --lib -p if_else` to apply 2 suggestions)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.10s

第一个警告:function 'is_even' is never used 可以忽略。

关于 unneeded 'return' statement: 由于 Rust 是一种面向表达式的语言,函数最后的 return 是不必要的。

关于 unneeded late initialization: 同样地,由于 Rust 是面向表达式的语言,if 结构也是一个表达式(在练习中有解释),因此不需要在各个分支中对 result 进行初始化。

综上所述(忽略最后一个警告),代码可以改成这样!

rust 复制代码
fn is_even(n: u32) -> bool {
    if n % 2 == 0 {
        true
    } else {
        false
    }
}

由于 if 是一个表达式,因此可以将返回值直接绑定到 result 上(冗长的写法已折叠)。

但其实我们可以注意到,函数会返回最后被计算的值,因此根本不需要绑定到 result

总结来说,上述代码中,函数的返回值将是 if 表达式的计算结果,从而正确返回了 n 的偶数或奇数判定!

rust 复制代码
$ cargo clippy
    Checking if_else v0.1.0 (/path/to/100-exercises-to-learn-rust/exercises/02_basic_calculator/03_if_else)
warning: function `is_even` is never used
 --> exercises/02_basic_calculator/03_if_else/src/lib.rs:2:4
  |
2 | fn is_even(n: u32) -> bool {
  |    ^^^^^^^
  |
  = note: `#[warn(dead_code)]` on by default

warning: this if-then-else expression returns a bool literal
 --> exercises/02_basic_calculator/03_if_else/src/lib.rs:3:5
  |
3 | /     if n % 2 == 0 {
4 | |         true
5 | |     } else {
6 | |         false
7 | |     }
  | |_____^ help: you can reduce it to: `n % 2 == 0`
  |
  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_bool
  = note: `#[warn(clippy::needless_bool)]` on by default

warning: `if_else` (lib) generated 2 warnings (run `cargo clippy --fix --lib -p if_else` to apply 1 suggestion)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.12s
rust 复制代码
fn is_even(n: u32) -> bool {
    n % 2 == 0
}

其实根本不需要 if 表达式啊...!我以为是 if 表达式的问题,所以陷入了先入为主的思维...

顺便说一下,模范解答里也没有用 if 表达式。这是故意设计成这样的题目吗?

在这种时候,Clippy 真是太伟大了...!

[02_basic_calculator/04_panics](https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/02_basic_calculator/04_panics/src/lib.rs "02_basic_calculator/04_panics") Panic

这是问题的内容。虽然有点长,但因为包含了解题所需的信息,所以连测试部分也一起提供了。

rust 复制代码
/// Given the start and end points of a journey, and the time it took to complete the journey,
/// calculate the average speed of the journey.
fn speed(start: u32, end: u32, time_elapsed: u32) -> u32 {
    // TODO: Panic with a custom message if `time_elapsed` is 0

    (end - start) / time_elapsed
}

#[cfg(test)]
mod tests {
    use crate::speed;

    #[test]
    fn case1() {
        assert_eq!(speed(0, 10, 10), 1);
    }

    #[test]
    // 👇 With the `#[should_panic]` annotation we can assert that we expect the code
    //    under test to panic. We can also check the panic message by using `expected`.
    //    This is all part of Rust's built-in test framework!
    #[should_panic(expected = "The journey took no time at all, that's impossible!")]
    fn by_zero() {
        speed(0, 10, 0);
    }
}

首先,前提是 speed 函数在出现除以 0 的情况时会发生 panic。这本身没有问题,但在这种情况下,希望输出测试中指定的自定义消息:The journey took no time at all, that's impossible!

Rust Playground

Rust Playground

Exercises 中介绍了这个工具,所以我也在这里介绍一下。Rust Playground 是一个类似 WandboxideoneTS playground的在线编辑器,但专门用于 Rust。

和 Wandbox 一样,它可以通过永久链接分享源代码,因此在不创建项目时快速检查语法,或者在社交媒体上讨论 Rust 时都非常方便。一定要试试哦!

解释

如果 time_elapsed == 0,我们可以预先触发 panic,并输出想要显示的消息。这可以通过 if 表达式和 panic! 宏来实现。(这个问题才是更适合用 if 的场景吧?)

rust 复制代码
fn speed(start: u32, end: u32, time_elapsed: u32) -> u32 {
    // TODO: Panic with a custom message if `time_elapsed` is 0

    if time_elapsed == 0 {
        panic!("The journey took no time at all, that's impossible!");
    }

    (end - start) / time_elapsed
}

这段话可以翻译为:


顺便提一下,在这种情况下,使用 assert_* 系列宏会很方便!因为我们想确认 time_elapsed != 0,所以这次可以使用assert_ne宏。

rust 复制代码
fn speed(start: u32, end: u32, time_elapsed: u32) -> u32 {
    // TODO: Panic with a custom message if `time_elapsed` is 0

    assert_ne!(
        time_elapsed, 0,
        "The journey took no time at all, that's impossible!"
    );

    (end - start) / time_elapsed
}

这段话可以翻译为:


assert_* 系列宏可以理解为"保证条件被满足",这样理解起来会更容易。

  • assert_eq: 保证第一个参数和第二个参数相等,如果不相等就会触发 panic。
  • assert_ne: 保证第一个参数和第二个参数不相等,如果相等就会触发 panic。

这些宏如果不熟悉,确实容易混淆。此外,第三个参数可以指定 panic 时的自定义消息,因此可以在这里传入测试中要求的文本。

虽然在行数上使用 ifassert_* 宏区别不大(甚至可能多一行),但因为能传达"保证"的含义,所以可能有一部分人会觉得这种写法更易读。

问:听说使用 Result 类型更好是吗?

答:是的。在这种可能会出现错误的情况下,与其直接引发 panic,不如使用程序可以自定义错误处理的 Result 类型更为理想。(据说在 "100 Exercises" 中期会介绍这个概念。)

虽然在不熟悉的情况下尽可能使用 Result 类型确实不会出错,但理想情况下,我们也应该学习如何在合适的场景下使用 panic。以下是一些适合使用 panic(也就是 unwrap 系方法)的情况。

  • 在编译时已确定值且不可能发生 panic 的情况下

    使用 unwrap反而会让代码更易读。

  • main 函数的开头读取环境变量或依赖文件时

    使用 Result 进行分支处理往往显得冗长。在这种情况下,使用 expect会是个不错的选择。

一般来说,Result 类型适用于处理用户输入等"在运行时未确定输入"的情况。考虑到大多数程序都涉及这样的场景(尽管这有些偏见),所以使用 Result 类型的情况会更多。

[02_basic_calculator/05_factorial](https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/02_basic_calculator/05_factorial/src/lib.rs "02_basic_calculator/05_factorial") 尝试编写函数

问题是...除了测试部分之外,只有注释,所以我将注释内容总结如下:

请定义一个接收非负整数 n 的函数 factorial,并返回 n 的阶乘(n!)。

阶乘是从 1 到 n 的所有正整数的乘积。例如:5! = 5 * 4 * 3 * 2 * 1 = 120
0! 定义为 1。
factorial(0) 应返回 1,factorial(1) 也应返回 1,factorial(2) 应返回 2...依此类推。

请使用到目前为止所学的内容。你还没有学到循环,所以请使用递归来实现!

问题:

rust 复制代码
// Define a function named `factorial` that, given a non-negative integer `n`,
// returns `n!`, the factorial of `n`.
//
// The factorial of `n` is defined as the product of all positive integers up to `n`.
// For example, `5!` (read "five factorial") is `5 * 4 * 3 * 2 * 1`, which is `120`.
// `0!` is defined to be `1`.
//
// We expect `factorial(0)` to return `1`, `factorial(1)` to return `1`,
// `factorial(2)` to return `2`, and so on.
//
// Use only what you learned! No loops yet, so you'll have to use recursion!

#[cfg(test)]
mod tests {
    use crate::factorial;

    #[test]
    fn first() {
        assert_eq!(factorial(0), 1);
    }

    #[test]
    fn second() {
        assert_eq!(factorial(1), 1);
    }

    #[test]
    fn third() {
        assert_eq!(factorial(2), 2);
    }

    #[test]
    fn fifth() {
        assert_eq!(factorial(5), 120);
    }
}

确实,我们在 Rust 里还没学到循环,但就这么直接用递归真的好吗......算了,没关系。

解释

只需要写一个用于终止递归的保护条件,以及一个用于递归求值的部分就行了。

rust 复制代码
fn factorial(n: u32) -> u32 {
    if n == 0 {
        return 1;
    }

    n * factorial(n - 1)
}

这道题作为到目前为止问题的总结是很合适的。

  • 语法

    通过定义函数的形式复习了之前的内容。

  • 非负整数

    将类型设为 u32 后,就不必担心传入负数作为参数,因此不需要额外的异常处理。

  • 变量

    以函数参数的形式出现。

  • if 表达式和 Panic

    如果传入 0 作为参数,执行 n - 1 会引发 panic,但因为有保护条件阻止了这种情况的发生,所以没有问题。

    虽然在我的写法中没有使用表达式,但模范解答似乎要求对表达式有一定的理解。

回顾这道题,发现它设计得相当好,让人惊讶。

总结

虽然还没涉及到循环等更高级的语法,但最后这个问题确实相当有趣。

那么,让我们进入下一个问题吧!

下一篇文章: 【3】可变性・循环・溢出

相关推荐
红尘散仙4 小时前
我把终端小说阅读器接上了 AI Agent:TRNovel 现在能用 skill 生成书源了
人工智能·后端·rust
卷毛的技术笔记6 小时前
告别硬编码!Spring AI Alibaba 实现 AI Agent 智能工具调用(Tool Calling)
java·人工智能·后端·python·spring·ai编程
isyangli_blog6 小时前
OpenDayLight (Carbon 版本) 启动与组件安装
开发语言·php
vb2008116 小时前
FastAPI APIRouter
开发语言·python
Benszen6 小时前
KVM虚拟化解决方案
开发语言·perl
会编程的土豆6 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
東雪木6 小时前
多线程与并发编程 专属复习笔记
java·开发语言·笔记·java面试
喵个咪6 小时前
GoWind Toolkit Go后端代码生成 完整全流程实战
后端·go·orm
杨充7 小时前
1.3 浮点型数据设计灵魂
开发语言·python·算法
噜噜噜阿鲁~7 小时前
python学习笔记 | 11.3、面向对象高级编程-多重继承
java·开发语言