はじめに
I/O プロジェクトの改善へようこそ。この実験は、Rust 本の一部です。LabEx で Rust のスキルを練習できます。
この実験では、反復子を使って、第 12 章の I/O プロジェクトの Config::build
関数と search
関数の実装をどのように改善できるかを検討します。
This tutorial is from open-source community. Access the source code
💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください
I/O プロジェクトの改善へようこそ。この実験は、Rust 本の一部です。LabEx で Rust のスキルを練習できます。
この実験では、反復子を使って、第 12 章の I/O プロジェクトの Config::build
関数と search
関数の実装をどのように改善できるかを検討します。
反復子に関するこの新しい知識を使って、第 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 でさらに多くの実験を行って練習してください。