Rust:forおよびイテレーター

前の記事



この記事では、 forループ、および関連するイテレーターと「反復可能なオブジェクト」の概念について説明ます。



他のプログラミング言語での以前の経験に応じて、これらの概念は構文とセマンティクスの点で非常に馴染みがあるように見える場合や、まったく新しくて理解できない場合があります。 それらに最も近い類似物はPythonで見つけることができますが、Java、C#または(現代の)C ++のプログラマーは、言語にあるものと多くの共通点を見ることになると思います。



基本



Rastでは、 forループの構文はほとんどスパルタン簡潔です。



let v = vec!["1", "2", "3"]; for x in v { println!("{}", x); }
      
      





(ダブルセミコロンを介したforループのバリアントは現象としてRastaにありません。Pythonのように特定の範囲で反復するか、より複雑なケースではwhileまたはloopを使用できます)



予想どおり、上記のコードは1、2、3の3行を出力します。ベクトルvが実行中にループ内で移動したという事実は、おそらくそれほど明白ではありません。 ループ後にこのベクトルを使用しようとすると、エラーがスローされます。



 <anon>:6:22: 6:23 error: use of moved value: `v` [E0382] <anon>:4 println!("{}", x); <anon>:5 } <anon>:6 println!("{:?}", v); ^
      
      





ベクトルとその要素の所有は、完全に取り消せずにループに移動しました。 他の言語と比較して非常に予想外であるため、この動作は「デフォルトで移動する」という一般的なラスタポリシーと完全に一致しています。



しかし、引っ越しと借用のルールに完全には慣れていないため、この事実はあなたにとって驚きになることがあります。 ほとんどの場合、移動は関数とそのコンテキストの呼び出しに関連付けられています。 ほとんどの場合、理解を簡単にするために、上記のforループをfor_each関数と同様に検討できます。



 for_each(v, |x| println!("{}", x));
      
      





このビューは、ループ内で値が移動しないようにする方法のヒントも提供します。 ベクトル自体を渡す代わりに、リンクを渡すことしかできません。



 for_each_ref(&v, |x| println!("{}", x));
      
      





このコードをループ形式に変換することにより



  for x in &v { println!("{}", x); } println!("{:?}", v);
      
      





コンパイラエラーを取り除きます。



反復子と反復可能オブジェクト



追加されたアンパサンド( )は、 forループ構文の一部ではないことに注意してください。 ベクトルそのものであるVec <T>の代わりに、反復するオブジェクトを変更し、不変(不変)リンクである&Vec <T>を渡します。 この結果、タイプxTから&Tに変更されます。 これは現在、要素参照です。 (これは、「 参照解除時の変換 」が存在するため、サイクルの本体にはまったく影響しませんでした)



したがって、 Vec <T>&Vec <T>は両方とも「反復可能なオブジェクト」であることがわかります。 これをプログラミング言語に実装する通常の方法は、特別なオブジェクト「イテレータ」を導入することです。



反復子は、現在どの要素を指しているかを追跡し、少なくとも次の操作をサポートします。



  1. 現在のアイテムを取得する
  2. 次のアイテムに移動
  3. アイテムがなくなったという通知




一部の言語では、このタスクごとに異なるイテレーターを提供していますが、Rastではそれらを1つに結合することが決定されました。 Iteratorトレイトのドキュメントを見ると、実装を満たすために次のメソッドが1つあれば十分であることがわかります。



構文糖を削除



しかし、反復可能オブジェクトから反復子オブジェクトはどのくらい正確に作成されますか?



典型的なラスタの方法では、このタスクはIntoIteratorと呼ばれる別の特性に委任されます



 // () trait IntoIterator { fn into_iter(self) -> Iterator; }
      
      





Rastのユニークな機能は、この特性の唯一のメソッドであるinto_iterは、コレクションからイテレーターを作成するだけでなく、元のコレクションを本質的に吸収し、結果のイテレーターがコレクションの要素にアクセスする唯一の方法を残すことです。 (これは何と言えますか?それはinto_iter&selfまたは&mut selfではなくselfを引数として取るということです。つまり、オブジェクトの所有権はこのメソッド内で渡されます)



(翻訳者のメモ:以降、著者は、イテレータを作成するためのinto_iteriteriter_mutコレクションのメソッドの違いを詳細に調べません。つまり、最初はコレクションを内部に移動し、2番目はコレクションを不変に借用します。つまり、反復は要素への不変リンクを経由します、 3番目のものは可変的に借用するため、コレクションの要素を反復中に変更できます)



この動作により、イテレータの無効化と呼ばれる非常に一般的な間違いから保護されます。これはおそらくC ++プログラマーによく知られています。 なぜなら コレクションは基本的にイテレータに「変換」されるため、以下は不可能になります。



  1. コレクションを指す複数のイテレータの存在
  2. 反復子の1つがスコープ内にある間にコレクションを変更する


これらすべての「動き」と「借金」はあなたになじみのある音ですか? 以前、 forループでベクトルを反復処理することに注意しました 、基本的には「ループ内」に移動します。



