はじめに
I/O プロジェクトの改善へようこそ。この実験は、Rust 本の一部です。LabEx で Rust のスキルを練習できます。
この実験では、反復子を使って、第 12 章の I/O プロジェクトの Config::build 関数と search 関数の実装をどのように改善できるかを検討します。
I/O プロジェクトの改善
反復子に関するこの新しい知識を使って、第 12 章の I/O プロジェクトを改善できます。反復子を使ってコードの場所を明確で簡潔にすることができます。反復子が Config::build 関数と search 関数の実装をどのように改善できるか見てみましょう。
反復子を使ったクローンの削除
リスト 12-6 では、String値のスライスを取り、スライスにインデックスを付けて値をクローンすることでConfig構造体のインスタンスを作成し、Config構造体がそれらの値を所有するようにしました。リスト 13-17 では、リスト 12-23 にあったConfig::build関数の実装を再現しています。
ファイル名:src/lib.rs
impl Config {
pub fn build(
args: &[String]
) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
リスト 13-17: リスト 12-23 のConfig::build関数の再現
当時、非効率的なclone呼び出しは心配しなくて良いと言いました。なぜなら、将来的にそれらを削除するからです。さて、その時が来ました!
ここでcloneが必要だったのは、パラメータargsにString要素のスライスがあるためですが、build関数はargsを所有していません。Configインスタンスの所有権を返すには、Configのqueryとfilenameフィールドから値をクローンする必要がありました。そうすることで、Configインスタンスがそれらの値を所有できるようになります。
反復子に関する新しい知識を使って、build関数を変更して、スライスを借りる代わりに反復子の所有権を引数として取るようにします。スライスの長さをチェックし、特定の場所にインデックスを付けるコードの代わりに、反復子機能を使います。これにより、Config::build関数が何をしているかが明確になります。なぜなら、反復子が値にアクセスするからです。
Config::buildが反復子の所有権を取得し、借りるインデックス操作を使わなくなると、String値を反復子からConfigに移動できるようになります。その代わりにcloneを呼び出して新しい割り当てを行う必要はありません。
直接返される反復子の使用
あなたの I/O プロジェクトの src/main.rs ファイルを開きます。このファイルは次のようになっているはずです。
ファイル名:src/main.rs
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
--snip--
}
まず、リスト 12-24 にあったmain関数の始まりを、今度は反復子を使うリスト 13-18 のコードに変更します。Config::buildも更新しない限り、これはコンパイルされません。
ファイル名:src/main.rs
fn main() {
let config =
Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
--snip--
}
リスト 13-18: env::argsの返り値をConfig::buildに渡す
env::args関数は反復子を返します!反復子の値をベクタに収集してから、スライスをConfig::buildに渡す代わりに、今回はenv::argsから返される反復子の所有権を直接Config::buildに渡しています。
次に、Config::buildの定義を更新する必要があります。あなたの I/O プロジェクトの src/lib.rs ファイルで、Config::buildのシグネチャをリスト 13-19 のように変更しましょう。これもまだコンパイルされません。なぜなら、関数本体も更新する必要があるからです。
ファイル名:src/lib.rs
impl Config {
pub fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
--snip--
リスト 13-19: 反復子を期待するようにConfig::buildのシグネチャを更新する
env::args関数の標準ライブラリドキュメントによると、それが返す反復子の型はstd::env::Argsで、その型はIteratorトレイトを実装し、String値を返します。
Config::build関数のシグネチャを更新しました。これにより、パラメータargsは&[String]ではなく、トレイト境界impl Iterator<Item = String>を持つジェネリック型になりました。「トレイトをパラメータとして」で議論したimpl Trait構文のこの使用法は、argsがIterator型を実装し、String項目を返す任意の型であることを意味します。
argsの所有権を取得し、反復子を使ってargsを変更するため、argsパラメータの仕様にmutキーワードを追加して、可変にすることができます。
インデックス付けの代わりに反復子トレイトメソッドを使用する
次に、Config::buildの本体を修正します。argsはIteratorトレイトを実装しているので、それに対してnextメソッドを呼び出せることがわかります!リスト 13-20 は、リスト 12-23 のコードを更新して、nextメソッドを使用するようにしています。
ファイル名:src/lib.rs
impl Config {
pub fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
args.next();
let query = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a query string"),
};
let file_path = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a file path"),
};
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
リスト 13-20: 反復子メソッドを使用するようにConfig::buildの本体を変更する
env::argsの返り値の最初の値はプログラムの名前であることを思い出してください。それを無視して次の値に行きたいので、まずnextを呼び出して返り値は何もせずに済みます。その後、nextを呼び出してConfigのqueryフィールドに入れたい値を取得します。nextがSomeを返す場合、matchを使って値を抽出します。Noneを返す場合は、引数が足りないことを意味し、Err値を返して早期に終了します。filenameの値についても同じことを行います。
反復子アダプタを使ってコードを明確にする
I/O プロジェクトの search 関数でも反復子を利用できます。ここでは、リスト 12-19 にあったものと同じように、リスト 13-21 に再現しています。
ファイル名: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-21: リスト 12-19 のsearch関数の実装
反復子アダプタメソッドを使って、このコードをもっと簡潔に書くことができます。これにより、中間の可変なresultsベクタを持たなくて済むようになります。関数型プログラミングスタイルでは、コードを明確にするために可変状態の量を最小限に抑えることが好まれます。可変状態を削除することで、将来的に並列で検索を行うような機能強化が可能になるかもしれません。なぜなら、resultsベクタへの同時アクセスを管理する必要がなくなるからです。リスト 13-22 にこの変更を示します。
ファイル名:src/lib.rs
pub fn search<'a>(
query: &str,
contents: &'a str,
) -> Vec<&'a str> {
contents
.lines()
.filter(|line| line.contains(query))
.collect()
}
リスト 13-22: search関数の実装で反復子アダプタメソッドを使用する
search関数の目的は、contentsの中でqueryを含むすべての行を返すことです。リスト 13-16 のfilterの例と同様に、このコードはfilterアダプタを使って、line.contains(query)がtrueを返す行だけを残します。その後、collectを使って一致する行を別のベクタに収集します。はるかに簡単です!search_case_insensitive関数でも反復子メソッドを使う同じ変更を自由に行ってください。
ループと反復子の選択
次に理にかなった質問は、自分のコードでどちらのスタイルを選ぶべきか、そしてなぜかということです。リスト 13-21 の元の実装と、リスト 13-22 の反復子を使ったバージョンです。ほとんどの Rust プログラマは反復子スタイルを好みます。最初は少し慣れるのが難しいかもしれませんが、一旦さまざまな反復子アダプタとそれらの機能を理解すると、反復子は理解しやすくなります。ループの様々な部分をいじり、新しいベクタを作成する代わりに、コードはループの高レベルな目的に焦点を当てます。これにより、いくつかの一般的なコードが抽象化され、このコード固有の概念、たとえば反復子の各要素が通過しなければならないフィルタ条件がより明確になります。
しかし、2 つの実装は本当に同等なのでしょうか?直感的な仮定は、低レベルのループの方が速いだろうということかもしれません。では、パフォーマンスについて話し合いましょう。
まとめ
おめでとうございます!あなたは「I/O プロジェクトの改善」の実験を完了しました。あなたの技術を向上させるために、LabEx でさらに多くの実験を行って練習してください。