やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

【Rust】for文でiter関数を使う時に&(参照)をなぜ書くのか

rustの公式ドキュメントであるThe Rust Programming Language 日本語版を読み進めていると、第10章のジェネリックなデータ型の章で、以下のコードが記載されていました。

fn largest_i32(list: &[i32]) -> i32 {
    let mut largest = list[0];

    for &item in list.iter() {
        if item > largest {
            largest = item;
        }
    }

    largest
}

処理内容はシンプルで引数に与えられたスライスから最大値を線形探索して返すというものです。

しかし、for &item in iter()と書くのはなぜでしょうか。for item in iter()ではダメなのでしょうか。
結論としては参照外しと呼ばれる機能を上手く使うために、for &item in iter()としていました。参照外しが行われることで&itemは参照が消えて&i32ではなくi32型となるのです。

for文の秘密

まずですが、for文はいわゆる糖衣構文(syntax sugar)であり、内部では以下のようなコードが実行されます。

{
    let result = match IntoIterator::into_iter(iter_expr) {
        mut iter => 'label: loop {
            let mut next;
            match Iterator::next(&mut iter) {
                Option::Some(val) => next = val,
                Option::None => break,
            };
            let PATTERN = next;
            let () = { /* loop body */ };
        },
    };
    result
}

Loop expressions - The Rust Reference

for文が呼び出されれると内部でIntoIteratorトレイトに定義されているinto_iter関数が呼び出されて構造体IntoIterが返されます。今回はvec型に定義されている構造体IntoIterが返されます。

pub trait IntoIterator {
    // 長いので省略
    fn into_iter(self) -> Self::IntoIter;
}

rust/collect.rs at master · rust-lang/rust · GitHub

vec型の場合に返される構造体IntoIterの定義です。

pub struct IntoIter<
    T,
    #[unstable(feature = "allocator_api", issue = "32838")] A: Allocator = Global,
> {
    pub(super) buf: NonNull<T>,
    // 長いので省略
    pub(super) end: *const T,
}

rust/into_iter.rs at master · rust-lang/rust · GitHub

into_iter関数が返す値の型を確認してみると、vec型に定義されているIntoIterが返ってくることが分かります。

fn type_of<T>(_: T) -> String{
    let a = std::any::type_name::<T>();
    return a.to_string();
}

fn main() {
    let v = vec![1,2,3];
    println!("type: {}", type_of(v.into_iter()));
}

// type: alloc::vec::into_iter::IntoIter<i32>

pineplanter.moo.jp

すなわちfor文はfor x in y(=vec型)であれば、vec型に用意されたIntoIterが使用されます。しかし、今回は事前にyの部分がy.iter()となっていることに注意が必要です。

for &item in list.iter()

vec型のiter関数が返すもの

先ほどのinto_iter関数を考えれば、iter関数を実行した時もvec型に用意されている何らかの構造体が返ってくるのではないかと思います。しかしながら不思議なことに、iter関数をvec型に対して実行したときは何とslice型に用意された構造体Iterが返ってきます。

fn main() {
  let v = vec![1,2,3];
  println!("type: {}", type_of(v.iter()));
}

// type: core::slice::iter::Iter<i32> なんとsliceのIterに!

この謎については後日、別の記事に記載をしようと思います。
そしてslice型の構造体Iternext関数が呼び出される事に&T型の値を返します。

fn main() {
  for x in vec![1, 2, 3].iter() {
    println!("type_of: {}", type_of(x));
  }
}

// type_of: &i32
// type_of: &i32
// type_of: &i32

参照外し(destructure the reference)について

残る謎はfor &x in y.iter()と記述した際にxの型が&TではなくTになる理由ですが、これは「参照外し」と呼ばれる参照が自動で外される現象によるものです。

doc.rust-lang.org

(※destructure the referenceと記載があるので、直訳すると参照破壊になります)

参照外しが行われるケースはいくつかありますが、今回の例はパターンマッチによる参照外しです。興味深いことに&T型の値をlet &xで宣言すると、&T型ではなくT型の値として束縛されます。

fn main() {
  let x = 190;
  let borrow_x = &x;
  let &destructuring_x = &x;

  println!("x: {}", type_of(x));
  println!("borrow_x: {}", type_of(borrow_x));
  println!("destructuring_x: {}", type_of(destructuring_x));
}

// x: i32
// borrow_x: &i32
// destructuring_x: i32

「こんな宣言どこでしてるの?」と思われるかもしれませんが、冒頭に添付したfor文が内部で実行しているコードをもう一度、見てみます。

{
    let result = match IntoIterator::into_iter(iter_expr) {
        mut iter => 'label: loop {
            let mut next;
            match Iterator::next(&mut iter) {
                Option::Some(val) => next = val,
                Option::None => break,
            };
            let PATTERN = next;
            let () = { /* loop body */ };
        },
    };
    result
}

コード内のPATTERNにはfor x in yxの部分が適応されます。

'label: for PATTERN in iter_expr {
    /* loop body */
}

よって、for &x in y.iter()とすればPATTERNには&xが適応されて、さらにiter関数が実行された場合には&Tで返ってくる値に対してlet &xという宣言がされたことになり、先ほどの参照外しが発生するコードになります。

let &x = &T // x: T型

まとめ

  • for文は糖衣構文で内部でIntoIteratorinto_iter関数を呼び出している
  • into_iterは各データ型に定義された構造体IntoIterを返す
  • vec型の場合にはiter関数を実行するとslice型に定義された構造体Iterが返る
  • slice型のiternext関数が呼び出される毎に&T型の値を返す
  • let &x = &i32のようなパターンマッチでは参照外しが発生する
  • 参照外しを利用するために、for &item in list.iter()と宣言していた

ここまで深く調べたのは久々で、rust本体のコードを読んだため非常に勉強になりました。やはりOSSのコードを読むのは良いですね。

www.okb-shelf.work

参考文献