【问题标题】:Is it safe to delete from an Array inside each?从每个内部的数组中删除是否安全?
【发布时间】:2015-09-26 11:30:21
【问题描述】:

在通过each 对其进行迭代时,是否可以安全地从Array 中删除元素?第一个测试看起来很有希望:

a = (1..4).to_a
a.each { |i| a.delete(i) if i == 2 }
# => [1, 3, 4] 

但是,我在以下方面找不到确凿的事实:

  • 是否安全(按设计)
  • 从哪个 Ruby 版本开始是安全的

在过去的某些时候,似乎是not possible to do

它不起作用,因为 Ruby 在尝试删除某些内容时退出了 .each 循环。

documentation 没有说明迭代期间的可删除性。

我不是在寻找 rejectdelete_if。我想对数组的元素做一些事情,有时还从数组中删除一个元素(在我对所述元素做了其他事情之后)。

更新 1:我对“安全”的定义不是很清楚,我的意思是:

  • 不要引发任何异常
  • 不要跳过Array 中的任何元素

【问题讨论】:

  • 在我熟悉的所有语言中都不安全(取决于您如何定义“安全”)。在迭代集合时修改集合肯定会破坏您的程序或让您感到惊讶。想不出一个合法的用例。
  • 为了安全起见,迭代数组的副本,同时从原始元素中删除元素

标签: arrays ruby


【解决方案1】:

您不应过度依赖未经授权的答案。正如凯文对此的评论所指出的那样,您引用的答案是错误的。

从 Array 中删除元素是安全的(从 Ruby 开始),而 each 在某种意义上说 Ruby 不会因此而引发错误,并且会给出一个决定性的(即,不是随机的)结果。

但是,你需要小心,因为当你删除一个元素时,它后面的元素会被移动,因此下一个应该迭代的元素会被移动到被删除元素的位置,该元素已经被迭代已经结束了,将被跳过。

【讨论】:

  • 我想挑战你所说的,但是通过在我的示例代码中添加puts i;,我可以验证下一个元素确实被“跳过”了。这意味着我将不得不复制我的数组并遍历副本。
  • @NobodysNightmare:“我想挑战你所说的”——一般来说,带有金色徽章的用户知道他们在说什么。 :)
【解决方案2】:

为了回答您的问题,这样做是否“安全”,您首先必须定义“安全”的含义。你是说

  1. 它不会使运行时崩溃?
  2. 不是raiseException
  3. raiseException?
  4. 它的行为是确定性的?
  5. 它会按照您的预期执行吗? (您希望它做什么?)

很遗憾,Ruby 语言规范并没有什么帮助:

15.2.12.5.10 数组#each


each(&block)


可见性:公开

行为

  • 如果给出块:
    1. 对于索引顺序中接收者的每个元素,调用 block 并将元素作为唯一参数。
    2. 归还接收器。

这似乎暗示着它确实在上述1.、2.、4.和5.的意义上是完全安全的。

The documentation 说:

each { |item| block }ary

self 中的每个元素调用一次给定块,并将该元素作为参数传递。

再一次,这似乎暗示着与规范相同的东西。

不幸的是,当前存在的 Ruby 实现中没有一个以这种方式解释规范。

在 MRI 和 YARV 中实际上发生的情况如下:数组的突变,包括元素和/或索引的任何移动,立即变得可见,包括迭代器代码的内部实现它基于数组索引。因此,如果您在当前迭代的位置或之前删除一个元素,您将跳过 next 元素,而如果您在当前位置删除 after 元素迭代,您将跳过 那个 元素。对于each_with_index,您还将观察到删除元素之后的所有元素的索引都发生了变化(或者相反:索引保持不变,但元素发生了变化)。

因此,从 1.、2. 和 4 的意义上说,这种行为是“安全的”。

其他 Ruby 实现大部分复制了这种(未记录的)行为,但由于未记录,您不能依赖它,事实上,我相信至少有人做过简单的实验,提出了某种 @改为 987654331@。

【讨论】:

  • 感谢您的详细解答。我确实期待像 5 这样的东西,我的期望是:元素会被删除,但在任何情况下都不会跳过任何元素。
【解决方案3】:

我会说它是安全的,基于以下几点:

2.2.2 :035 > a = (1..4).to_a
 => [1, 2, 3, 4] 
2.2.2 :036 > a.each { |i| a.delete(i+1) if i > 1 ; puts i }
1
2
4
 => [1, 2, 4]

我从这个测试中推断,Ruby 在遍历内容时正确识别元​​素“3”已被删除而元素“2”正在处理,否则元素“4”也将被删除。

然而,

2.2.2 :040 > a.each { |i| puts i; a.delete(i) if i > 1 ; puts i }
1
1
2
2
4
4

这表明删除“2”后,下一个处理的元素是数组中现在的第三个元素,因此之前在第三位的元素根本不会被处理。每个似乎都重新检查数组以找到下一个要在每次迭代中处理的元素。

我认为考虑到这一点,您应该在处理之前根据您的情况复制数组。

【讨论】:

  • 一旦您删除当前正在查看的元素(即i,而不是i+1),迭代将跳过未来的元素。这就是您的示例代码有效的原因:您删除了一个未来元素,它应该(正如我现在所知道的)完美地工作。
  • 它会有效地跳过数组中的下一个元素,当然不是所有未来的元素。它仍然会处理现在位于当前位置后续位置的元素。
【解决方案4】:

视情况而定。

所有.each 所做的只是返回一个枚举器,它保存集合一个指向它离开位置的指针。示例:

a = [1,2,3]
b = a.each # => #<Enumerator: [1, 2, 3]:each>
b.next # => 1
a.delete(2)
b.next # => 3
a.clear
b.next # => StopIteration: iteration reached an end

每个 with 块调用 next 直到迭代结束。因此,只要您不修改任何“未来”数组记录,它就应该是安全的。

然而,在 ruby​​ 的 EnumerableArray 中有很多有用的方法,你真的不需要这样做。

【讨论】:

  • “所以只要你不修改......它应该是安全的” OP 询问当你修改时会发生什么。你的回答毫无意义。而且您提到 each 的枚举器使用似乎与 OP 的观点无关。
  • 我只是用一个例子解释了删除未来记录如何移动数组,但我猜每个人都有自己的
【解决方案5】:

您是对的,过去建议不要在迭代集合时从集合中删除项目。在我的测试中,至少在 1.9.3 版本的数组中,这没有问题,即使删除前面或后面的元素也是如此。

我认为,虽然你可以,但你不应该这样做。 更清晰和安全的方法是拒绝元素并分配给新数组。

b = a.reject{ |i| i == 2 } #[1, 3, 4]

如果你想重用你的数组也是可能的

a = a.reject{ |i| i == 2 } #[1, 3, 4]

其实和

是一样的
a.reject!{ |i| i == 2 } #[1, 3, 4]

您说您不想使用拒绝,因为您想在删除之前对元素做其他事情,但这也是可能的。

a.reject!{ |i| puts i if i == 2;i == 2 } 
# 2
#[1, 3, 4]

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-09-12
    • 1970-01-01
    • 2019-04-04
    • 1970-01-01
    • 2010-10-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多