Rust イテレータ

Rust のイテレータ(Iterator)は、コレクション(配列、ベクタ、連結リストなど)に対して段階的にアクセスし操作するための、強力で柔軟なツールです。

Rust のイテレータは遅延評価されます。つまり、イテレータ自体は操作をすぐに実行せず、必要なときに値を生成します。

イテレータを使用すると、配列、スライス、連結リストなどのコレクション型の要素を、宣言的な方法で走査できます。

イテレータの背後にある核となる考え方は、データ処理のプロセスとデータ自体を分離し、コードをより明確で、読みやすく、保守しやすくすることです。

Rust では、イテレータは Iterator trait を実装することで定義されます。

最も基本的な trait メソッドは next で、イテレータ内の次の要素を1つずつ返し、None を返すと終了を表します。

例

pub trait Iterator {
    type Item;
   
    fn next(&mut self) -> Option<Self::Item>;
   
    // map、filter など、他のデフォルト実装のメソッド。
}

イテレータは以下の原則に従います:

  • 遅延評価(Laziness):Rust のイテレータは遅延評価であり、データを明示的に要求するまで、イテレータ自体は計算や操作を一切実行しないことを意味します。これにより、イテレータはパフォーマンスが良好で、不要な計算を回避できます。

  • 所有権と借用チェック(Ownership and Borrowing Checks):Rust のイテレータは所有権と借用の規則を厳密に守り、データ競合やメモリエラーを回避します。イテレータのライフタイムは基盤となるデータに関連付けられ、安全なデータアクセスを保証します。

  • チェーン呼び出し(Chaining):Rust のイテレータはチェーン呼び出しをサポートしています。つまり、複数のイテレータメソッドを連結して組み合わせた操作ができ、コードが簡潔で読みやすくなります。例えば、.map()、.filter()、.collect()などのメソッドを使用することで、複雑なデータ処理パイプラインを作成できます。

  • 効率的なメモリ管理(Efficient Memory Management):イテレータは不要なメモリ割り当てを回避します。ほとんどの操作は遅延評価され、使用時に直接走査操作が行われるためです。これは大きなデータコレクションを扱う場合に特に重要です。

  • 抽象性と汎用性(Abstraction and Generality):Rust のイテレータはIteratortrait によって抽象性と汎用性を実現しています。任意のIteratortrait を実装した型は、さまざまなコンテキストでイテレータとして使用できます。この設計により、コードの再利用性とモジュール化が向上します。


イテレータの作成

最も一般的な方法は、コレクションの.iter()、.iter_mut() 或 .into_iter()メソッドを使用してイテレータを作成することです:

  • .iter():コレクションの不変参照イテレータを返します。
  • .iter_mut():コレクションの可変参照イテレータを返します。
  • .into_iter():コレクションの所有権を移動し、値イテレータを生成します。

iter() メソッドを使用して借用イテレータを作成する:

let vec = vec![1, 2, 3, 4, 5];
let iter = vec.iter();

iter_mut() メソッドを使用して可変借用イテレータを作成する:

let mut vec = vec![1, 2, 3, 4, 5];
let iter_mut = vec.iter_mut();

into_iter() メソッドを使用して所有権を取得するイテレータを作成する:

let vec = vec![1, 2, 3, 4, 5];
let into_iter = vec.into_iter();

例

let v = vec![1, 2, 3];
let mut iter = v.iter();

assert_eq!(iter.next(), Some(&1));
assert_eq!(iter.next(), Some(&2));
assert_eq!(iter.next(), Some(&3));
assert_eq!(iter.next(), None);  // イテレーション終了

イテレータのメソッド

Rust のイテレータは、コレクション内の要素を処理するための豊富なメソッドを提供しています。一般的なメソッドには次のようなものがあります:

  • map():各要素に指定された変換関数を適用します。
  • filter():指定された条件に基づいてコレクション内の要素をフィルタリングします。
  • fold():コレクション内の要素を累積処理します。
  • skip():指定された数の要素をスキップします。
  • take():指定された数の要素を取得します。
  • enumerate():各要素にインデックスを提供します。
  • ......

map() メソッドを使用して各要素を変換する:

