Posted at

コーディングミスから学ぶこと

More than 1 year has passed since last update.


問題

最近、またRustを基礎からやり直してみたいと思い、Aizu Online Judgeの基礎レベル問題からやっています。こういうのでRust対応しているのは珍しいですよね(そうでもないのかな?)。

で、この問題に対して以下のようなコードを書いて通りませんでした。

let mut line = String::new();

loop {
std::io::stdin().read_line(&mut line).unwrap();
{
let mut iter = line.trim().split_whitespace();
let m: i8 = iter.next().unwrap().parse().unwrap();
let f: i8 = iter.next().unwrap().parse().unwrap();
let r: i8 = iter.next().unwrap().parse().unwrap();

if m < 0 && f < 0 && r < 0 {
break;
}

if m < 0 || f < 0 { // (1)
println!("F");
continue;
}
// ...
}
line.clear();
}

こうしてシンプルにすると明らかですが、(1)のifの条件が満たされたときはcontinueしてしまうので、最後のline.clear()が実行されず、lineにゴミが残ったまま次の行を読んでしまうわけです。

気づいたときは自分に呆れてしまいました。


対応

では、こういうミスを防ぐにはどうすればいいか、どういう教訓が得られるか考えてみます。


入力が予想通りの形式か確認する

この問題では、1行は空白で区切られた3つの数値です。つまり、こうなります。

let m: i8 = iter.next().unwrap().parse().unwrap();

let f: i8 = iter.next().unwrap().parse().unwrap();
let r: i8 = iter.next().unwrap().parse().unwrap();
iter.next(); // Noneが返るはず

line.clear()を呼ばないままstdin().read_line(&mut line)を呼べばlineは2行分の内容を抱えることとなり、split_whitespace()は6つの数値を生成するので、上のコードでassertしておけば引っかかります。

ただこれをやっても、バグのあるコードを書いてしまうのは避けられません。


ループの再初期化はループ冒頭で行う

今回のline.clear()は、Cスタイルのforで言えば再初期化の部分(セミコロンで区切られた3つ目)でやりたい内容です。そういうのはループ冒頭でやりましょうという教訓です。

loop {

line.clear();
std::io::stdin().read_line(&mut line).unwrap();
// ...


やりたいことを捉え直す

そもそも今回read_line()では「その1行がほしい」はずです。しかしread_line()はバッファにアペンドする仕様です。このギャップを埋めるため、read_line()の前にバッファをクリアします。

loop {

line.clear();
std::io::stdin().read_line(&mut line).unwrap();
// ...

修正内容は上と同じになりましたが、目的は違います。今回なら、この2行を「標準入力から1行読んでバッファに上書きする操作」として抽象化し、関数として抽出するという考えにも至れます。


ループ内では極力ループ外の変数に触らない

ループで生じたものを蓄積したりする場合はしょうがないですが、それ以外はループ内でまかなうのが安全なのではないでしょうか。

loop {

let mut line = String::new();
std::io::stdin().read_line(&mut line).unwrap();
// ...

こうすると、修正前と比べて、ループのたびにStringの生成と解放が行われるのが気になってしまいます。しかし同時にclear()の呼び出しもなくなっています。

そもそも、ループのたびにStringの生成と解放が行われるのが問題になるようなタスクなのか。メモリリークでも起こしていたら問題ですがそれもない。コンパイラがうまくやってくれるかもしれないのに、測定もしていないパフォーマンス低下を恐れる必要はないのではないか。

しかし「スリムなコード」を書きたいという欲求には抗いがたい。いや、冗長なコードを書いて「こいつわかってないな」と思われたくない……

そんな染み付いた観念を捨てるのも、重要な教訓ですね。


最後に

いろいろな観点から今回のミスを捉えて、教訓を得てみたわけですが、他に重要なことが1つあります。

「書籍やサイトから学ぶ」です。自分がやったミスなんて世界で誰かがやっていて、それを防ぐ策もいろいろと考えられているでしょう。それを勉強できていれば、そもそもミスを犯さずに済んだかも知れません。

勉強したいなあ……