Rails 里获取数组中除指定元素外的其他元素,且不修改原数组

当需要获取数组中除了某些值以外的其他元素时,我们可以使用数组的 without 方法,这个方法是 ActiveSupport 给 Array 加上的。

使用

获取数组中除 0 以外的元素,且不改变原有数组:

arr = [1, 2, 3, 4, 0]
arr.without(0) # => [1, 2, 3, 4]

arr # => [1, 2, 3, 4, 0]

实现

通过查看源码我们可以看出,withoutexcluding 方法的别名,所以二者可以互换。而 excluding 通过 Array#- 这个方法实现排除元素且不修改原数组的效果。

# activesupport-7.1.2/lib/active_support/core_ext/array/access.rb
class Array

  # ...
  
  # Returns a copy of the Array excluding the specified elements.
  #
  #   ["David", "Rafael", "Aaron", "Todd"].excluding("Aaron", "Todd") # => ["David", "Rafael"]
  #   [ [ 0, 1 ], [ 1, 0 ] ].excluding([ [ 1, 0 ] ]) # => [ [ 0, 1 ] ]
  #
  # Note: This is an optimization of Enumerable#excluding that uses Array#-
  # instead of Array#reject for performance reasons.
  def excluding(*elements)
    self - elements.flatten(1)
  end
  alias :without :excluding

  # ...
end

这里有个比较巧妙的地方,即使用 *elements 接受数组参数,并 flatten 了一层传入的参数 。好处是在对一维数组进行处理时,可以接受两种方式的传参:

  1. 单个数组里边包含多个需要排除的元素;

  2. 将需要排除的元素以单个参数的形式传入
    即:

[1, 2, 3, 4].without(3, 4)     # => [1, 2]

[1, 2, 3, 4].without([3, 4])   # => [1, 2]

缺点是在二维及二维以上的数组时,只能接受单个数组包含多个元素的传参方式:

# unexpected behavior
[[1, 2], [3, 4], [5, 6]].without([1, 2], [3, 4])            # => [[1, 2], [3, 4], [5, 6]]

# correct
[[1, 2], [3, 4], [5, 6]].without([[1, 2], [3, 4]])         # => [[5, 6]]

扩展

Hash 中也定义了 without 方法,不过传入的参数是需要排除的键:

h = {name: "ian", email: "xxx@gmail.com", password: "xxxx"}
h.without(:password) # => {name: "ian", email: "xxx@gmail.com"}

h # => {name: "ian", email: "xxx@gmail.com", password: "xxxx"}

原本 Hash 和 Array 都是使用 ActiveSupport 在 Enumerable 模块扩展中的定义:

module Enumerable
  # ...

  # Returns a copy of the enumerable excluding the specified elements.  
  #  
  #   ["David", "Rafael", "Aaron", "Todd"].excluding "Aaron", "Todd"  
  #   # => ["David", "Rafael"]  
  #  
  #   ["David", "Rafael", "Aaron", "Todd"].excluding %w[ Aaron Todd ]  
  #   # => ["David", "Rafael"]  
  #  
  #   {foo: 1, bar: 2, baz: 3}.excluding :bar  
  #   # => {foo: 1, baz: 3}  
  def excluding(*elements)  
    elements.flatten!(1)  
    reject { |element| elements.include?(element) }  
  end  
  alias :without :excluding

  # ...
end

后来发现由于使用 Array#reject + include? 判断的性能不如直接使用 Array#-, 所以又为 Array 单独写了一个实现,覆盖了 Enumerable 中的实现

评论