100个练习学习Rust!可变性・循环・溢出

【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 循环,所以这次反而让我有点手忙脚乱了......

在这个问题中出现了两个新的要素:

  1. while 循环
  2. 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 存在......如果采用类似迭代器的写法(例如使用 mapreduce等),甚至可以去掉这个可变变量。

这与后面可能会涉及到的"迭代器"有关,1..=5 是生成 1, 2, 3, 4, 5Range 类型的值,并不是 for 循环所特有的语法。for 循环的语法实际上是 for 变量 in 迭代器 {},它从 Range 类型等迭代器中逐个取出元素进行处理。

loop 表达式

whilefor 不返回值,因此它们不是表达式。然而,loop 表达式可以返回值(这也是之前提到的伏笔)。

loop 表达式写作 loop {},它意味着 while true {},即一个无限循环的专用语法。

由于 loop 是专用于无限循环的,所以必须使用 break 才能退出,因此通过使用 break 可以返回一个值。

rust 复制代码
let 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)

相关推荐
YAy1730 分钟前
CC3学习记录
java·开发语言·学习·网络安全·安全威胁分析
代码小鑫31 分钟前
A035-基于Spring Boot的企业内管信息化系统
java·开发语言·spring boot·后端·spring
cleverpeople34 分钟前
11.15作业
c语言·开发语言·算法
Spy9734 分钟前
django 过滤器的执行
后端·python·django
_.Switch35 分钟前
Django SQL 查询优化方案:性能与可读性分析
开发语言·数据库·python·sql·django·sqlite·自动化
谈谈叭4 小时前
Javascript中的深浅拷贝以及实现方法
开发语言·javascript·ecmascript
lx学习4 小时前
Python学习26天
开发语言·python·学习
大今野5 小时前
python习题练习
开发语言·python
爱编程的鱼5 小时前
javascript用来干嘛的?赋予网站灵魂的语言
开发语言·javascript·ecmascript
camellias_5 小时前
SpringBoot(二十三)SpringBoot集成JWT
java·spring boot·后端