【问题标题】:Why does ruby array.delete_at inside an array.each fail?为什么 array.each 中的 ruby​​ array.delete_at 会失败?
【发布时间】:2015-03-05 01:52:05
【问题描述】:

我是 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]

【问题讨论】:

  • 在迭代 Array(或通常为任何 Enumerable)时更改它会产生未定义的行为(至少在 MRI 中)。致citeMatz(Ruby 的创建者):“如果块修改了迭代对象,这是迭代器的未定义行为。它可能会崩溃或陷入无限循环。”
  • @Holger 好消息。我希望这是在相关方法的文档中。我敢称它为文档错误吗?

标签: ruby arrays iterator


【解决方案1】:

灯泡!!

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

【讨论】:

  • arr[3] 将返回 nil,但是如果您添加另一个 puts 语句,您可以看到迭代实际上从未达到 arr[3],因为到 Ruby 时数组的长度小于 4运行时达到这一点。我制作了一个在运行时打印索引的代码版本:gist.github.com/DavidEGrayson/31250dfa0a0dd8b443ed
  • @David Right - 打印索引使其更清晰。我会更新问题。关于运行时看到块顶部的长度并且没有进入最后一个循环的块的有趣点。
  • 嘿,IMO,任何熟悉从 self 到块的产生值如何工作的人都会清楚。我的意思是,在屈服于一个块时,它不必知道 self 是否发生了变化,只需在每次屈服时将 self[i] 的内容传递给块。但我相信很多新手会忽略这一点,而其他人只会使用Array#rejectArray#selectArray#delete_if
  • @llkin - 所以我的学习清单上有一个待办事项 - 了解“从自我到块的屈服值”。你有什么精彩的总结吗?
  • @user3546411 :没那么必要(但有助于学习)。你在答案中说的是真的,我只是从Ruby方面说的。值从原始数组迭代传递给blocks,delete_at修改原始数组,两者之间没有交互。
【解决方案2】:

看看: 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 移动元素的神奇之处。

【讨论】:

  • 这一切都非常有趣,因为您需要了解幕后发生的事情才能了解性能影响。使用这种方法在嵌套循环中对大型数组执行大量删除的算法将具有很大的性能乘数(可能为 O(log(n))。我希望像 array.keep_if 这样的东西可以处理一次完成整个事情,会更有效地实现,但是嗯......可能不会?除了查看解释器源代码之外,是否有书籍或网站讨论这对实现和性能?
  • Ruby 并不是最快的。 Ruby 的力量/美丽来自于它的表现力,而不是速度。我很少看到删除数组中元素的 ruby​​ 代码。通常,数据结构的转换存在变化,这些转换以函数样式链接。如果您需要/希望保留这种转换的最终结果 - 大多数情况下这样做比更改原始结构更快/更便宜。
  • 一般来说,作为一个经验法则(这适用于所有语言),你不应该在迭代集合时改变它——这就是为什么你'不允许这样做。其他语言对此更严格(发生这种情况时会引发错误/异常),其他语言则更宽松,但会警告这样的意外结果。
  • 一旦我了解了发生了什么,构建一个使用delete_at 的算法就没有问题了。但它的效率非常低,因为它每次删除一个元素时都会移动整个数组。使用keep_if 创建一个新数组,复制一份未被删除的元素。所以 delete_at 是一个 O(n) 方法; keep_if 是一个 O(log(n)) 方法。对于一个巨大的字符串(为了好玩,我将字典中的所有单词连接起来),区别是 #real 0m2.865s (keep_if) 与 #real 26m7.780s (delete_at)。我回到每个函数描述发生的事情的好地方
【解决方案3】:

让我们一步一步来:

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]] 

这告诉我们Enumerator#each(将调用Array#each)会将枚举器enum1 的四个元素传递到块中,依次将它们分配给块变量。第一个是:

elmt, i = enum1.next
  #=> [10, 0] 
puts elmt, i
  # 10
  #  0
elmt == 20
  #=> false 

所以arr.delete_at(i) 没有被执行。

arrenum1 均未更改:

arr
  #=> [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 都已更改。这是完全有道理的,因为当创建枚举器时,会建立对原始接收器的引用,以及要使用它做什么的规则。因此,对接收器的更改将影响枚举器。

我们可以使用Enumerator#peek 来查看enum1each 将传递到块中的下一个元素:

enum1.peek
  #=> [40, 2]

所以你看到each 移动到下一个索引位置,忽略了更早的元素已被删除的事实,导致后面的元素每个向下移动一个位置,导致each 跳过@987654343 @。

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] 

将被退回。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-07-09
    • 2016-04-11
    • 2021-10-10
    • 2013-01-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多