やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

Rustでリファクタリング(モジュール性とエラー処理の向上)

 本番に近い考え方。

成果物

参考

前回からの続き

  1. Rustでコマンドライン引数を受け取る
  2. Rustのファイル読込

問題

  • 1番目: main関数が2つの仕事を受け持っている。引数解析とファイルオープン。機能を小分けして、各関数が1つの仕事のみに責任を持つようにするのが最善。
  • 2番目: queryとfilenameの構造化。設定用変数を一つの構造に押し込め、目的を明瞭化するのが最善。
  • 3番目: エラーメッセージが不正確になりうる。ファイルを開き損ねた時に「ファイルが見つかりませんでした」としか表示しない。開く権限がなく失敗したときも同様のメッセージになってしまう。正確なメッセージを出すべき。
  • 4番目: 「範囲外アクセス(index out of bounds)」エラーが起こりうる。

バイナリにおける責任分離

  • プログラムをmain.rsとlib.rsに分け、ロジックをlib.rsに移動する
  • コマンドライン引数の解析ロジックが小規模な限り、main.rsに置いても良い
  • コマンドライン引数の解析ロジックが複雑化の様相を呈し始めたら、main.rsから抽出してlib.rsに移動する

main.rsの責任

  • 引数の値でコマンドライン引数の解析ロジックを呼び出す
  • 他のあらゆる設定を行う
  • lib.rsのrun関数を呼び出す
  • runがエラーを返した時に処理する

リファクタリング

引数解析器の抽出

src/main.rs

fn main() {
    let args: Vec<String> = env::args().collect();
    let (query, filename) = parse_config(&args);
    // --snip--
}
fn parse_config(args: &[String]) -> (&str, &str) {
    let query = &args[1];
    let filename = &args[2];
    (query, filename)
}

設定値をまとめる

fn main() {
    let args: Vec<String> = env::args().collect();
    let config = parse_config(&args);

    println!("query: {}", config.query);
    println!("filename: {}", config.filename);
    let mut f = File::open(config.filename).expect("file not found");
}
struct Config {
    query: String,
    filename: String,
}
fn parse_config(args: &[String]) -> Config {
    let query = args[1].clone();
    let filename = args[2].clone();
    Config { query, filename }
}

 タプルでなく型にすることでデータ構造化できた。

clone()のメリットとデメリット

 メリットはコードが単純化すること。ライフタイム管理せずに済むため。

 デメリットはメモリ消費が増えること。メモリの確保やコピーするための実行時間がかかること。

Configのコンストラクタ作成

fn main() {
    let args: Vec<String> = std::env::args().collect();
    let config = Config::new(&args);
    // --snip--
}
impl Config {
    pub fn new(args: &[String]) -> Config {
        let query = args[1].clone();
        let filename = args[2].clone();
        Config { query, filename }
    }
}

 メソッド化することで責任の所在を局所化できた。

エラー処理

index out of boundsエラー

 現状、引数が2個未満だと以下エラーになる。

$ cargo run
...
thread 'main' panicked at 'index out of bounds: the len is 1 but the index is 1', src/main.rs:28:17
note: Run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

panic!

 以下のように修正する。

impl Config {
    pub fn new(args: &[String]) -> Config {
        if args.len() < 3 { panic!("引数不足。2つ必要です。第一引数に検索文字列、第二引数に検索対象ファイルパス。"); }
        // --snip--

 エラーメッセージが変わる。

$ cargo run
...
thread 'main' panicked at '引数不足。2つ必要です。第一引数に検索文字列、第二引数に検索対象ファイルパス。', src/main.rs:29:29
note: Run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

Result

 Result型を返すようにする。

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 { return Err("引数不足。2つ必要です。第一引数に検索文字列、第二引数に検索対象ファイルパス。"); }
        let query = args[1].clone();
        let filename = args[2].clone();
        Ok(Config { query, filename })
    }
}

 エラー受取処理。

