2012-01-27 2 views
6

Я использую Ruby, 1.9.2-P290 и нашел:Почему поведение array.each зависит от синтаксиса Array.new?

a = Array.new(2, []).each {|i| i.push("a")}  
=> [["a", "a"], ["a", "a"]] 

, который не то, что я бы ожидать. Но следующий стиль конструктора делает то, что я ожидаю:

b = Array.new(2) {Array.new}.each {|i| i.push("b")} 
=> [["b"], ["b"]] 

Является ли первый пример ожидаемым поведением?

В ruby-doc это похоже на мой аргумент size=2 - это тот же аргумент для обоих конструкторов. Я думаю, что если метод each получает этот аргумент, он будет использовать его одинаково для обоих конструкторов.

ответ

10

Это распространенное недоразумение. В первом примере вы создаете массив с двумя элементами. Оба они являются указателями на то же самое массив. Итак, когда вы перебирать ваш внешний массив вы добавляете 2 элемента к внутреннему массиву, который затем отражается в вашем выводе дважды

Сравните этот:

> array = Array.new(5, []) 
=> [[], [], [], [], []] 

# Note - 5 identical object IDs (memory locations) 
> array.map { |o| o.object_id } 
=> [70228709214620, 70228709214620, 70228709214620, 70228709214620, 70228709214620] 

> array = Array.new(5) { [] } 
=> [[], [], [], [], []] 

# Note - 5 different object IDs (memory locations) 
> array.map { |o| o.object_id } 
=> [70228709185900, 70228709185880, 70228709185860, 70228709185840, 70228709185780] 
4

В первом случае вы используете один экземпляр массива как значение по умолчанию для элементов основного массива:

a = Array.new(2, []).each {|i| i.push("a")} 

Второй аргумент просто переработаны, поэтому push применяется к тому же экземпляру дважды. Вы только создали один экземпляр здесь, тот, который поставляется в качестве аргумента, поэтому он используется снова и снова.

Второй метод является правильным способом сделать это:

b = Array.new(2) {Array.new}.each {|i| i.push("b") 

Это намеренно создает новый экземпляр массива для каждой позиции в главном массиве. Важным отличием здесь является использование блока { ... }, который выполняется один раз для каждой позиции в новом массиве. Короткая форма версии это будет:

b = Array.new(2) { [ ] }.each {|i| i.push("b") 
1

Из документации рубина:

new(size=0, obj=nil) 
new(array) 
new(size) {|index| block } 

Возвращает новый массив. В первом виде новый массив пуст. Во втором создается с копиями размера obj (то есть, ссылки на размер для одного и того же объекта). Третья форма создает копию массива, переданного в качестве параметра (массив генерируется путем вызова to_ary по параметру). В последней форме создается массив заданного размера.

Таким образом, в a массиве вы создаете, у вас есть две ссылки на тот же массив, поэтому push работает на них обоих. То есть вы нажимаете "a" на один и тот же массив дважды. В массиве b, который вы создаете, вы фактически создаете новый массив для каждого элемента.

Смежные вопросы