ベクトルの反復中に既に推測できるように、実際にこのベクトルに対してIntoIterator :: into_iterを呼び出し、出力でイテレーターを取得します。 各反復でnextを呼び出すことにより、 Noneを取得するまで循環し続けます。



ループ:



 for x in v { //   }
      
      





基本的に、次の式の構文糖衣です。



 let mut iter = IntoIterator::into_iter(v); loop { match iter.next() { Some(x) => { //   }, None => break, } }
      
      





サイクルが終了した後だけでなく、 開始する前でもvを使用できないことがよくわかります 。 これは、 特性...のInto_iterメソッドを使用して、イテレータ内でベクトルを移動しました



シンプルでしょ? :)



forループは、 IntoIterator :: into_iterを呼び出し、続いてIterator :: nextを繰り返し呼び出すための構文糖衣です。




アンパサンド



ただし、この動作は常に望ましいとは限りません。 しかし、これを回避する方法はすでにわかっています。 ベクトル自体を反復処理する代わりに、そのリンクを使用します。



 for x in &v { //   }
      
      





transl。:v.iter(){...}のxに相当することに注意してください)



同時に、上で説明したことはすべて、構文糖の開示までここに適用されます。 into_iterメソッド以前と同様に呼び出されますが、1つ違いがありますが、ベクターの代わりにリンクを取得します。



 // () impl IntoIterator for &Vec<T> { fn into_iter(self) -> Iterator<Item=&T> { ... } }
      
      





したがって、出力反復子は、要素( T )自体ではなく、ベクトルの要素( &T )への参照を生成します。 そして以来 self aboveもリンクであるため、コレクションはどこにも移動しないため、サイクルの終了後に安全にアクセスできます。



可変リンクについても同じことが言えます。

 for x in &mut v { //   }
      
      





translに注意してください: v.iter_mut(){...}のxに相当)



唯一の違いは、 into_iter&mut Vec <T>に対して呼び出されるようになったことです。 したがって、 Iterator <Item =&mut T>という形式の結果により、コレクションの要素を変更できます。



これら2つのケースをサポートするために、追加のコンパイラサポートは必要ありませんでした。 すべてがすでに同じ特性でカバーされています。



IntoIteratorを使用してループ内の構文糖を拡張すると、コレクションオブジェクト自体、およびオブジェクトへの可変および不変の参照に対して同じように機能します。




iterメソッドはどうですか?



これまでのところ、非常に命令的なスタイルの計算を表すforループについてのみ説明しました。



関数型プログラミングにもっと興味がある場合は、次のようなメソッドを組み合わせたさまざまな構成を見たり書いたりしたかもしれません。



 let doubled_odds: Vec<_> = numbers.iter() .filter(|&x| x % 2 != 0).map(|&x| x * 2).collect();
      
      





マップフィルターなどのメソッドはイテレーターアダプターと呼ばれ、すべてイテレータートレイトに対して定義されます。 それらは非常に多数で表現力豊かであるだけでなく、サードパーティのラックで提供することもできます



アダプタを利用するには、最初にイテレータを取得する必要があります。 通常、ループはinto_iterを介して取得されるため、基本的にはここで同じアプローチを使用できます。



 let doubled_odds: Vec<_> = IntoIterator::into_iter(&numbers) .filter(|&x| x % 2 != 0).map(|&x| x * 2).collect();
      
      





コードの読みやすさを改善し、サイズを小さくするために、コレクションは通常、 iterメソッドを提供します。これは上記の式の短縮形です。 上記のような式で通常表示されるのはこのメソッドです。



v.iter()は IntoIterator :: into_iter(&v)の短縮形にすぎません。




両方とも?



注目に値する最後のこと:Rastは、コレクションを操作するために何を使用するか、イテレーターまたはループを示していません。 リリースモードで最適化を有効にすると、両方のアプローチをコンパイルして、必要に応じてインラインクロージャーとループをデプロイした同等の効率のマシンコードにする必要があります。



したがって、アプローチの選択は、スタイルと習慣の問題にすぎません。 適切な解決策は両方のアプローチを組み合わせることである場合がありますが、Rastを使用すると問題なく実行できます。



 fn print_prime_numbers_upto(n: i32) { println!("Prime numbers lower than {}:", n); for x in (2..n).filter(|&i| is_prime(i)) { println!("{}", x); } }
      
      





前と同様に、これはIntoIteratorトレイトを使用した構文糖衣の開示を通じて可能です。 この場合、Rastはイテレーター自身の変換を適用します。



イテレータ自体も、 IntoIterator :: into_iter traitの「透過的な」実装を通じて、「反復可能なオブジェクト」です。




結論として



イテレータとループに関する詳細情報を知りたい場合は、 公式ドキュメントが最適なソースになります。 すべてのイテレーターアダプターをマスターすることは、Rastで効果的なコードを書くために必ずしも必要ではありませんが、 collectメソッドと関連するFromIteratorトレイトのドキュメントを注意深く確認することは非常に役立つでしょう。



All Articles