这是由于delete_if
/reject!
(及其对应的keep_if
/select!
)在内部的工作方式.让我们看看另一个例子,它在每个步骤打印nums
的同时删除偶数:
nums = [0, 1, 2, 3, 4, 5, 6]
def remove_elements(nums)
nums.reject! do |num|
p nums: nums
num.even?
end
end
p result: remove_elements(nums)
输出:
{:nums=>[0, 1, 2, 3, 4, 5, 6]}
{:nums=>[0, 1, 2, 3, 4, 5, 6]}
{:nums=>[1, 1, 2, 3, 4, 5, 6]}
{:nums=>[1, 1, 2, 3, 4, 5, 6]}
{:nums=>[1, 3, 2, 3, 4, 5, 6]}
{:nums=>[1, 3, 2, 3, 4, 5, 6]}
{:nums=>[1, 3, 5, 3, 4, 5, 6]}
{:result=>[1, 3, 5]}
正如你所看到的,nums
正在以一种看似奇怪的方式进行修改.但这里到底发生了什么?
Ruby的实现没有创建临时数组,而是重用现有数组来存储结果.当它遍历数组时,它将每个应该保留的元素(块返回falsy的结果)复制到前面,复制任何现有的元素.最后,数组被截断到其最终大小:(ASCII艺术在前面)
[0, 1, 2, 3, 4, 5, 6]
┌──┘
[1, 1, 2, 3, 4, 5, 6]
┌─────┘
[1, 3, 2, 3, 4, 5, 6]
┌────────┘
[1, 3, 5, 3, 4, 5, 6]
[1, 3, 5]
───┬───
result
注意,1
、3
和5
在数组中中间出现两次.这就是为什么你的nums.count
不能像预期的那样工作--你在重新构造数组的时候计算元素.
要解决这个问题,您可以提前计算元素,例如通过tally
:
nums = [1, 1, 1, 2, 2, 3]
counts = nums.tally
#=> {1=>3, 2=>2, 3=>1}
nums.delete_if { |num| counts[num] > 2 }
#=> [2, 2, 3]
这也更快,因为您不必为了计数而再次遍历整个数组的每个元素.