🦀

TS使っている人のRust0から勉強日記3

に公開

こんにちは!普段フロントエンド開発をしているまるです🙌
Rustの日記2を書いて一年経ってました….

今回はRustの配列やコレクションをループする仕組みであるiter()とfor文についてです。


そもそも配列とコレクションの違い

上の内容紹介のところで出てきた「配列」と「コレクション」とは何が違うのかについて説明します。

名前 Rustでの型 サイズの性質 使う場面 メモリ
配列(array) [T;N] 固定サイズ 要素数が変わらない時 スタック領域
コレクション(collection) Vec<T>など 可変サイズ 要素を増やしたり減らしたりする時 ヒープ領域
スタック領域とヒープ領域の特徴
項目 スタック ヒープ
サイズ 小さい(固定) 大きい(可変)
確保・解放 自動(関数の呼び出しにより) 所有権やBoxで制御
アクセス速度 高速 やや遅い(ポインタを経由)
メモリ管理 自動 所有権により自動

Rustの配列とは、「サイズが決まっていて、変えられないもの」です。

let nums = [1,2,3];

Typescriptで言うと、下のように型定義で固定の要素の数のみで指定されたもののことですね。

const nums:[number,number,number] = [1,2,3]

Rustのコレクションとは、標準ライブラリで提供される「可変データ構造」の総称です。
よく使われるのはVec<T>やStringなど

let mut numbers = Vec::new();
numbers.push(1);
numbers.push(2);
numbers.push(3);
println!("{:?}", numbers); // [1, 2, 3]

Typescriptで言うと、

const numbers: number[] = [];
numbers.push(1);
numbers.push(2);
numbers.push(3);
console.log(numbers); // [1, 2, 3]

iter()の使い方

iter()

iter()とは「コレクションの要素を順番に取り出せるイテレーター(仕組み)を作る」というメゾットです。
配列やコレクションの要素に対して何かしらの処理を行いたい時に使います。

代表的なメソッド一覧

メソッド 説明 返り値
map 各要素に関数を適用 新しいイテレーター
filter 条件を満たす要素だけ残す 新しいイテレーター
collect イテレーターをコレクションに変換 コレクション(Vec, HashMapなど)
sum 要素の合計を計算 値(合計)
product 要素の積を計算 値(積)
enumerate インデックス付きのタプルにする 新しいイテレーター
take 先頭n個の要素を取得 新しいイテレーター
skip 先頭n個を飛ばす 新しいイテレーター
find 条件に合う最初の要素を取得 Option<&T>
all 全ての要素が条件を満たすか bool
any どれか1つでも条件を満たすか bool
max 最大値を取得 Option<&T>
min 最小値を取得 Option<&T>
chain 他のイテレーターと連結 新しいイテレーター
zip 2つのイテレーターを組にする 新しいイテレーター(タプル)
flat_map イテレーターを平坦化して別のイテレーターに変換 新しいイテレーター
peekable 次の要素を覗き見可能にする Peekableイテレーター
rev 逆順にイテレーション 新しいイテレーター
for_each 各要素に関数を適用(副作用用) ()(値は返さない)

mapの使い方を例とします。

let numbers = vec![1, 2, 3, 4, 5];

let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect(); //NG
    
