【0】准备
【1】构文・整数・变量
【2】 if・Panic・演练 ←前次
【3】可变性・循环・溢出 本次
这是**"** 100 Exercise To Learn Rust" 的第三次练习!这次是关于两个循环结构的讨论,可能与上上次练习中讨论的溢出有关!
本次相关的页面如下:
那么让我们开始吧
[02_basic_calculator/06_while] while
问题如下
rust
// Rewrite the factorial function using a `while` loop.
pub fn factorial(n: u32) -> u32 {
// The `todo!()` macro is a placeholder that the compiler
// interprets as "I'll get back to this later", thus
// suppressing type errors.
// It panics at runtime.
todo!()
}
#[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);
}
}
上一次,我们用递归方式编写了计算阶乘的代码,这次的任务似乎是要将其改写为使用 while
循环的方式。
todo!
宏还未实现,并且在代码执行到这里时会引起 panic。可能在未来的某个练习中会处理这个宏。因为 todo!
宏具有一种特殊的性质,即它可以返回与函数返回类型相同的值,所以它通常不会导致编译错误。
无论如何,看起来我们需要将这部分代码替换成 while
循环。
解説
rust
pub fn factorial(n: u32) -> u32 {
let mut res = 1;
let mut i = 1;
while i <= n {
res *= i;
i += 1;
}
res
}
我真的很想用 for
循环来写这段代码......因为平时并不会这么勉强地使用 while
循环,所以这次反而让我有点手忙脚乱了......
在这个问题中出现了两个新的要素:
while
循环mut
关键字
看起来这节内容顺便引入了"可变性"这个概念。实际上,这个关键词经常在类似的循环结构中使用,所以这也是很合理的。
在 Rust 中,所有变量默认都是不可变的。除非有意为之,否则这种不可变性会贯穿到结构体、引用、数组等各种情况中。值不变的特性可以为阅读代码的人带来安心感,从而避免诸如"这个函数是否以可变引用传递了变量?"这样的多余猜测,进而实现更清晰明确的编码风格。
然而,当一切都是不可变的时,有时会显得不方便,其中一个典型例子就是循环中的变量。为了应对这种情况,当你想要使用可变变量时,可以使用 mut
关键字。通过这个关键字,你可以明确地告知代码的阅读者:"这个变量可能会因为副作用而发生变化"。
让我们回到 while
循环的讨论上来。(就像 if
表达式一样),除了条件部分没有被括号包围之外,while
循环与其他语言并没有太大区别。与 if
表达式不同,while
循环在被评估后不会立即确定返回值,因此它不具有表达式的性质(伏笔)。
没有
i++
吗?在 Rust 中没有增量运算符 (
++
)。我想这可能就是原因 。本质上,
i++
这一单行语句做的事情太多了。由于 Rust 语言不太喜欢"偷懒"的语法(在引入新特性时非常谨慎),所以故意没有提供类似的运算符。
[02_basic_calculator/07_for] for
终于可以用 for
循环来写代码了...!
rust
// Rewrite the factorial function using a `for` loop.
pub fn factorial(n: u32) -> u32 {
todo!()
}
#[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
pub fn factorial(n: u32) -> u32 {
let mut res = 1;
for i in 1..=n {
res *= i;
}
res
}
通过将循环变量的作用域限制在 for
循环内,代码比使用 while
循环更加简洁!由于 i
不需要 mut
关键字,因此保证了在每个循环中 i
是不可变的,这一点非常好。
不过,仍然有一个可变变量 res
存在......如果采用类似迭代器的写法(例如使用 map或 reduce等),甚至可以去掉这个可变变量。
这与后面可能会涉及到的"迭代器"有关,1..=5
是生成 1, 2, 3, 4, 5
的 Range 类型的值,并不是 for
循环所特有的语法。for
循环的语法实际上是 for 变量 in 迭代器 {}
,它从 Range
类型等迭代器中逐个取出元素进行处理。
loop
表达式
while
和for
不返回值,因此它们不是表达式。然而,loop
表达式可以返回值(这也是之前提到的伏笔)。
loop
表达式写作loop {}
,它意味着while true {}
,即一个无限循环的专用语法。由于
loop
是专用于无限循环的,所以必须使用break
才能退出,因此通过使用break
可以返回一个值。
rustlet mut i = 0; let res = loop { if i == 10 { break i; } println!("{}", i); i += 1; }; println!("{}", res);
通过
break i;
退出循环时,作为表达式返回了i
的值!......不过,我认为使用的机会应该不多。我只是觉得这是一个有趣的语法,所以介绍一下。
[02_basic_calculator/08_overflow] オーバーフロー
问题如下
rust
// Customize the `dev` profile to wrap around on overflow.
// Check Cargo's documentation to find out the right syntax:
// https://doc.rust-lang.org/cargo/reference/profiles.html
//
// For reasons that we'll explain later, the customization needs to be done in the `Cargo.toml`
// at the root of the repository, not in the `Cargo.toml` of the exercise.
pub fn factorial(n: u32) -> u32 {
let mut result = 1;
for i in 1..=n {
result *= i;
}
result
}
#[cfg(test)]
mod tests {
use crate::factorial;
#[test]
fn twentieth() {
// 20! is 2432902008176640000, which is too large to fit in a u32
// With the default dev profile, this will panic when you run `cargo test`
// We want it to wrap around instead
assert_eq!(factorial(20), 2_192_834_560);
// ☝️
// A large number literal using underscores to improve readability!
}
#[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);
}
}
刚才的问题答案已经写出来了......!
虽然这问题有点难以理解,但看起来问题的意思是:"20! = 2432902008176640000 这个结果太大了,会导致溢出并引发 panic,所以请通过修改 Cargo.toml 的设置,避免出现 panic!"
解説
在项目的 Cargo.toml 文件中(不是 exercises 内各个问题的 Cargo.toml,而是项目根目录下的 Cargo.toml文件),需要添加一个设置,用来在发生溢出时忽略它。
rust
[workspace]
members = ["exercises/*/*", "helpers/common", "helpers/mdbook-exercise-linker", "helpers/ticket_fields"]
resolver = "2"
+ [profile.dev]
+ overflow-checks = false
......不,这也太勉强了吧......?!顺便提一下,在 dev(调试构建)中,overflow-checks
的默认值是 true
,而在 release(发布构建)中,默认值是 false
。在 AtCoder上选择 Rust 进行解答时,实际上是进行发布构建,所以在本地(调试构建)时由于溢出导致运行时错误(RE),但提交后显示为错误答案(WA),这是一个很容易让人出错的陷阱。这实际上是由于不同构建配置中的默认值造成的。
应对溢出的办法有很多。在 AtCoder 上回答时,通常会取模,例如使用 10e9+7 这样的素数,而我在学习密码学时,常常使用"有限体",这种方法保证了所有计算结果都落在一个有限的集合(体)内。
虽然在这个问题中我们并没有使用素数取模的方式,这可能会影响运算的一致性,但本题的测试用例看起来像是实现了溢出后重新从 0 开始计数(wrap around)。考虑到这点,我尝试了另一种避免编辑 Cargo.toml 并避免溢出的解决方案。
rust
pub fn factorial(n: u32) -> u32 {
let mut result = 1u32;
for i in 1..=n {
result = result.wrapping_mul(i);
}
result
}
总结
使用 u32::wrapping_mul可以让运算在溢出时自动 wrap around,从而通过测试!在源代码中明确说明已经考虑了溢出处理,这样就不需要每次查看 Cargo.toml 了,这会更方便。
那么,让我们进入下一个问题吧!
下一篇文章:【4】类型转换与结构体(有时涉及 UFCS)