入出力プロジェクトを改善する
このイテレータに関する新しい知識があれば、イテレータを使用してコードのいろんな場所をより明確で簡潔にすることで、
第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 })
}
}
その際、将来的に除去する予定なので、非効率的な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--
}
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--
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 }) } }
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
}
イテレータアダプタメソッドを使用して、このコードをもっと簡潔に書くことができます。そうすれば、
可変な中間の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()
}
search
関数の目的は、query
を含むcontents
の行全てを返すことであることを思い出してください。
リスト13-19のfilter
例に酷似して、このコードはfilter
アダプタを使用してline.contains(query)
が真を返す行だけを残すことができます。
それから、合致した行を別のベクタにcollect
で集結させます。ずっと単純です!ご自由に、
同じ変更を行い、search_case_insensitive
関数でもイテレータメソッドを使うようにしてください。
次の論理的な疑問は、自身のコードでどちらのスタイルを選ぶかと理由です: リスト13-28の元の実装とリスト13-29のイテレータを使用するバージョンです。 多くのRustプログラマは、イテレータスタイルを好みます。とっかかりが少し困難ですが、 いろんなイテレータアダプタとそれがすることの感覚を一度掴めれば、イテレータの方が理解しやすいこともあります。 いろんなループを少しずつもてあそんだり、新しいベクタを構築する代わりに、コードは、ループの高難度の目的に集中できるのです。 これは、ありふれたコードの一部を抽象化するので、イテレータの各要素が通過しなければならないふるい条件など、 このコードに独特の概念を理解しやすくなります。
ですが、本当に2つの実装は等価なのでしょうか?直観的な仮説は、より低レベルのループの方がより高速ということかもしれません。 パフォーマンスに触れましょう。