fn main() {
    let args: Vec<String> = std::env::args().collect();
    let config = Config::new(&args).unwrap_or_else(|err| {
        println!("異常終了します。引数解析に問題が生じました。: {}", err);
        std::process::exit(1);
    });
    // --snip--

 実行結果。

$ cargo run
...
異常終了します。引数解析に問題が生じました。: 引数不足。2つ必要です。第一引数に検索文字列、第二引数に検索対象ファイルパス。
unwrap_or_else()

 unwrap()ResultOkErrの中にある型データを取り出す。

 unwrap_or_else()Errのとき、クロージャ内の処理を実行する。クロージャについては以降の章でやる。

mainからロジックを抽出する

run()

fn main() {
    // --snip--
    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    run(config);
}
fn run(config: Config) {
    let mut f = File::open(config.filename).expect("file not found");
    let mut contents = String::new();
    f.read_to_string(&mut contents)
        .expect("something went wrong reading the file");
    println!("With text:\n{}", contents);
}
// --snip--

 ファイルオープンから結果表示までをrun()へ移した。

エラー返却

use std::error::Error;
// --snip--
fn run(config: Config) -> Result<(), Box<Error>> {
    let mut f = File::open(config.filename)?;
    let mut contents = String::new();
    f.read_to_string(&mut contents)?;
    println!("With text:\n{}", contents);
    Ok(())
}

Box<Error>

 トレイトオブジェクト。(詳細は別の章でやる)

 std::error::ErrorErrorトレイトを実装する型を返す。具体的な型でなくともよいため、柔軟性が高い。

?でエラーを呼出元へ返す

//    let mut f = File::open(config.filename).expect("file not found");
    let mut f = File::open(config.filename)?;

 ファイル権限がないのか、ファイルが無いのかはわからない。Rustのデフォルト処理にまかせる。

Ok(())

 成功時に返すべきものはない。そういうときは()を返す。

エラー受取

 実行すると以下の警告が出る。

warning: unused `std::result::Result` that must be used
  --> src/main.rs:16:5
   |
16 |     run(config);
   |     ^^^^^^^^^^^^
   |
   = note: #[warn(unused_must_use)] on by default
   = note: this `Result` may be an `Err` variant, which should be handled

 エラーを受け取るようにする。

fn main() {
    // --snip--
    println!("query: {}", config.query);
    println!("filename: {}", config.filename);

    if let Err(e) = run(config) {
        println!("アプリエラー: {}", e);
        std::process::exit(1);
    };

 成功時はOk(())という空値なので不要。

ライブラリクレートに分割する

src/lib.rs

use std::error::Error;
use std::fs::File;
use std::io::prelude::*;

pub struct Config {
    pub query: String,
    pub filename: String,
}
impl Config {
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        // --snip--
    }
}
pub fn run(config: Config) -> Result<(), Box<Error>> {
    // --snip--
}

src/main.rs

fn main() {
    // コマンドライン引数受付
    let args: Vec<String> = std::env::args().collect();
    let config = minigrep::Config::new(&args).unwrap_or_else(|err| {
        println!("異常終了します。引数解析に問題が生じました。: {}", err);
        std::process::exit(1);
    });
    println!("query: {}", config.query);
    println!("filename: {}", config.filename);

    // grepを実行する(ファイルから検索する)
    if let Err(e) = minigrep::run(config) {
        println!("アプリエラー: {}", e);
        std::process::exit(1);
    };
}

 lib.rsに実装したpubな要素はmain.rsから参照できる。クレート名minigrepを指定することで。

 なお、Rust2015だとドキュメントどおりextern crate minigrep;が必要。

pub

 main.rsで呼び出す構造体、フィールド、関数にはpubを付与して公開する。

疑問

 将来的にmain.rsはminigrep.run();だけで完結する作りのほうがいい気がする。main.rsはテストできないからエラー時の出力におけるテストができない。それとも標準出力のテストはできないのか?

対象環境

$ uname -a
Linux raspberrypi 4.19.42-v7+ #1219 SMP Tue May 14 21:20:58 BST 2019 armv7l GNU/Linux

前回まで