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>
すなわち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
型の構造体Iter
はnext
関数が呼び出される事に&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
になる理由ですが、これは「参照外し」と呼ばれる参照が自動で外される現象によるものです。
(※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 y
のx
の部分が適応されます。
'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
文は糖衣構文で内部でIntoIterator
のinto_iter
関数を呼び出しているinto_iter
は各データ型に定義された構造体IntoIter
を返すvec
型の場合にはiter
関数を実行するとslice
型に定義された構造体Iter
が返るslice
型のiter
はnext
関数が呼び出される毎に&T
型の値を返すlet &x = &i32
のようなパターンマッチでは参照外しが発生する- 参照外しを利用するために、
for &item in list.iter()
と宣言していた
ここまで深く調べたのは久々で、rust
本体のコードを読んだため非常に勉強になりました。やはりOSSのコードを読むのは良いですね。