Перечислитель `Array#each` 's {block} не всегда может изменять значения массива?

Хорошо, может быть, это просто, но... учитывая это:

arr = ("a".."z").to_a

arr

=> ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]

... и что я пытаюсь изменить все значения "arr" на "плохие"

почему не это работает?

arr.each { |v| v = "bad" }

arr

=> ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]

Ответы предполагали, что «v» является локальной переменной для блока («копия» значения массива), и я полностью понимаю это (и никогда не озадачивал меня раньше), но затем

.. почему это работает, если элементы массива являются объектами?

class Person
  def initialize
    @age = 0
  end
  attr_accessor :age
end

kid = Person.new
man = Person.new
arr = [kid, man]


arr.each { |p| p.age = 50 }

arr[0]
=> #<Person:0xf98298 @age=50>

Разве здесь "p" все еще не является локальным для этого блока? но тогда это действительно влияет на объекты, почему?


person jj_    schedule 23.10.2012    source источник
comment
v — это локальная переменная, связанная с значением, которое было в индексе. Это не ссылка на оригинальный слот массива!   -  person    schedule 23.10.2012
comment
@pst, пожалуйста, проверьте правки, спасибо   -  person jj_    schedule 23.10.2012
comment
Объекты тоже являются ценностями! — Ruby использует Вызов по объекту (совместное использование ), что объясняет побочные эффекты мутации - это то же самое значение/объект! Ключевое значение имеет то, что переменная не является значением.   -  person    schedule 23.10.2012


Ответы (4)


Я расширю комментарий @pst:

почему это не работает?

arr.each { |v| v = "bad" }

Поскольку each выполняет итерацию по массиву и помещает каждый элемент в блок, указанный вами как локальную переменную v, поскольку v не является ссылкой на массив arr.

new_arr = arr.each { |v| v = "bad" }

each не возвращает массив, для этого вы должны использовать map (см. ответ @benjaminbenben). Поэтому его назначение не «работает».

arr.each { |v| arr[arr.index v] = "bad" }

Здесь вы помещаете каждый элемент в arr в локальную переменную v, но вы также ссылаетесь на сам массив в блоке, поэтому вы можете присваивать массиву и использовать локальную переменную v для найдите индекс, соответствующий содержимому v (но вы можете обнаружить, что это не сработает, как вы ожидаете, если элементы не все уникальны).

arr.each { |p| p.age = 50 }

kid.age #-> 50

Здесь вы снова заполнили локальную переменную p каждым элементом/объектом в arr, но затем вы получили доступ к каждому элементу через метод, поэтому вы можете изменить этот элемент - вы не изменяете массив< /сильный>. Это отличается, потому что ссылка относится к содержимому локальной переменной, которую вы перепутали со ссылкой на массив. Это отдельные вещи.


В ответ на комментарий ниже:

arr[0]
# => #<Person:0xf98298 @age=50>

Все дело в том, кто на кого когда ссылается.

Попробуй это:

v = Person.new
# => #<Person:0x000001008de248 @age=0>
w = Person.new
# => #<Person:0x000001008d8050 @age=0>
x = v
# => #<Person:0x000001008de248 @age=0>
v = Person.new
# => #<Person:0x00000100877e80 @age=0>
arr = [v,w,x]
# => [#<Person:0x00000100877e80 @age=0>, #<Person:0x000001008d8050 @age=0>, #<Person:0x000001008de248 @age=0>]

v ссылается на 2 разных объекта. v - это не фиксированная вещь, это имя. Сначала это относится к #<Person:0x000001008de248 @age=0>, затем к #<Person:0x00000100877e80 @age=0>.

Теперь попробуйте следующее:

arr.each { |v| v = "bad" }
# => [#<Person:0x00000100877e80 @age=0>, #<Person:0x000001008d8050 @age=0>, #<Person:0x000001008de248 @age=0>]

Все они являются объектами, но ничего не обновлялось и не «работало». Почему? Потому что при первом входе в блок v относится к элементу в массиве, который был получен (дан). Итак, на первой итерации v равно #<Person:0x00000100877e80 @age=0>.