let vec = vec![1, 2, 3, 4, 5];
let squared_vec: Vec<i32> = vec.iter().map(|x| x * x).collect();

filter() メソッドを使用して条件に応じて要素をフィルタリングする:

let vec = vec![1, 2, 3, 4, 5];
let filtered_vec: Vec<i32> = vec.into_iter().filter(|&x| x % 2 == 0).collect();

for ループを使用してイテレータを走査する

Rust は、イテレータ内の要素を走査するための for ループ構文を提供しており、より簡潔で直感的な走査方法です。

Rust の for ループは、内部で実際にイテレータを使用しています。

let vec = vec![1, 2, 3, 4, 5];
for &num in vec.iter() {
    println!("{}", num);
}

このループでは、vec.iter() がイテレータを返し、for ループがこのイテレータを走査して各要素を num 変数に代入し、ループ本体のコードを実行します。

消費型アダプタ

イテレータが完全に消費されるまで使用します。

イテレータには、イテレータを消費できるメソッドが多数あります。これらは反復を実行して最終結果(合計やコレクションなど)を返し、イテレータ自体を消費します。

  • collect():イテレータをコレクション(ベクタ、ハッシュセットなど)に変換します。
  • sum():イテレータ内のすべての要素の合計を計算します。
  • product():イテレータ内のすべての要素の積を計算します。
  • count():イテレータ内の要素の数を返します。

例

let v = vec![1, 2, 3];
let sum: i32 = v.iter().sum();
assert_eq!(sum, 6);

アダプタ

イテレータアダプタは、メソッドチェーンを使用してイテレータの内容を変更またはフィルタリングでき、すぐに消費することはありません。

  • map():各要素に関数を適用し、新しいイテレータを返します。
  • filter():条件を満たす要素をフィルタリングします。
  • take(n):最初のn個の要素だけを返すイテレータ。
  • skip(n):最初のn個の要素をスキップし、残りの要素のイテレータを返します。
let v = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
assert_eq!(doubled, vec![2, 4, 6, 8, 10]);

イテレータチェーン

複数のイテレータアダプタを連結して、イテレータチェーンを形成できます。

例

use std::iter::Peekable;

let arr = [1, 2, 3, 4, 5];
let mut iter = arr.into_iter().peekable();
while let Some(val) = iter.next() {
    if val % 2 == 0 {
        continue;
    }
    println!("{}", val);
}

コレクタ

collect メソッドを使用して、イテレータの要素を何らかのコレクションに収集します。

let arr = [1, 2, 3, 4, 5];
let sum: i32 = arr.into_iter().sum();

遅延評価

前述のとおり、Rust のイテレータは遅延評価です。つまり、map()、filter() などはすぐに操作を実行せず、collect() のような消費型メソッドを呼び出すまで実際にはデータを処理しません。これにより、不要な計算を回避できるため、イテレータの処理がより効率的になります。

カスタムイテレータ

独自の型に Iterator trait を実装することもできます。next() メソッドを定義するだけです。

たとえば、1 から 5 までの簡単なイテレータを実装します:

例

struct Counter {
    count: usize,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = usize;
   
    fn next(&mut self) -> Option<Self::Item> {
        self.count += 1;
        if self.count <= 5 {
            Some(self.count)
        } else {
            None
        }
    }
}

let mut counter = Counter::new();
while let Some(num) = counter.next() {
    println!("{}", num);  // 1 から 5 を出力
}

並列イテレータ

マルチスレッド環境で操作を並列化する必要がある場合、rayon crate が並列イテレータのサポートを提供します。.iter() の代わりに .par_iter() を使用することで、マルチスレッド環境でイテレータ操作を高速化できます。

イテレータとライフタイム

イテレータのライフタイムは、反復対象の要素のライフタイムに関連付けられています。イテレータは要素を借用することも、要素の所有権を取得することもできます。これはイテレータの実装においてライフタイムパラメータによって制御されます。

イテレータとクロージャ

イテレータアダプタはクロージャと一緒に使われることがよくあります。クロージャを使用すると、イテレータ操作にカスタムロジックを提供できます。

イテレータとパフォーマンス

イテレータは通常非常に効率的です。コンパイラが最適化を行えるためです。たとえば、コンパイラはイテレータアダプタの呼び出しをインライン化でき、イテレータの遅延評価特性を利用できます。

