簡體   English   中英

為什么我的過濾方法沒有刪除一些應該刪除的元素?

[英]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]
  • 第二次,迭代中的 index 為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 實現類似,例如這里是JRubycore/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不再小於數組的大小: i2並且大小也是2

請注意,在 JRuby 實現中,通過調用size()方法來檢查大小,這意味着每次都會重新計算大小。 然而,在 Rubinius 中,大小是緩存的,並且只在循環開始之前計算一次。 因此,Rubinius 實際上會嘗試繼續訪問此處並訪問不存在的支持元組的第三個元素,這會導致此NoMethodError異常:

NoMethodError: undefined method `odd?' on nil:NilClass.

訪問Rubinius::Tuple的不存在元素返回nil ,然后eachnil傳遞給塊,該塊試圖調用Integer#odd? .

重要的是要注意Rubinius 在這里沒有做錯任何事 它引發異常的事實並不是Rubinius 的錯誤。 錯誤在您的代碼中:根本不允許在迭代集合時對其進行變異。

現在所有這一切都解釋了為什么調用Array#uniq的解決方案首先起作用: Array#uniq返回一個數組,因此您正在迭代的數組(從Array#uniq返回的那個)和您正在變異的數組(那個由array參數綁定引用)是兩個不同的 arrays 您會得到相同的結果,例如Object#cloneObject#dupEnumerable#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

這解決了您的代碼的所有問題:

  • 它不會改變數組。
  • 它不會改變任何論點。
  • 事實上,根本沒有發生突變。
  • 它使用來自 Ruby 核心庫的現有方法,而不是重新發明輪子。
  • 沒有砰!
  • 兩個空格縮進。

唯一真正的問題是是否需要該方法,或者僅編寫是否更有意義

arr_2 = [1, 7, 3, 5]
arr_3 = arr_2.delete_if(&:odd?)
p arr_3

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM