ruby - 为什么在这个问题中需要 uniq?(红宝石)
问题描述
编码新手,并尝试独立学习。处理 Ruby 上的一个问题,我们创建一个方法(filter_out!),当数组通过 proc 时,它会改变原始数组以删除真正的元素。唯一的规定是我们不允许使用 Array#reject!。
谁能向我解释为什么
arr.uniq.each { |ele| arr.delete(ele) if prc.call(ele)}
作品和
arr.each { |ele| arr.delete(ele) if prc.call(ele)}
才不是?以下是两个示例问题:
arr_2 = [1, 7, 3, 5 ]
filter_out!(arr_2) { |x| x.odd? }
p arr_2 # []
arr_3 = [10, 6, 3, 2, 5 ]
filter_out!(arr_3) { |x| x.even? }
p arr_3 # [3, 5]
我查过它并了解#uniq 删除重复值,对吗?
任何见解将不胜感激 - 谢谢!
编辑:查看更多,是因为 uniq 会使用 proc 的返回值进行比较吗?仍然不确定为什么某些数字需要这样做,但不是全部。
解决方案
唯一的规定是我们不允许使用 Array#reject!。
这是一个令人难以置信的脑残规定。例如,您可以只使用Array#select!
和反转条件,或Array#reject
与 一起使用Array#replace
。
谁能向我解释为什么
arr.uniq.each { |ele| arr.delete(ele) if prc.call(ele)}
作品和
arr.each { |ele| arr.delete(ele) if prc.call(ele)}
每当您不太了解一段代码的作用时,一个好主意是自己用笔和一张纸来运行它。
所以,让我们这样做吧。假设arr = [1, 2, 4, 5, 2]
和prc = -> e { e.even? }
。
Array#each
遍历数组,我们并不确切知道它是如何做到的(这就是抽象的全部思想),但我们可以想象它保留了某种索引来记住它当前位于数组的哪个部分。
因此,在数组的第一次迭代期间,索引位于第一个元素处,数组如下所示:
[1, 2, 4, 5, 2]
# ↑
Array#each
传递1
给块,后者又将它传递给prc
,然后返回false
,因此该块不做任何事情。
Array#each
增加索引,所以现在我们的数组看起来像这样:
[1, 2, 4, 5, 2]
# ↑
Array#each
传递2
给块,后者又将其传递给prc
,后者返回true
。结果,块现在传递2
给Array#delete
,它从数组中删除每个等于 的元素2
。所以,现在数组看起来像这样:
[1, 4, 5]
# ↑
Array#each
增加索引,所以现在我们的数组看起来像这样:
[1, 4, 5]
# ↑
Array#each
传递5
给块,后者又将它传递给prc
,然后返回false
,因此该块不做任何事情。
Array#each
增加索引,所以现在我们的数组看起来像这样:
[1, 4, 5]
# ↑
由于 index 超出了数组的末尾,因此迭代完成,结果为[1, 4, 5]
.
如您所见,问题在于您在迭代数组时正在对其进行变异。
我查过它并了解#uniq 删除重复值,对吗?
正确,但这与它使您的代码工作的原因无关。或者,更准确地说,使它看起来有效,因为您的代码实际上还有很多其他问题。
Array#uniq
删除重复值不是相关的事实,而是返回Array#uniq
一个新数组的事实。由于Array#uniq
返回一个新数组,因此您不再在迭代数组时对其进行变异,而是在迭代另一个数组时对其进行变异。
您可以使用返回新数组的任何方法Array
或其祖先之一,例如Object#clone
、Object#dup
、Array#map
或Array#select
,甚至是真正有创意的东西,例如arr + []
:
arr.select { true }.each {|ele| arr.delete(ele) if prc.call(ele) }
实际上,您甚至不需要返回 an Array
,您只需要返回某种Enumerable
,例如 an Enumerator
,例如使用Array#each
:
arr.each.each {|ele| arr.delete(ele) if prc.call(ele) }
编辑:查看更多,是因为 uniq 会使用 proc 的返回值进行比较吗?仍然不确定为什么某些数字需要这样做,但不是全部。
由于您没有将 proc 传递给Array#uniq
,因此它不可能将其用于任何事情,因此这显然是不可能的解释。
请注意,正如我上面解释的那样,您的代码存在更多问题,而不仅仅是在迭代数组时对其进行变异。
即使您的原始代码确实有效,它实际上仍然会被破坏。您正在使用删除返回Array#delete
的元素。问题是删除数组中作为参数传递的元素的所有元素,即使是可能返回的元素。prc
true
Array#delete
#==
prc
false
这是一个简单的例子:
a = [2, 2, 2]
i = -1
filter_out!(a) { (i += 1) == 1 }
这应该只过滤掉第二个元素,但实际上删除了所有元素,所以结果是[]
它实际上应该是[2, 2]
.
您的版本Array#uniq
使这个问题变得更糟,因为在运行之后Array#uniq
,数组甚至没有第二个元素了!因此,结果是[2]
,实际上应该是[2, 2]
。
下一个问题是方法的名称:filter_out!
. Bang 方法(即名称以感叹号结尾的方法!
)用于标记一对方法中更令人惊讶的方法。您永远不应该单独使用 bang 方法。仅当且仅当还有方法时才应使用该名称。因此,该方法应命名为.filter_out!
filter_out
filter_out
最后一个问题是你正在改变传递给方法的参数。这是绝对不行的。您永远不会改变传递给方法的参数。绝不。你从不破坏别人的玩具,你从不触碰别人的私处。
一般来说,应尽可能避免突变,因为它可能会导致混乱和难以追踪错误,因为您已经发现了自己。只有在绝对必要的情况下,你才应该改变状态,并且只声明你自己拥有。永远不要改变别人的状态。相反,filter_out
应该返回一个带有结果的新数组。
以下是几个filter_out
可能看起来像的示例:
def filter_out(enum)
enum.select {|el| !yield el }
end
def filter_out(enum)
enum.reduce([]) {|acc, el| if yield el then acc else acc + [el] end }
end
def filter_out(enum)
enum.each_with_object([]) {|el, acc| acc << el unless yield el }
end
注意:就个人而言,我不是 的忠实粉丝Enumerable#each_with_object
,因为它依赖于变异,所以我会尽可能避免这种解决方案。
然而,真正的问题似乎是,无论您学习的任何课程、书籍、教程或课程似乎都非常糟糕,无论是教授 Ruby,还是教授良好的软件工程实践,例如测试和调试。