例

次の例では、イテレータを使用して配列を走査し、配列内の要素を出力する方法を示します。

例

// メイン関数
fn main() {
    // 整数を含む配列を定義する
    let numbers = vec![1, 2, 3, 4, 5];

    // イテレータを使用して配列を走査し、各要素を出力する
    println!("Iterating through the array:");
    for num in numbers.iter() {
        println!("{}", num);
    }

    // イテレータの map メソッドを使用して配列内の各要素を2乗し、結果を新しい配列に収集する
    let squared_numbers: Vec<i32> = numbers.iter().map(|x| x * x).collect();

    // 2乗後の配列を出力する
    println!("Squared numbers: {:?}", squared_numbers);
}

上記のコードでは、まず整数を含む配列を定義しnumbers、次にiter()メソッドを使用して配列のイテレータを取得し、forループでイテレータを走査し、配列内の各要素を出力します。次に、イテレータのmap()メソッドを使用して配列内の各要素を2乗し、collect()メソッドを使用して結果を新しい配列にsquared_numbers収集します。最後に2乗後の配列を出力しました。

プログラムを実行すると、元の配列の各要素と、2乗演算後の新しい配列が出力されることがわかります:

Iterating through the array:
1
2
3
4
5
Squared numbers: [1, 4, 9, 16, 25]

この例は、Rust におけるイテレータの基本的な使用方法、すなわち走査、変換、結果の収集を示しています。

以下の例では、filter() メソッドを使用して配列をフィルタリングし、フィルタリング後の結果を出力します。

例

// メイン関数
fn main() {
    // 整数を含む配列を定義する
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // イテレータの filter メソッドを使用して配列をフィルタリングし、偶数を抽出する
    let even_numbers: Vec<i32> = numbers.iter().filter(|&x| x % 2 == 0).cloned().collect();

    // フィルタリング後の結果を出力する
    println!("Even numbers: {:?}", even_numbers);
}

上記のコードでは、まず整数を含む配列を定義しnumbers、次に、イテレータのfilter()メソッドを使用して配列をフィルタリングし、その中の偶数を抽出します。filter()メソッドのクロージャ内では、剰余演算を使用して要素が偶数かどうかを判定します。最後にcloned()メソッドを使用して各偶数の値をクローンし、collect()メソッドを使用して結果を新しい配列に収集しますeven_numbers。最終的にフィルタリング後の結果を出力しました。

プログラムを実行すると、配列内のすべての偶数が出力されることがわかります。

Even numbers: [2, 4, 6, 8, 10]

この例は、Rustのイテレータのfilter()メソッドの使用方法と、他のメソッドを組み合わせて配列のフィルタリング操作を実現する方法を示しています。

Rust イテレータのメソッド

以下は、Rustでよく使われるイテレータメソッドと、その簡単な説明および例です。

