あまり使うシチュエーションもなさそうだが、興味があったので調べてみた。
Ruby のバージョンは下記の通り。
$ ruby -v
ruby 2.2.2p95 (2015-04-13 revision 5Th0295) [x86_64-darwin14]
実験
a = (0..9).to_a
b = (10..14).to_a
a.each do |i|
if i == 0
a.concat(b) # 1 回目のループで末尾に要素追加
end
puts i
end
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
末尾に追加した要素も列挙されるようだ。
最初のほうの要素を削除したらどうなるだろう。
a = (0..9).to_a
a.each do |i|
if i == 0
a[0..1] = [] # 1 回目のループで最初の 2 要素を削除
end
puts i
end
0
3
4
5
6
7
8
9
1 度目のブロック実行後、a は [2, 3, 4, 5, 6, 7, 8, 9]
になっている。
つまり 2 度目のブロックには a[1]
が渡されているようだ。
これらの挙動から次のことが予想できる。
- ブロック実行毎にインデックスをインクリメントして、ブロック実行直前に要素を取り出している。
- 終了条件のチェックでは毎回、配列の長さを調べ直している。
実装を確認してみよう。
実装の確認
array.c というファイルがあるので、ここに実装がありそう。これかな。
// https://github.com/ruby/ruby/blob/trunk/array.c#L1808
VALUE
rb_ary_each(VALUE ary)
{
long i;
RETURN_SIZED_ENUMERATOR(ary, 0, 0, ary_enum_length);
for (i=0; i<RARRAY_LEN(ary); i++) {
rb_yield(RARRAY_AREF(ary, i));
}
return ary;
}
// いちおう rb_define_method で Ruby のメソッドと対応してることを確認する。
// https://github.com/ruby/ruby/blob/trunk/array.c#L5810
rb_define_method(rb_cArray, "each", rb_ary_each, 0);
RETURN_SIZED_ENUMERATOR
は block がなければ Enumerator を返すマクロっぽい。
RARRAY_LEN
は配列の要素数の取得、RARRAY_AREF
はインデックスから要素を取得するマクロだろう (名称と文脈からの推測)。
Ruby で書けばこんな感じだ。
ただし、実際には length や size メソッドを呼んでいるわけではないので、これらのメソッドを上書きしても挙動は変わらない。
class Array
def each
return enum_for unless block_given?
i = 0
while i < length
yield self[i]
i += 1
end
end
end
前節で予想した内容と一致した。