Но затем мы присваиваем "bad" v. Мы не присваиваем "bad" первому индексу массива, потому что мы вообще не ссылаемся на массив. arr — это ссылка на массив. Поместите arr внутрь блока, и вы сможете изменить его:

arr.each { |v| 
  arr[0] = "bad" # yes, a bad idea!
}

Почему же тогда arr.each { |p| p.age = 50 } обновляет элементы в массиве? Потому что p относится к объектам, которые также оказались в массиве. В первой итерации p ссылается на объект, также известный как kid, а kid имеет метод age=, и вы вставляете в него 50. kid также является первым элементом в массиве, но вы говорите о kid, а не о массиве. Вы можете сделать это:

arr.each { |p| p = "bad"; p.age }
NoMethodError: undefined method `age' for "bad":String

Сначала p ссылался на объект, который также оказался в массиве (откуда он был получен), но затем p стал ссылаться на "bad".

each перебирает массив и возвращает значение на каждой итерации. Вы получаете только значение не массива. Если вы хотите обновить массив, вы либо делаете:

new_arr = arr.map{|v| v = "bad" }
new_arr = arr.map{|v| "bad" } # same thing

or

arr.map!{|v| v = "bad"}
arr.map!{|v| "bad"}  # same thing

поскольку map возвращает массив, заполненный возвращаемым значением блока. map! обновит ссылку, по которой вы его вызвали, массивом, заполненным возвращаемым значением блока. Как правило, в любом случае обновлять объект при его повторении — плохая идея. Я считаю, что всегда лучше думать об этом как о создании нового массива, а затем вы можете использовать методы ! в качестве ярлыка.

person iain    schedule 23.10.2012
comment
я опечатался в своем тестовом запросе: kid.age #=› 50 Вместо этого было arr[0] # =› #‹Person:0xf98298 @age=50› . Так разве это не показывает, что фактический массив содержит измененное значение? - person jj_; 23.10.2012
comment
@lain я отредактировал вопрос с изменениями, чтобы вы могли лучше прочитать его, если хотите, спасибо - person jj_; 23.10.2012
comment
@lain спасибо, теперь все ясно, спасибо за потраченное на это время! :) Мне очень нужно подробное объяснение! Голосование за лучший ответ здесь и надежда, что другим людям будет полезен ваш ответ! Молодец! - person jj_; 23.10.2012
comment
Нет проблем, рад, что смог помочь. - person iain; 23.10.2012

В примере

arr.each { |v| v = "bad" }

"v" - это просто ссылка на строку, когда вы делаете v = "bad", вы переназначаете локальную переменную. Сделать все плохо можно так:

arr.each { |v| v.replace "bad" }

В следующий раз ты сможешь поиграть с Object#object_id

puts arr[0].object_id #will be save as object_id in first iteration bellow
arr.each { |v| puts v.object_id }
person Pavel Evstigneev    schedule 23.10.2012
comment
хорошо, я не знал метода .replace :) +1 - person jj_; 24.10.2012

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

arr.map { "bad" }

=> ["bad", "bad", "bad", "bad", …] 

использование .map! изменит содержимое исходного массива, а не вернет новый.

person benjaminbenben    schedule 23.10.2012
comment
спасибо, я, вероятно, пойду по этому пути, но все же я задаюсь вопросом, почему мое первое решение работает в одном случае, а не в другом (проверьте мое редактирование) спасибо - person jj_; 23.10.2012
comment
ах! Ответ @Iain, похоже, довольно хорошо это описывает! - person benjaminbenben; 23.10.2012

Как насчет этого

arry = Array.new(arry.length,"bad")

Это установит значение по умолчанию "плохой" в arry.length.

person Viren    schedule 23.10.2012
comment
что за..! Я могу поверить, что пропустил это! Это самый простой, удобный и элегантный способ решить проблему, которую я пытался решить в первый раз! Спасибо! В следующий раз я потрачу больше времени на чтение основных классов. Новый метод! Голосую! - person jj_; 23.10.2012
comment
p.s.: если вы используете partesys для Array.new (параметр), помните, что они должны идти после .new без пробелов! -> Class.new(параметр). Если у вас есть пробел между .new и (параметр), вы получите синтаксическую ошибку. Просто подумал, что было бы полезно напомнить об этом (сначала себе): - person jj_; 23.10.2012