如何从一个哈希表中提取子哈希表?

109

我有一个哈希表:

h1 = {:a => :A, :b => :B, :c => :C, :d => :D}

最佳方法如何提取这样的子哈希?

h1.extract_subhash(:b, :d, :e, :f) # => {:b => :B, :d => :D}
h1 #=> {:a => :A, :c => :C}

5
附注:http://apidock.com/rails/Hash/slice%21 - tokland
1
@JanDvorak 这个问题不仅涉及返回子哈希,还涉及修改现有的哈希。非常相似的东西,但 ActiveSupport 处理它们的方式不同。 - skalee
17个回答

156

ActiveSupport 从2.3.8版本开始,提供了四个方便的方法:#slice#except以及它们的破坏性对应方法:#slice!#except!。其他答案中已经提到了它们,但为了总结在一个地方:

x = {a: 1, b: 2, c: 3, d: 4}
# => {:a=>1, :b=>2, :c=>3, :d=>4}

x.slice(:a, :b)
# => {:a=>1, :b=>2}

x
# => {:a=>1, :b=>2, :c=>3, :d=>4}

x.except(:a, :b)
# => {:c=>3, :d=>4}

x
# => {:a=>1, :b=>2, :c=>3, :d=>4}

请注意 bang 方法的返回值。它们不仅会修改现有的哈希表,还会返回被移除(而非保留)的条目。在问题中给出的示例中, Hash#except! 最适合使用:

x = {a: 1, b: 2, c: 3, d: 4}
# => {:a=>1, :b=>2, :c=>3, :d=>4}

x.except!(:c, :d)
# => {:a=>1, :b=>2}

x
# => {:a=>1, :b=>2}

ActiveSupport并不需要整个Rails框架,它非常轻量级。实际上,许多非Rails的gem都依赖于它,所以您很可能已经在Gemfile.lock中有了它。无需自己扩展Hash类。


3
x.except!(:c, :d) 的结果应该是 # => {:a=>1, :b=>2}。如果您能编辑您的答案就很好了。 - 244an

59
如果您特别想让方法返回提取出的元素,但 h1 保持不变:
h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
h2 = h1.select {|key, value| [:b, :d, :e, :f].include?(key) } # => {:b=>:B, :d=>:D} 
h1 = Hash[h1.to_a - h2.to_a] # => {:a=>:A, :c=>:C} 

如果您想将它打补丁到Hash类中:

class Hash
  def extract_subhash(*extract)
    h2 = self.select{|key, value| extract.include?(key) }
    self.delete_if {|key, value| extract.include?(key) }
    h2
  end
end

如果你只是想从哈希表中删除指定的元素,那么使用delete_if会容易得多。

h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
h1.delete_if {|key, value| [:b, :d, :e, :f].include?(key) } # => {:a=>:A, :c=>:C} 
h1  # => {:a=>:A, :c=>:C} 

2
这是O(n2) - 你将在选择上有一个循环,在包含上又有另一个循环,它将被调用h1.size次。 - magicgregz
3
如果您正在使用Rails,以下的答案(使用内置的sliceexcept,具体取决于您的需求)更加简洁。虽然这个答案对于纯Ruby来说还不错。 - Krease
.slice和.except是正确的答案,请参见下文。 - Oz Ben-David

42

Ruby 2.5 新增了 Hash#slice 方法:

h = { a: 100, b: 200, c: 300 }
h.slice(:a)           #=> {:a=>100}
h.slice(:b, :c, :d)   #=> {:b=>200, :c=>300}

30

如果你使用RailsHash#slice 是一个好的选择。

{:a => :A, :b => :B, :c => :C, :d => :D}.slice(:a, :c)
# =>  {:a => :A, :c => :C}

如果您不使用Rails,则Hash#values_at将按您请求的顺序返回值,因此您可以这样做:
def slice(hash, *keys)
  Hash[ [keys, hash.values_at(*keys)].transpose]
end

def except(hash, *keys)
  desired_keys = hash.keys - keys
  Hash[ [desired_keys, hash.values_at(*desired_keys)].transpose]