メソッド名 説明 例
next() イテレータ内の次の要素を返します。 let mut iter = (1..5).into_iter(); while let Some(val) = iter.next() { println!("{}", val); }
size_hint() イテレータ内の残り要素数の下限と上限を返します。 let iter = (1..10).into_iter(); println!("{:?}", iter.size_hint());
count() イテレータ内の要素数を数えます。 let count = (1..10).into_iter().count();
nth() イテレータ内の n 番目の要素を返します。 let third = (0..10).into_iter().nth(2);
last() イテレータ内の最後の要素を返します。 let last = (1..5).into_iter().last();
all() イテレータ内のすべての要素が条件を満たす場合に返すtrue。 let all_positive = (1..=5).into_iter().all(|x| x > 0);
any() イテレータ内の少なくとも1つの要素が条件を満たす場合に返すtrue。 let any_negative = (1..5).into_iter().any(|x| x < 0);
find() イテレータ内で条件を満たす最初の要素を返します。 let first_even = (1..10).into_iter().find(|x| x % 2 == 0);
find_map() イテレータ内の要素に関数を適用し、最初に返されたSome結果を返します。 let first_letter = "hello".chars().find_map(|c| if c.is_alphabetic() { Some(c) } else { None });
map() イテレータ内の各要素に関数を適用します。 let squares: Vec<i32> = (1..5).into_iter().map(|x| x * x).collect();
filter() イテレータ内で条件を満たす要素を保持します。 let evens: Vec<i32> = (1..10).into_iter().filter(|x| x % 2 == 0).collect();
filter_map() イテレータ内の要素に関数を適用し、関数がSomeを返した場合、結果を保持します。 let chars: Vec<char> = "hello".chars().filter_map(|c| if c.is_alphabetic() { Some(c.to_ascii_uppercase()) } else { None }).collect();
map_while() イテレータ内の要素に関数を適用し、関数がNone。 let first_three = (1..).into_iter().map_while(|x| if x <= 3 { Some(x) } else { None });
take_while() イテレータから条件を満たす要素を、条件を満たさなくなるまで取り出します。 let first_five = (1..10).into_iter().take_while(|x| x <= 5).collect::<Vec<_>>()
skip_while() イテレータ内で条件を満たす要素を、条件を満たさなくなるまでスキップします。 let odds: Vec<i32> = (1..10).into_iter().skip_while(|x| x % 2 == 0).collect();
for_each() イテレータ内の各要素に対して何らかの操作を実行します。 let mut counter = 0; (1..5).into_iter().for_each(|x| counter += x);
fold() イテレータ内の要素をアキュムレータを使用して畳み込みます。 let sum: i32 = (1..5).into_iter().fold(0, |acc, x| acc + x);
try_fold() イテレータ内の要素を畳み込みます。エラーが発生した場合は途中で返る可能性があります。 let result: Result = (1..5).into_iter().try_fold(0, |acc, x| if x == 3 { Err("Found the number 3") } else { Ok(acc + x) });
scan() イテレータ内の要素を状態を持つ畳み込みを実行します。 let sum: Vec<i32> = (1..5).into_iter().scan(0, |acc, x| { *acc += x; Some(*acc) }).collect();
take() イテレータから最大 n 個の要素を取り出します。 let first_five = (1..10).into_iter().take(5).collect::<Vec<_>>()
skip() イテレータ内の最初の n 個の要素をスキップします。 let after_five = (1..10).into_iter().skip(5).collect::<Vec<_>>()
zip() 2つのイテレータの要素をタプルにまとめます。 let zipped = (1..3).zip(&['a', 'b', 'c']).collect::<Vec<_>>()
cycle() イテレータ内の要素を無限に繰り返します。 let repeated = (1..3).into_iter().cycle().take(7).collect::<Vec<_>>()
chain() 複数のイテレータを連結します。 let combined = (1..3).chain(4..6).collect::<Vec<_>>()
rev() イテレータ内の要素の順序を反転します。 let reversed = (1..4).into_iter().rev().collect::<Vec<_>>()
enumerate() イテレータ内の各要素にインデックスを追加します。 let enumerated = (1..4).into_iter().enumerate().collect::<Vec<_>>()
peeking_take_while() 条件を満たす要素を取り出しつつ、イテレータの状態を保持するため、後続の要素も取り出し続けることができます。 let (first, rest) = (1..10).into_iter().peeking_take_while(|&x| x < 5);
step_by() 指定されたステップごとにイテレータ内の要素を返します。 let even_numbers = (0..10).into_iter().step_by(2).collect::<Vec<_>>()
fuse() 追加のイテレータを作成し、イテレータが尽きた後でもnext()メソッドを呼び出すことができます。 let mut iter = (1..5).into_iter().fuse(); while iter.next().is_some() {}
inspect() 各要素を取り出すときにクロージャを実行しますが、要素は変更しません。 let mut counter = 0; (1..5).into_iter().inspect(|x| println!("Inspecting: {}", x)).for_each(|x| println!("Processing: {}", x));
same_items() 2つのイテレータが同じ要素シーケンスを生成するかどうかを比較します。 let equal = (1..5).into_iter().same_items((1..5).into_iter());

まとめ

Rustのイテレータは、強力で柔軟なツールであり、宣言的な方法でシーケンスを処理できます。イテレータの設計は、安全性、パフォーマンス、表現力を考慮しており、Rust言語の主要な特性の1つです。イテレータを使用することで、Rustプログラマは安全で効率的なコードを書くことができます。

その他の拡張