LoginSignup
7
4

More than 5 years have passed since last update.

Array#each のブロック内で配列の長さを変更したらどうなる?

Last updated at Posted at 2015-11-25

あまり使うシチュエーションもなさそうだが、興味があったので調べてみた。
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

前節で予想した内容と一致した。

7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4