let doubled: Vec<i32> = numbers.iter().map(|x| *x * 2).collect();
// または
let doubled: Vec<i32> = numbers.iter().cloned().map(|x| x * 2).collect();
// または
let doubled: Vec<i32> = numbers.iter().copied().map(|x| x * 2).collect();
その他のメソッド一覧のコード例
fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    let other_numbers = vec![6, 7, 8];

    // filter: 偶数だけを取り出して新しいベクタにする(clonedで値にする)
    let even: Vec<i32> = numbers.iter().filter(|x| **x % 2 == 0).cloned().collect();
    println!("filter: {:?}", even); // [2, 4]

    // collect: 各要素を2乗して新しいベクタにする(copiedで値にする例)
    let squared: Vec<i32> = numbers.iter().copied().map(|x| x * x).collect();
    println!("collect: {:?}", squared); // [1, 4, 9, 16, 25]

    // sum: 合計を計算(copiedで値にして合計)
    let total: i32 = numbers.iter().copied().sum();
    println!("sum: {}", total); // 15

    // product: 積を計算(copiedで値にして積)
    let prod: i32 = numbers.iter().copied().product();
    println!("product: {}", prod); // 120

    // enumerate: インデックスと値を表示
    for (i, x) in numbers.iter().enumerate() {
        println!("enumerate: {} -> {}", i, x);
    }

    // take: 先頭3つだけ取り出す(clonedで値にする)
    let first_three: Vec<i32> = numbers.iter().take(3).cloned().collect();
    println!("take: {:?}", first_three); // [1, 2, 3]

    // skip: 先頭2つを飛ばして残りを取り出す(clonedで値にする)
    let after_two: Vec<i32> = numbers.iter().skip(2).cloned().collect();
    println!("skip: {:?}", after_two); // [3, 4, 5]

    // find: 3より大きい最初の要素を探す(**xで値にする)
    if let Some(x) = numbers.iter().find(|x| **x > 3) {
        println!("find: {}", x); // 4
    }

    // all: 全て正の数か?
    let all_positive = numbers.iter().all(|x| *x > 0);
    println!("all: {}", all_positive); // true

    // any: 偶数が1つでもあるか?
    let any_even = numbers.iter().any(|x| *x % 2 == 0);
    println!("any: {}", any_even); // true

    // max: 最大値(copiedで値にしてmax)
    if let Some(max_val) = numbers.iter().copied().max() {
        println!("max: {}", max_val); // 5
    }

    // min: 最小値(copiedで値にしてmin)
    if let Some(min_val) = numbers.iter().copied().min() {
        println!("min: {}", min_val); // 1
    }

    // chain: 2つのベクタをつなげて1つのベクタにする(clonedで値にする)
    let combined: Vec<i32> = numbers.iter().chain(other_numbers.iter()).cloned().collect();
    println!("chain: {:?}", combined); // [1, 2, 3, 4, 5, 6, 7, 8]

    // zip: 数字と文字をペアで表示
    let letters = vec!['a', 'b', 'c', 'd', 'e'];
    for (num, letter) in numbers.iter().zip(letters.iter()) {
        println!("zip: {} -> {}", num, letter);
    }

    // flat_map: ネストしたベクタを1つにまとめる(clonedで値にする)
    let nested = vec![vec![1, 2], vec![3, 4], vec![5]];
    let flat: Vec<i32> = nested.iter().flat_map(|v| v.iter()).cloned().collect();
    println!("flat_map: {:?}", flat); // [1, 2, 3, 4, 5]

    // peekable: 次の要素を覗き見できる
    let mut iter = numbers.iter().peekable();
    while let Some(x) = iter.next() {
        if let Some(next_val) = iter.peek() {
            println!("peekable: current: {}, next: {}", x, next_val);
        } else {
            println!("peekable: current: {}, next: None", x);
        }
    }

    // rev: 逆順に並べる(clonedで値にする)
    let reversed: Vec<i32> = numbers.iter().rev().cloned().collect();
    println!("rev: {:?}", reversed); // [5, 4, 3, 2, 1]

    // for_each: すべての要素を表示
    numbers.iter().for_each(|x| println!("for_each: {}", x));
}

最初の例のNGのもの以外、map内の要素に*がついていたり、cloned()やcopy()が書かれていますが、これは所有権が関係してきます。一行目のNGの例を順番に見ていきましょう!

① numbers.iter()は&i32のイテレーターを返します。
= iter()は要素を「値」で取り出すのではなく、参照で取り出します。
② map(|x|x * 2)のxは&i32ですが、x * 2はi32型の値としか計算できません。
Rustでは、参照(&i32)と値(i32)は別物です。
なので、iter()の後に参照ではなく値にするためにcloned()やcopy()、変数の前に*を使用します。

毎回cloned()などをつけないといけないのは、ミスも起こりやすい…ので別の書き方があります!

iter_mut()

mut()とついているのでわかりやすいかと思いますが、これは可変参照イテレータを返します。
要素はそれぞれ&mut Tの形になります。

let mut numbers: Vec<i32> = vec![1, 2, 3, 4, 5];
for x in numbers.iter_mut() {
    *x *= 2; //&mut i32
}

println!("{:?}",numbers); //[2, 4, 6, 8, 10]

