入出力プロジェクトを改善する
このイテレータに関する新しい知識があれば、イテレータを使用してコードのいろんな場所をより明確で簡潔にすることで、
第12章の入出力プロジェクトを改善することができます。イテレータがConfig::new関数とsearch関数の実装を改善する方法に目を向けましょう。
イテレータを使用してcloneを取り除く
リスト12-6において、スライスに添え字アクセスして値をクローンすることで、Config構造体に値を所有させながら、
String値のスライスを取り、Config構造体のインスタンスを作るコードを追記しました。リスト13-24では、
リスト12-23のようなConfig::newの実装を再現しました:
ファイル名: src/lib.rs
impl Config {
pub fn new(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let filename = args[2].clone();
let case_sensitive = env::var("CASE_INSENSITIVE").is_err();
Ok(Config { query, filename, case_sensitive })
}
}
リスト13-24: リスト12-23からConfig::new関数の再現
その際、将来的に除去する予定なので、非効率的なclone呼び出しを憂慮するなと述べました。
えっと、その時は今です!
引数argsにString要素のスライスがあるためにここでcloneが必要だったのですが、
new関数はargsを所有していません。Configインスタンスの所有権を返すためには、
Configインスタンスがその値を所有できるように、Configのqueryとfilenameフィールドから値をクローンしなければなりませんでした。
イテレータについての新しい知識があれば、new関数をスライスを借用する代わりに、
引数としてイテレータの所有権を奪うように変更することができます。スライスの長さを確認し、
特定の場所に添え字アクセスするコードの代わりにイテレータの機能を使います。これにより、
イテレータは値にアクセスするので、Config::new関数がすることが明確化します。
ひとたび、Config::newがイテレータの所有権を奪い、借用する添え字アクセス処理をやめたら、
cloneを呼び出して新しくメモリ確保するのではなく、イテレータからのString値をConfigにムーブできます。
返却されるイテレータを直接使う
入出力プロジェクトのsrc/main.rsファイルを開いてください。こんな見た目のはずです:
ファイル名: src/main.rs
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {}", err);
process::exit(1);
});
// --snip--
}
リスト12-24のようなmain関数の冒頭をリスト13-25のコードに変更します。
これは、Config::newも更新するまでコンパイルできません。
ファイル名: src/main.rs
fn main() {
let config = Config::new(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {}", err);
process::exit(1);
});
// --snip--
}
リスト13-25: env::argsの戻り値をConfig::newに渡す
env::args関数は、イテレータを返します!イテレータの値をベクタに集結させ、それからスライスをConfig::newに渡すのではなく、
今ではenv::argsから返ってくるイテレータの所有権を直接Config::newに渡しています。
次に、Config::newの定義を更新する必要があります。入出力プロジェクトのsrc/lib.rsファイルで、
Config::newのシグニチャをリスト13-26のように変えましょう。関数本体を更新する必要があるので、
それでもコンパイルはできません。
ファイル名: src/lib.rs
impl Config {
pub fn new(mut args: std::env::Args) -> Result<Config, &'static str> {
// --snip--
リスト13-26: Config::newのシグニチャをイテレータを期待するように更新する
env::args関数の標準ライブラリドキュメントは、自身が返すイテレータの型は、std::env::Argsであると表示しています。
Config::new関数のシグニチャを更新したので、引数argsの型は、&[String]ではなく、
std::env::Argsになりました。argsの所有権を奪い、繰り返しを行うことでargsを可変化する予定なので、
args引数の仕様にmutキーワードを追記でき、可変にします。
添え字の代わりにIteratorトレイトのメソッドを使用する
次に、Config::newの本体を修正しましょう。標準ライブラリのドキュメントは、
std::env::ArgsがIteratorトレイトを実装していることにも言及しているので、
それに対してnextメソッドを呼び出せることがわかります!リスト13-27は、
リスト12-23のコードをnextメソッドを使用するように更新したものです:
ファイル名: src/lib.rs
fn main() {} use std::env; struct Config { query: String, filename: String, case_sensitive: bool, } impl Config { pub fn new(mut args: std::env::Args) -> Result<Config, &'static str> { args.next(); let query = match args.next() { Some(arg) => arg, // クエリ文字列を取得しませんでした None => return Err("Didn't get a query string"), }; let filename = match args.next() { Some(arg) => arg, // ファイル名を取得しませんでした None => return Err("Didn't get a file name"), }; let case_sensitive = env::var("CASE_INSENSITIVE").is_err(); Ok(Config { query, filename, case_sensitive }) } }
リスト13-27: Config::newの本体をイテレータメソッドを使うように変更する
env::argsの戻り値の1番目の値は、プログラム名であることを思い出してください。それは無視し、
次の値を取得したいので、まずnextを呼び出し、戻り値に対して何もしません。2番目に、
nextを呼び出してConfigのqueryフィールドに置きたい値を得ます。nextがSomeを返したら、
matchを使用してその値を抜き出します。Noneを返したら、十分な引数が与えられなかったということなので、
Err値で早期リターンします。filename値に対しても同じことをします。
イテレータアダプタでコードをより明確にする
入出力プロジェクトのsearch関数でも、イテレータを活用することができます。その関数はリスト12-19に示していますが、以下のリスト13-28に再掲します。
ファイル名: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
リスト13-28: リスト12-19のsearch関数の実装
イテレータアダプタメソッドを使用して、このコードをもっと簡潔に書くことができます。そうすれば、
可変な中間のresultsベクタをなくすこともできます。関数型プログラミングスタイルは、可変な状態の量を最小化することを好み、
コードを明瞭化します。可変な状態を除去すると、検索を同時並行に行うという将来的な改善をするのが、
可能になる可能性があります。なぜなら、resultsベクタへの同時アクセスを管理する必要がなくなるからです。
リスト13-29は、この変更を示しています:
ファイル名: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents.lines()
.filter(|line| line.contains(query))
.collect()
}
リスト13-29: search関数の実装でイテレータアダプタのメソッドを使用する
search関数の目的は、queryを含むcontentsの行全てを返すことであることを思い出してください。
リスト13-19のfilter例に酷似して、このコードはfilterアダプタを使用してline.contains(query)が真を返す行だけを残すことができます。
それから、合致した行を別のベクタにcollectで集結させます。ずっと単純です!ご自由に、
同じ変更を行い、search_case_insensitive関数でもイテレータメソッドを使うようにしてください。
次の論理的な疑問は、自身のコードでどちらのスタイルを選ぶかと理由です: リスト13-28の元の実装とリスト13-29のイテレータを使用するバージョンです。 多くのRustプログラマは、イテレータスタイルを好みます。とっかかりが少し困難ですが、 いろんなイテレータアダプタとそれがすることの感覚を一度掴めれば、イテレータの方が理解しやすいこともあります。 いろんなループを少しずつもてあそんだり、新しいベクタを構築する代わりに、コードは、ループの高難度の目的に集中できるのです。 これは、ありふれたコードの一部を抽象化するので、イテレータの各要素が通過しなければならないふるい条件など、 このコードに独特の概念を理解しやすくなります。
ですが、本当に2つの実装は等価なのでしょうか?直観的な仮説は、より低レベルのループの方がより高速ということかもしれません。 パフォーマンスに触れましょう。