はじめに
こんにちは。細々とプログラミングをしているsotanengelです。
この記事は以下の記事の連載です。
他の連載記事 (詳細)
- Day 1:型システムを使ってデータ構造を再現しよう
- Day 2:型システムを用いて共通の挙動を表現しよう
- Day 3:OptionとResultに対してはmatchを用いずに変換しよう
- Day 4:標準のErrorを使おう
- Day 5:型変換を理解しよう
- Day 6:newtypeパターンを活用しよう
- Day 7:複雑な型にはビルダを使おう
- Day 8:明示的なループの代わりにイテレータ変換を使用することを検討しよう
- Day 9:標準トレイトに習熟しよう
- Day 10:RIIパターンにはDropトレイトを実装しよう
- Day 11:ジェネリクスとトレイトオブジェクトのトレードオフを理解しよう
- Day 12:デフォルト実装を用いて、実装しなければならないトレイトメソッドを最小限にしよう
- Day 13:Don't panic
また本記事はEffective Rust(David Drysdale (著), 中田 秀基 (翻訳))を参考に作成されております。とてもいい書籍ですので興味を持った方は、ぜひ読んでみてください!
今日の内容
概要
Rustではpanicによって回復不能なバグに対応させることができますが、以下のような問題を内包しています。
- cargoによって挙動が変えられてしまう(The Cargo Book)
- "unwind" : パニック時にスタックを巻き戻します
- "abort" : パニック時にプロセスを終了します
- データ構造の作成中にpanicが起こると構造の整合性を保証できない
- FFI境界との相性が良くない(参考記事)
これらの問題を引き起こさないために、どのような場合にpanicを使うべきか考えましょう。
どれをpanicにすべきだろうか?
問(リンク)
get_critical_envとparse_str_to_intという関数があります。
panic!とResultどちらを使うべきでしょうか?
コード (詳細)
use std::env;
use std::num::ParseIntError;
// `CRITICAL_ENV` がセットされているか、確認する。
// TODO: このコードはpanic!とResultどちらがいいでしょうか?
fn get_critical_env() -> {
match env::var("PATH") {
Ok(value) => value,
Err(_) => ,
}
}
// 文字列を整数に変換する関数
// TODO: このコードはpanic!とResultどちらがいいでしょうか?
fn parse_str_to_int(input: &str) -> {
input.parse::<i32>()
}
fn main() {
let critical_value = get_critical_env();
println!("PATH is set to: {}", critical_value);
let inputs = vec!["42", "100", "not_number", "256"];
// TODO: 以下でparse_str_to_intでintに変更して出力してください。
}
// -------------------------------------------------------
// テストコード
// -------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
use std::env;
// PATH が正しくセットされている場合のテスト
//
// 注意: 環境変数はプロセス全体で共有されるため、テスト並列実行の設定によっては
// 衝突が起こる可能性があります。実際のプロダクションコードでは
// テスト時に専用のプロセスを立ち上げるか、シリアル実行するなどの工夫が必要です。
#[test]
fn test_get_critical_env_ok() {
// テスト用に環境変数をセット
env::set_var("PATH", "test_value");
let value = get_critical_env();
assert_eq!(value, "test_value");
}
// CRITICAL_ENV がセットされていない場合は panic! することを確認
#[test]
#[should_panic(expected = "PATH environment variable is not set! Aborting...")]
fn test_get_critical_env_panic() {
// 一旦アンセット
env::remove_var("PATH");
// ここで必ず panic! するはず
let _ = get_critical_env();
}
// -------------------------------------------------------
// parse_str_to_int のテスト (Result の確認)
// -------------------------------------------------------
#[test]
fn parse_str_to_int_success() {
let result = parse_str_to_int("123");
assert!(result.is_ok());
assert_eq!(result.unwrap(), 123);
}
#[test]
fn parse_str_to_int_error() {
let result = parse_str_to_int("not_a_number");
assert!(result.is_err());
// エラーの型が ParseIntError であることを確認
match result {
Err(e) => {
// ここではあえてエラーメッセージまでは比較しないが、
// メッセージを比較する場合は e.to_string() 等を用いる
assert!(e.to_string().contains("invalid digit"));
}
_ => unreachable!(),
}
}
}
解答(リンク)
コード参照。
コード (詳細)
use std::env;
use std::num::ParseIntError;
// これは「必須の環境変数」が無いとプログラムを継続できないケースを想定した関数。
// もし `PATH` がセットされていなければ、回復不能なエラーとみなして panic! する。
fn get_critical_env() -> String {
match env::var("PATH") {
Ok(value) => value,
Err(_) => panic!("PATH environment variable is not set! Aborting..."),
}
}
// -------------------------------------------------------
// Result を使ったエラー処理の例(回復可能なエラー)
// -------------------------------------------------------
// 文字列を整数に変換する関数
// 変換が失敗した場合は ParseIntError を返し、呼び出し元で対処できるようにする
fn parse_str_to_int(input: &str) -> Result<i32, ParseIntError> {
input.parse::<i32>()
}
fn main() {
println!("=== めったにないが必須条件が満たされていない場合の panic! ===");
// 本来であれば CRITICAL_ENV が必ず設定されている前提だが、
// もし設定が漏れていたらプログラムを継続できないので panic! する。
let critical_value = get_critical_env();
println!("PATH is set to: {}", critical_value);
// 以下は通常の処理(例: サーバー起動など)を続ける
println!("The program continues with the critical environment value...");
println!("\n=== Result を使うケース ===");
let inputs = vec!["42", "100", "not_number", "256"];
for s in inputs {
match parse_str_to_int(s) {
Ok(num) => println!("'{}' -> {}", s, num),
Err(e) => eprintln!("'{}' は整数に変換できませんでした: {}", s, e),
}
}
}
// -------------------------------------------------------
// テストコード
// -------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
use std::env;
// PATH が正しくセットされている場合のテスト
//
// 注意: 環境変数はプロセス全体で共有されるため、テスト並列実行の設定によっては
// 衝突が起こる可能性があります。実際のプロダクションコードでは
// テスト時に専用のプロセスを立ち上げるか、シリアル実行するなどの工夫が必要です。
#[test]
fn test_get_critical_env_ok() {
// テスト用に環境変数をセット
env::set_var("PATH", "test_value");
let value = get_critical_env();
assert_eq!(value, "test_value");
}
// PATH がセットされていない場合は panic! することを確認
#[test]
#[should_panic(expected = "PATH environment variable is not set! Aborting...")]
fn test_get_critical_env_panic() {
// 一旦アンセット
env::remove_var("PATH");
// ここで必ず panic! するはず
let _ = get_critical_env();
}
// -------------------------------------------------------
// parse_str_to_int のテスト (Result の確認)
// -------------------------------------------------------
#[test]
fn parse_str_to_int_success() {
let result = parse_str_to_int("123");
assert!(result.is_ok());
assert_eq!(result.unwrap(), 123);
}
#[test]
fn parse_str_to_int_error() {
let result = parse_str_to_int("not_a_number");
assert!(result.is_err());
// エラーの型が ParseIntError であることを確認
match result {
Err(e) => {
// ここではあえてエラーメッセージまでは比較しないが、
// メッセージを比較する場合は e.to_string() 等を用いる
assert!(e.to_string().contains("invalid digit"));
}
_ => unreachable!(),
}
}
}
さいごに
もしも本リポジトリで不備などあれば、リポジトリのissueやPRなどでご指摘いただければと思います。