Rust 中逻辑上非法但未初始化的变量

Logically yet illegaly uninitialized variable in Rust

我对 Rust 比较陌生。这个问题原来很长,所以我将从底线开始: 您更喜欢哪种解决方案?你有什么想法或意见吗?

我的代码无法编译,因为第 6 行 (prev = curr) 和第 12 行 (bar(...)) 使用编译器怀疑可能未初始化的变量。作为程序员,我知道没有理由担心,因为第 6 行和第 12 行在第一次迭代期间没有 运行。

let mut curr: Enum;
let mut prev: Enum;

for i in 0..10 {
    if i > 0 {
        prev = curr;
    }

    curr = foo();

    if i > 0 {
        bar(&curr, &prev);
    }
}

我知道您对编译器的期望是有限的。所以我想出了 3 种不同的方式来回答语言的安全限制。

1)初始化就别想太多了

我可以用任意值初始化变量。风险在于维护者可能会错误地认为那些初始的、希望未使用的值具有某些重要意义。第 1-2 行将变为:

let mut curr: Enum = Enum::RED; // Just an arbitrary value!
let mut prev: Enum = Enum::BLUE; // Just an arbitrary value!

2) 将 None 值添加到 Enum

第 1-2 行将变为

let mut curr: Enum = Enum::None;
let mut prev: Enum = Enum::None;

我不喜欢这个解决方案的原因是现在我已经为我的枚举添加了一个新的可能的和不自然的值。为了我自己的安全,我将不得不向 foo()bar() 以及任何其他使用 Enum.

的函数添加断言检查和匹配分支

3) Option<Enum>

我认为这个解决方案是最“循规蹈矩”的解决方案,但它使代码更长且更难理解。

第 1-2 行将变为

let mut curr: Option<Enum> = None;
let mut prev: Option<Enum> = None;

这一次,None 属于 Option 而不是 Enum。无辜的 prev = curr 语句将变成

prev = match curr {
    Some(_) => curr,
    None => panic!("Ah!")
};

并且天真的调用 bar() 会丑化为

match prev {
    Some(_) => bar(&curr, &prev),
    None => panic!("Oy!")
};

问题:您更喜欢哪种解决方案?您有什么想法或意见吗?

我对 Rust 也是非常新,但我发现第三个选项最清楚。我会补充说 unwrap()expect() 可以用来代替具有相同含义的匹配表达式(“我知道这是被占用的”):

let mut curr: Option<Enum> = None;
let mut prev: Option<Enum> = None;

for i in 0..10 {
    if i > 0 {
        prev = curr;
    }

    curr = Some(foo());

    if i > 0 {
        bar(curr.as_ref().unwrap(), prev.as_ref().unwrap());
    }
}

参见 the playground。它仍然有一点额外的仪式感。

一般来说,我发现这种形式的循环的较长的现实生活示例很难理解;循环的结构掩盖了所需的逻辑。如果可以将其构造得更像一个迭代,我会发现它更清楚:

for (prev, curr) in FooGenerator::new().tuple_windows().take(10) {
    bar(&prev, &curr);
}

参见 the playground

尽可能避免在 Rust 中使用太多可变变量绑定,尤其是在循环中。使用可变变量在循环内管理状态会使代码更难理解并导致错误。在 for 循环中看到计数器变量通常是一个危险信号,它通常可以用值的迭代器代替。

在您的情况下,您可以在 foo 生成的值上创建一个迭代器,并将它们成对传递给 bar,使用 itertools 中的 tuple_windows板条箱:

use itertools::Itertools as _; // 0.9.0
use std::iter::repeat_with;

for (prev, curr) in repeat_with(foo).tuple_windows().take(9) {
    bar(&curr, &prev);
}

请注意,您不能在这里犯错而忘记设置 prevcurr,以后的维护者也不能不小心调换两行而搞砸了。

这种风格非常简洁和富有表现力。例如,我编写上面代码片段的方式强调 bar 将被调用 9 次。如果您更愿意强调 foo 将被调用 10 次,您也可以稍微修改它:

for (prev, curr) in repeat_with(foo).take(10).tuple_windows() {
    bar(&curr, &prev);
}

实际上,您表达的逻辑就是所谓的“折叠”,您可以使用折叠而不是循环。

可以做的是:

(0..9).map(|_|foo()) //an iterator that outputs foo() 9 times, not 10
   .fold(foo(), |prev, curr| {bar(&curr, &prev); curr});

业务端当然是.fold那一行。 fold 的作用是它采用单个参数作为初始值,根据该初始值和迭代器生成的当前值调用二元函数,然后将结果用作下一次迭代的新初始值。我在这里简单地使用了一个函数,它像你一样为 side-effect 调用,结果值使用 curr 值,因此用作下一次迭代的初始值,并用作prev.

这个表达式返回的最终值是最后一个 curr,当然可以忽略它。

https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.fold

无论如何,修复代码的一种简单方法是将第一次迭代从循环中拉出。由于 if 语句,第一次迭代唯一要做的就是调用 foo()。此外,您可以将 prev 的赋值移动到循环的末尾,这样您就可以将 curr 的范围限定在循环内部:

let mut prev = foo();
for _ in 1..10 {
    let curr = foo();
    bar(&curr, &prev);
    prev = curr;
}

请注意,我将范围更改为从 1 而不是 0 开始,因为我们将第一次迭代从循环中拉出。

我不认为这种方法比接受的答案中的方法更好,但我确实认为它非常简单易懂。