参照なのであまり変わったように思えないかもしれませんが、iter()との違いは「値を変更できるか」ということです。
iter()はあくまで読み取りや表示をして別の変数にするのに対して、iter_mut()は変数名を変えずに値の変更を行うことができます。

into_iter()

所有権を移動するイテレーターを返します。
要素はそれぞれTとなり、参照ではなく値そのものになります。

let numbers: Vec<i32> = vec![1, 2, 3, 4, 5];
// into_iter() で所有権を取得して値を加工
let doubled: Vec<i32> = numbers.into_iter().map(|x| x * 2).collect();

println!("{:?}", doubled); // [2, 4, 6, 8, 10]
// println!("{:?}", numbers); // これはもう使えない

値そのものを使用するため、元のコレクションを使用できなくなります。

まとめ

メソッド イテレーターの型 所有権 値の変更 元のコレクションの使用
.iter() &T 取らない ×(読み取り専用) 可能
.iter_mut() &mut T 取らない 〇(書き換え可能) 可能
.into_iter() T 移動する 〇(値そのものを加工可能) 不可(消費される)

for文の書き方

iter()を使用したfor文の書き方例です!

fn main() {
    //1. 配列やベクターの要素を順にループ
    let numbers = [1, 2, 3, 4, 5];
    for n in numbers {
        println!("{}", n);
    }

    let vec_numbers = vec![10, 20, 30];
    for n in &vec_numbers { // 参照を使う場合
        println!("{}", n);
    }

    //2. インデックスと要素を同時に取得 (enumerate) 
    let numbers = vec![5, 10, 15];
    for (i, n) in numbers.iter().enumerate() {
        println!("index: {}, value: {}", i, n);
    }

    //3. 値を書き換えながらループ (iter_mut)
    let mut numbers = vec![1, 2, 3];
    for n in numbers.iter_mut() {
        *n *= 2;
    }
    println!("{:?}", numbers); // [2, 4, 6]

    //4. 所有権ごとループ (into_iter)
    let numbers = vec![1, 2, 3];
    for n in numbers.into_iter() {
        println!("{}", n);
    }
    // numbers はここではもう使えない

    //5. 範囲 (range) を使ったループ
    for i in 0..5 { // 0 から 4 まで
        println!("{}", i);
    }
    for i in 0..=5 { // 0 から 5 まで
        println!("{}", i);
    }

    //6. ネストしたループ
    let matrix = vec![
        vec![1, 2],
        vec![3, 4]
    ];
    for row in &matrix {
        for val in row {
            println!("{}", val);
        }
    }
}

typescriptで書くとこんな感じ
//1. 配列やベクターの要素を順にループ
const numbersArr = [1, 2, 3, 4, 5];
for (const n of numbersArr) {
    console.log(n);
}

const vecNumbers = [10, 20, 30];
for (const n of vecNumbers) { // TypeScriptでは参照の概念はない
    console.log(n);
}

//2. インデックスと要素を同時に取得 (foreach) 
const numbers = [5, 10, 15];
numbers.forEach((n, i) => {
    console.log(`index: ${i}, value: ${n}`);
});

//3. 値を書き換えながらループ
let mutableNumbers = [1, 2, 3];
for (let i = 0; i < mutableNumbers.length; i++) {
    mutableNumbers[i] *= 2;
}
console.log(mutableNumbers); // [2, 4, 6]

//4. 所有権ごとループ (typescriptには所有権の概念は必要ないので似せて作りました)
const numbers2 = [1, 2, 3];
const doubled = numbers2.map(x => x * 2);
for (const n of doubled) {
    console.log(n);
}

//5. 範囲 (range) を使ったループ
// TypeScript には Rust の range 構文はないので、forループで代用
for (let i = 0; i < 5; i++) { // 0から4まで
    console.log(i);
}
for (let i = 0; i <= 5; i++) { // 0から5まで
    console.log(i);
}

//6. ネストしたループ
const matrix = [
    [1, 2],
    [3, 4]
];
for (const row of matrix) {
    for (const val of row) {
        console.log(val);
    }
}

結構Typescriptと似ているので、for文については違和感はなさそう…

まとめ

Rustの配列やコレクションをループする仕組みであるiter()とfor文についてでした。
やっぱりここでも鬼門となるのは所有権…😇
iter()を使う際は、どの書き方が一番いいか見ながら書きましょうね!

Discussion