[英]Why is my filter method not removing some elements that should be removed?
我正在嘗試創建一個名為 filer_out 的方法,它接受一個數組和一個 proc,並返回相同的數組,但每個元素在通過 proc 運行時都返回 true,但需要注意的是我們不能使用 Array#拒絕!
我寫了這個:
def filter_out!(array, &prc)
array.each { |el| array.delete(el) if prc.call(el)}
end
arr_2 = [1, 7, 3, 5 ]
filter_out!(arr_2) { |x| x.odd? }
p arr_2
但是當我運行代碼時,打印出來的是:
[7, 5]
即使答案應該是:
[]
在查看解決方案后,我看到首先使用了 Array#uniq:
def filter_out!(array, &prc)
array.uniq.each { |el| array.delete(el) if prc.call(el) }
end
arr_2 = [1, 7, 3, 5 ]
filter_out!(arr_2) { |x| x.odd? }
p arr_2
並顯示了正確的 output:
[]
所以我想我的問題是,為什么必須使用 Array#uniq 才能獲得正確的解決方案? 感謝您的幫助!
這里的問題是方法delete
修改了原始數組。 如果您提供一些信息,這里的交易:
def filter_out!(array, &prc)
array.each.with_index do |el, i|
p "Current index #{i}"
p "Current array #{array}"
p "Current element #{el}"
array.delete(el) if prc.call(el)
end
end
arr_2 = [1, 7, 3, 5 ]
filter_out!(arr_2) { |x| x.odd? }
# Output:
#"Current index 0"
# "Current array [1, 7, 3, 5]"
# "Current element 1"
# "Current index 1"
# "Current array [7, 3, 5]"
# "Current element 3"
解釋:
1
,刪除后數組為[7, 3, 5]
1
,它獲取當前數組中具有該索引的當前元素,在這種情況下,是3
而不是7
並刪除它,刪除后數組為[3, 5]
通過使用uniq
你會得到正確的結果,因為array.uniq
它會在原始數組被修改時創建原始數組的副本,它仍然按預期迭代。
遍歷數組使用某種指向當前元素的內部“光標”,例如:
[ 1, 7, 3, 5 ] # 1st step
# ^
[ 1, 7, 3, 5 ] # 2nd step (increment cursor)
# ^
# etc.
如果在迭代過程中刪除當前元素,cursor 會立即指向下一個元素,從而在下一輪遞增時跳過一個元素:
[ 1, 7, 3, 5 ] # 1st step
# ^
[ 7, 3, 5 ] # 1nd step (remove element under cursor)
# ^
[ 7, 3, 5 ] # 2nd step (increment cursor)
# ^
[ 7, 5 ] # 2nd step (remove element under cursor)
# ^
# done
一個典型的解決方法是以相反的順序迭代數組,即:
[ 1, 7, 3, 5 ] # 1st step
# ^
[ 1, 7, 3 ] # 1nd step (remove element under cursor)
# ^
[ 1, 7, 3 ] # 2nd step (decrement cursor)
# ^
[ 1, 7 ] # 2nd step (remove element under cursor)
# ^
# etc.
請注意,cursor 在此算法中可能超出范圍,因此在這方面您必須小心。
以上為 Ruby 代碼:
def filter_out!(array)
(array.size - 1).downto(0) do |i|
array.delete_at(i) if yield array[i]
end
end
作為一般規則,不僅對於這個問題,也不僅僅是對於 Ruby,在迭代集合時改變集合絕不是一個好主意。 只是不要那樣做。 曾經。
實際上,就個人而言,我只是認為您應該在可行和明智的情況下完全避免任何突變,但這可能有點極端。
你的代碼還犯了另一個大罪:永遠不要改變一個論點。 曾經。 你應該只使用 arguments 來計算結果,你不應該改變它們。 這對於任何調用你的方法的人來說都是非常令人驚訝的,並且在編程中,意外是危險的。 它們會導致錯誤和安全漏洞。
最后,您正在使用 bang !
命名約定錯誤。 劉海用來標記一對方法中“更令人驚訝”的地方。 你應該只有一個filter_out!
方法,如果你也有一個filter_out
方法。 說到風格,Ruby 中的縮進是 2 個空格,而不是 4 個,按照標准的社區編碼風格。
好的,話雖如此,讓我們看看您的代碼中發生了什么。
這是來自Rubinius Ruby 實現的Array#each
實現的相關部分,在core/array.rb#L62-L77
中定義:
def each
i = @start
total = i + @total
tuple = @tuple
while i < total
yield tuple.at(i)
i += 1
end
end
如您所見,這只是一個簡單的while
循環,每次都會增加索引。 其他 Ruby 實現類似,例如這里是JRuby在core/src/main/java/org/jruby/RubyArray.java#L1805-L1818
中實現的簡化版本:
public IRubyObject each(ThreadContext context, Block block) {
for (int i = 0; i < size(); i++) {
block.yield(context, eltOk(i));
}
}
同樣,只是一個簡單的索引循環。
在您的情況下,我們從一個數組開始,其后備存儲如下所示:
1 7 3 5
在each
的第一次迭代中,迭代計數器位於索引0
處:
1 7 3 5
↑
1
是奇數,所以我們刪除它,現在的情況是這樣的:
7 3 5
↑
我們在循環迭代中做的最后一件事是將迭代計數器增加一:
7 3 5
↑
好的,在下一次迭代中,我們再次檢查: 3
是奇數,所以我們刪除它:
7 5
↑
我們增加迭代計數器:
7 5
↑
現在我們有了循環的退出條件: i
不再小於數組的大小: i
是2
並且大小也是2
。
請注意,在 JRuby 實現中,通過調用size()
方法來檢查大小,這意味着每次都會重新計算大小。 然而,在 Rubinius 中,大小是緩存的,並且只在循環開始之前計算一次。 因此,Rubinius 實際上會嘗試繼續訪問此處並訪問不存在的支持元組的第三個元素,這會導致此NoMethodError
異常:
NoMethodError: undefined method `odd?' on nil:NilClass.
訪問Rubinius::Tuple
的不存在元素返回nil
,然后each
將nil
傳遞給塊,該塊試圖調用Integer#odd?
.
重要的是要注意Rubinius 在這里沒有做錯任何事。 它引發異常的事實並不是Rubinius 的錯誤。 錯誤在您的代碼中:根本不允許在迭代集合時對其進行變異。
現在所有這一切都解釋了為什么調用Array#uniq
的解決方案首先起作用: Array#uniq
返回一個新數組,因此您正在迭代的數組(從Array#uniq
返回的那個)和您正在變異的數組(那個由array
參數綁定引用)是兩個不同的 arrays 。 您會得到相同的結果,例如Object#clone
、 Object#dup
、 Enumerable#map
或許多其他方法。 舉個例子:
def filter_out(array)
array.map(&:itself).each { |el| array.delete(el) if yield el }
end
arr_2 = [1, 7, 3, 5]
filter_out(arr_2, &:odd?)
p arr_2
但是,更慣用的 Ruby 解決方案將是這樣的:
def filter_out(array, &blk)
array.delete_if(&blk)
end
arr_2 = [1, 7, 3, 5]
arr_3 = filter_out(arr_2, &:odd?)
p arr_3
這解決了您的代碼的所有問題:
!
唯一真正的問題是是否需要該方法,或者僅編寫是否更有意義
arr_2 = [1, 7, 3, 5]
arr_3 = arr_2.delete_if(&:odd?)
p arr_3
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.