为什么ruby array.delete_at在array.each中失败?

时间:2015-03-05 01:52:05

标签: ruby arrays iterator

我是ruby的新手,并清楚地看到并在网上发现以下内容失败:

arr = [10, 20, 30, 40]     
arr.each.with_index do |elmt, i|
  print "#{elmt}, #{i}, "
  arr.delete_at(i) if elmt == 20
  puts arr.length
end

显然,delete_at正在与迭代器进行交互,但我无法找到迭代器和delete_at如何工作的明确描述,以便这样做。 (顺便说一句,我理解有效的解决方案 - 我不是在寻找一种正确的方法来实现这一点,我正在努力理解语义,以便我知道为什么这不符合预期。) 为了完整性,这是输出

10, 0, 4
20, 1, 3
40, 2, 3
=> [10, 30, 40]

3 个答案:

答案 0 :(得分:1)

<强>灯泡!!

似乎很清楚delete_at会立即调整基础数据结构,左移所有元素到已删除项目的右侧。该数组看起来更像是一个链表而不是一个数组。所以“下一个”项(这里是“30”)现在是arr [1](迭代器已经处理过),当迭代器递增到arr [2]时,它看到“40”。 arr [3]返回nil并且看起来似乎对nil无效。

答案 1 :(得分:1)

看看: https://github.com/ruby/ruby/blob/ca6b174078fa15f33655be704d9409fdbc4f9929/include/ruby/intern.h

https://github.com/ruby/ruby/blob/ca6b174078fa15f33655be704d9409fdbc4f9929/enumerator.c

https://github.com/ruby/ruby/blob/ca6b174078fa15f33655be704d9409fdbc4f9929/array.c

每个都为您提供一个枚举器,with_index与枚举器一起使用。

当你到达元素20时,你将它打印出来,然后你擦除它,此时Ruby有效地向下移动阵列中的所有元素。现在,由数组支持的枚举器选取下一个元素40,因为所有内容都向下移动(30个被复制超过20个,40个被复制超过30个,数组被调整大小)

看看: https://github.com/ruby/ruby/blob/ca6b174078fa15f33655be704d9409fdbc4f9929/array.c#L3023 它通过memmove移动元素的魔力发生了。

答案 2 :(得分:1)

让我们一步一步:

arr = [10, 20, 30, 40]  
enum0 = arr.each
  #=> #<Enumerator: [10, 20, 30, 40]:each> 
enum1 = enum0.with_index
  #=> #<Enumerator: #<Enumerator: [10, 20, 30, 40]:each>:with_index> 

我们可以通过将enum1转换为数组来查看enum1.to_a #=> [[10, 0], [20, 1], [30, 2], [40, 3]] 的内容:

enum1

这告诉我们Enumerator#each(将调用Array#each)将枚举器elmt, i = enum1.next #=> [10, 0] puts elmt, i # 10 # 0 elmt == 20 #=> false 的四个元素传递给块,然后将它们依次分配给块变量。第一个是:

arr.delete_at(i)

因此arr未执行。

enum1arr #=> [10, 20, 30, 40] enum1.to_a #=> [[10, 0], [20, 1], [30, 2], [40, 3]] 都未被更改:

each

enum1现在将elmt, i = enum1.next #=> [20, 1] elmt == 20 #=> true 的下一个元素传递到块中:

arr.delete_at(i)
  #=> [10, 20, 30, 40].delete_at(1)
  #=> 20 
arr
  #=> [10, 30, 40] 
enum1.to_a
  #=> [[10, 0], [30, 1], [40, 2]]

所以我们执行:

arr

啊!因此,枚举器和enum1都已更改。这是完全合理的,因为在创建枚举器时,会建立对原始接收器的引用,以及对其进行操作的规则。因此,对接收器的更改将影响枚举器。

我们可以使用Enumerator#peek来查看each将传递到块中的enum1.peek #=> [40, 2] 的下一个元素:

each

所以你看到each移动到下一个索引位置,忽略了一个先前元素被移除的事实,导致后面的元素每个向下移动一个位置,导致[30,1]跳过elmt, i = enum1.next #=> [40, 2] elmt == 20 #=> false arr #=> [10, 30, 40] enum1.to_a #=> [[10, 0], [30, 1], [40, 2]]

each

此时arr到达枚举器的末尾,因此它的工作已经完成。因此它返回原始接收器[10, 30, 40] ,但是已经修改了,所以我们得到:

arr = [10, 20, 20, 40]  

更好的例子可能是:

[10, 20, 40] 

其中:

{{1}}

将被退回。