本番に近い考え方。
成果物
参考
前回からの続き
問題
- 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()
はResult
のOk
とErr
の中にある型データを取り出す。
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::Error
はError
トレイトを実装する型を返す。具体的な型でなくともよいため、柔軟性が高い。
?
でエラーを呼出元へ返す
// 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はテストできないからエラー時の出力におけるテストができない。それとも標準出力のテストはできないのか?
対象環境
- Raspbierry pi 3 Model B+
- Raspbian stretch 9.0 2018-11-13
- bash 4.4.12(1)-release
- rustc 1.34.2 (6c2484dc3 2019-05-13)
- cargo 1.34.0 (6789d8a0a 2019-04-01)
$ uname -a Linux raspberrypi 4.19.42-v7+ #1219 SMP Tue May 14 21:20:58 BST 2019 armv7l GNU/Linux
前回まで
- Rustを学んでみたい(プログラミング言語)
- Rustの環境構築
- RustでHelloWorld
- Rustの和訳ドキュメント
- Cargoでプロジェクト作成・ビルド・実行
- クレートとは?
- Rustで関数を使ってみる
- Rustでモジュールを使ってみる
- Rustで乱数を生成する(rand)
- Rustで標準入力する(std::io::stdin().read_line())
- RustでMatch判定する(match)
- Rustでprintとread_lineを1行にする方法
- Rustで数当てゲーム
- クレート名にドット.が使えない
- Rustの変数と可変性(let, mut) error[E0384]: cannot assign twice to immutable variable
x
- Rustのimmutable束縛とconst定数の違い
- RustのREPL、evcxrのインストールに失敗した
- Rustでコンパイルするときの変数未使用warningを消す
- Rustの変数(再代入、再宣言(シャドーイング))
- Rustのシャドーイングについて
- イミュータブルについて(副作用)
- Rustの定数(const)
- Rustのデータ型(数値)
- Rustのデータ型(論理)
- Rustのデータ型(文字)
- Rustのデータ型(タプル)
- Rustのデータ型(配列)
- Rustの関数
- Rustのif式
- Rustのくりかえし文(loop)
- Rustのくりかえし文(while)
- Rustのくりかえし文(for)
- Rustの所有権(ムーブ)
- Rustの所有権(関数)
- Rustの所有権(スライス)
- Rustの構造体(定義とインスタンス化)
- Rustの構造体(プログラム例)
- Rustの構造体(メソッド)
- Rustの列挙型(enum)
- Rustの列挙型(enum)
- Rustの列挙型(enum)
- Rustのmatch(制御フロー演算子)
- RustでNULLを扱う(Option, Some, None)
- NULL参照は10億ドルの失敗だった
- Rustの列挙型に独自表示を実装する(E0277 対策 std::fmt::Display 実装)
- RustのIfLet(matchの糖衣構文)
- Rustのプロジェクト構造
- Rustのcargoでライブラリ&テスト(単体、結合)
- Rustのモジュール(mod)
- Rustのモジュール(pub)
- Rustのmod参照方法(
mod 子モジュール名;
,use 要素名;
,extern crate クレート名;
,super
) - Rustのインポートまとめ(Rust2018)
- RustのコレクションVec型
- RustのコレクションString型
- RustのコレクションHashMap型
- Rustのコレクション(練習問題)
- Rustのエラー処理
- Rustのジェネリクス
- Rustのトレイト
- Rustのライフタイム1
- Rustのライフタイム2(構造体の定義)
- Rustのライフタイム3(ライフタイム省略)
- Rustのライフタイム4(impl定義)
- Rustの静的ライフタイム5('static)
- Rustのライフタイム6(ジェネリクス、トレイト境界とともに)
- Rustのテストコードを書く
- Rustのテスト実行
- Rustのテスト体系化
- Rustでコマンドライン引数を受け取る
- Rustのファイル読込