end

例:

slice({foo: 'bar', 'bar' => 'foo', 2 => 'two'}, 'bar', 2) 
# => {'bar' => 'foo', 2 => 'two'}

except({foo: 'bar', 'bar' => 'foo', 2 => 'two'}, 'bar', 2) 
# => {:foo => 'bar'}

解释:

{:a => 1, :b => 2, :c => 3}中选择{:a => 1, :b => 2}

hash = {:a => 1, :b => 2, :c => 3}
keys = [:a, :b]
values = hash.values_at(*keys) #=> [1, 2]
transposed_matrix =[keys, values].transpose #=> [[:a, 1], [:b, 2]]
Hash[transposed_matrix] #=> {:a => 1, :b => 2}

如果您觉得使用“猴子补丁”是正确的方式,以下就是您想要的:

module MyExtension
  module Hash 
    def slice(*keys)
      ::Hash[[keys, self.values_at(*keys)].transpose]
    end
    def except(*keys)
      desired_keys = self.keys - keys
      ::Hash[[desired_keys, self.values_at(*desired_keys)].transpose]
    end
  end
end
Hash.include MyExtension::Hash

2
在我看来,猴子补丁绝对是好方法。更加简洁,让意图更加清晰明了。 - Romário
1
添加修改代码以正确处理核心模块、定义模块和导入扩展哈希核心...模块 CoreExtensions 模块 Hash def slice(*keys) ::Hash[[keys, self.values_at(*keys)].transpose] end end end哈希包括 CoreExtensions::Hash - Ronan Fauglas

6
您可以使用 ActiveSupport 的核心扩展中提供的 slice!(*keys) 方法。
initial_hash = {:a => 1, :b => 2, :c => 3, :d => 4}

extracted_slice = initial_hash.slice!(:a, :c)

initial_hash现在将会是

{:b => 2, :d =>4}

提取的幻灯片现在将会是:

{:a => 1, :c =>3}

您可以查看ActiveSupport 3.1.3中的slice.rb文件。

我认为你在描述extract!。 extract!从初始哈希中删除键,返回一个包含已删除键的新哈希。slice!则相反:从初始哈希中删除除指定键之外的所有内容(同样,返回一个包含已删除键的新哈希)。因此,slice!更像是一种“保留”操作。 - Russ Egan
1
ActiveSupport 不是 Ruby STI 的一部分。 - Volte

5
module HashExtensions
  def subhash(*keys)
    keys = keys.select { |k| key?(k) }
    Hash[keys.zip(values_at(*keys))]
  end
end

Hash.send(:include, HashExtensions)

{:a => :A, :b => :B, :c => :C, :d => :D}.subhash(:a) # => {:a => :A}

1
干得好。但不完全是他要求的。你的方法返回:{:d=>:D, :b=>:B, :e=>nil, :f=>nil} {:c=>:C, :a=>:A, :d=>:D, :b=>:B}。 - Andy
一个等效的一行代码(也许更快)解决方案:<pre> def subhash(*keys) select {|k,v| keys.include?(k)} end - peak

4
h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
keys = [:b, :d, :e, :f]

h2 = (h1.keys & keys).each_with_object({}) { |k,h| h.update(k=>h1.delete(k)) }
  #=> {:b => :B, :d => :D}
h1
  #=> {:a => :A, :c => :C}

3

delete_ifkeep_if都是Ruby核心的一部分。在这里,你可以实现你想要的功能,而不需要修改Hash类型。

h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
h2 = h1.clone
p h1.keep_if { |key| [:b, :d, :e, :f].include?(key) } # => {:b => :B, :d => :D}
p h2.delete_if { |key, value| [:b, :d, :e, :f].include?(key) } #=> {:a => :A, :c => :C}

请查看以下文档中的链接获取更多信息:
  • delete_if:删除满足指定条件的键值对。
  • keep_if:保留满足指定条件的键值对,删除不满足条件的键值对。

3

2

如果您使用Rails,则可以方便地使用Hash.except

h = {a:1, b:2}
h1 = h.except(:a) # {b:2}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接