如何在Ruby/Rails中从哈希表中删除一个键并获取剩余的哈希表?

679

我要向哈希表添加一个新的键值对,我会这样做:

{:a => 1, :b => 2}.merge!({:c => 3})   #=> {:a => 1, :b => 2, :c => 3}
有没有类似的方法可以从哈希表中删除一个键?
这个代码是可行的:
{:a => 1, :b => 2}.reject! { |k| k == :a }   #=> {:b => 2}

但我希望有类似这样的东西:

{:a => 1, :b => 2}.delete!(:a)   #=> {:b => 2}

重要的是返回值应该是剩余的哈希值,这样我才能执行以下操作:

foo(my_hash.reject! { |k| k == my_key })

一行代码实现。


1
如果你真的需要,你可以随时扩展(在运行时打开)内置的哈希表来添加这个自定义方法。 - dbryson
Ruby 3 将拥有这个功能。https://www.ruby-lang.org/en/news/2020/09/25/ruby-3-0-0-preview1-released/ - B Seven
18个回答

882

对于那些只是想知道如何从哈希中删除键/值对的人,你可以使用:
hash.delete(key)

对于其他想要阅读完全不同内容的人,你可以继续阅读下面的回答:

Rails有一个except/except!方法,它会返回删除了这些键的哈希。如果你已经在使用Rails,那么没有必要自己创建这个方法。

class Hash
  # Returns a hash that includes everything but the given keys.
  #   hash = { a: true, b: false, c: nil}
  #   hash.except(:c) # => { a: true, b: false}
  #   hash # => { a: true, b: false, c: nil}
  #
  # This is useful for limiting a set of parameters to everything but a few known toggles:
  #   @person.update(params[:person].except(:admin))
  def except(*keys)
    dup.except!(*keys)
  end

  # Replaces the hash without the given keys.
  #   hash = { a: true, b: false, c: nil}
  #   hash.except!(:c) # => { a: true, b: false}
  #   hash # => { a: true, b: false }
  def except!(*keys)
    keys.each { |key| delete(key) }
    self
  end
end

59
不必使用完整的Rails框架,你可以在任何Ruby应用程序中引入ActiveSupport。 - Fryie
13
补充Fryie的答案,你甚至不需要加载全部ActiveSupport;只需包含它们,然后require "active_support/core_ext/hash/except"即可。 - GMA
太晚了,我是指“包含宝石”而不是“包含它们”。 - GMA
2
@GMA:当你编辑五分钟结束后,你可以复制、删除、修改并重新发布评论。 - iconoclast
一个不错的示例:User.first.as_json.except("id") - stevec

255

为什么不直接使用:

hash.delete(key)

hash现在是你要查找的“剩余哈希值”。


4
我同意有时候做某件事情是不值得的,我只是想知道为什么有 mergemerge!delete,但是没有 delete!…… - Misha Moroshko
30
如果 delete 不修改其参数,并且存在 delete! 来修改其参数,这将更符合 Ruby 的惯例。 - David J.
95
这并不能像问题中提到的那样返回剩余的哈希,它将返回与被删除键相关联的值。 - MhdSyrwan
1
删除返回键,但它也会更改哈希。至于为什么没有delete!,我猜这在语义上不合理,调用delete并不实际删除它。调用hash.delete()而不是hash.delete!()将是一个无操作。 - eggmatters
4
与普遍观念相反,感叹号并不表示变异,而是指“不寻常的行为”。我想没有“delete!”方法是因为一个具有变异性质的删除键方法似乎是唯一“符合预期”的行为。 - Jackson
显示剩余4条评论

244

这是一个简单的 Ruby 单行代码,仅适用于 Ruby 版本大于 1.9.x:

1.9.3p0 :002 > h = {:a => 1, :b => 2}
 => {:a=>1, :b=>2} 
1.9.3p0 :003 > h.tap { |hs| hs.delete(:a) }
 => {:b=>2} 

Tap方法始终返回调用它的对象...

否则,如果已经要求了active_support/core_ext/hash(在每个Rails应用程序中都会自动要求), 您可以根据需要使用以下方法之一:

➜  ~  irb
1.9.3p125 :001 > require 'active_support/core_ext/hash' => true 
1.9.3p125 :002 > h = {:a => 1, :b => 2, :c => 3}
 => {:a=>1, :b=>2, :c=>3} 
1.9.3p125 :003 > h.except(:a)
 => {:b=>2, :c=>3} 
1.9.3p125 :004 > h.slice(:a)
 => {:a=>1} 

except方法使用黑名单的方式,因此它会移除所有在参数中列出的键,而slice方法则使用白名单的方式,只保留参数中列出的键。这两个方法也有相应的修改原哈希表的bang版本(except!slice!),它们返回的值与原始方法不同,都是一个哈希表。对于slice!,返回被移除的键;对于except!,返回被保留的键:

1.9.3p125 :011 > {:a => 1, :b => 2, :c => 3}.except!(:a)
 => {:b=>2, :c=>3} 
1.9.3p125 :012 > {:a => 1, :b => 2, :c => 3}.slice!(:a)
 => {:b=>2, :c=>3} 

19
值得一提的是,这种方法对 h 有破坏性。Hash#except 不会修改原始哈希表。 - Mulan
9
使用 h.dup.tap { |hs| hs.delete(:a) } 可以避免修改原始哈希表。 - Jimbali

135

在Ruby中,有许多方法可以从哈希表中删除一个键并获取剩余的哈希表。

  1. .slice => 它将返回所选键而不从原始哈希表中删除它们。如果您想永久删除这些键,请使用slice!,否则请使用简单的slice

2.2.2 :074 > hash = {"one"=>1, "two"=>2, "three"=>3}
 => {"one"=>1, "two"=>2, "three"=>3} 
2.2.2 :075 > hash.slice("one","two")
 => {"one"=>1, "two"=>2} 
2.2.2 :076 > hash
 => {"one"=>1, "two"=>2, "three"=>3} 
  • .delete => 它将从原始哈希中删除所选键(它只能接受一个键而不是多个)。

    2.2.2 :094 > hash = {"one"=>1, "two"=>2, "three"=>3}
     => {"one"=>1, "two"=>2, "three"=>3} 
    2.2.2 :095 > hash.delete("one")
     => 1 
    2.2.2 :096 > hash
     => {"two"=>2, "three"=>3} 
    
  • .except => 它将返回余下的键,但不会从原始哈希中删除任何内容。如果您想永久删除键,请使用except!,否则请使用简单的except

  • 2.2.2 :097 > hash = {"one"=>1, "two"=>2, "three"=>3}
     => {"one"=>1, "two"=>2, "three"=>3} 
    2.2.2 :098 > hash.except("one","two")
     => {"three"=>3} 
    2.2.2 :099 > hash
     => {"one"=>1, "two"=>2, "three"=>3}         
    
  • .delete_if => 如果你需要基于值来删除一个键。它会从原始哈希表中显然删除匹配的键。

  • 2.2.2 :115 > hash = {"one"=>1, "two"=>2, "three"=>3, "one_again"=>1}
     => {"one"=>1, "two"=>2, "three"=>3, "one_again"=>1} 
    2.2.2 :116 > value = 1
     => 1 
    2.2.2 :117 > hash.delete_if { |k,v| v == value }
     => {"two"=>2, "three"=>3} 
    2.2.2 :118 > hash
     => {"two"=>2, "three"=>3} 
    
  • .compact => 用于从哈希中删除所有的nil值。如果您想永久性地删除nil值,则使用compact!,否则使用简单的compact

  • 2.2.2 :119 > hash = {"one"=>1, "two"=>2, "three"=>3, "nothing"=>nil, "no_value"=>nil}
     => {"one"=>1, "two"=>2, "three"=>3, "nothing"=>nil, "no_value"=>nil} 
    2.2.2 :120 > hash.compact
     => {"one"=>1, "two"=>2, "three"=>3}
    

    基于 Ruby 2.2.2 的结果。


    21
    可以使用ActiveSupport::CoreExtensions::Hash来添加sliceexcept。它们不是Ruby核心的一部分。可以通过 require 'active_support/core_ext/hash' 来使用它们。 - Madis Nõmme
    8
    自 Ruby 2.5 起,Hash#slice 已经成为标准库中的一部分。https://ruby-doc.org/core-2.5.0/Hash.html#method-i-slice 太好了! - Madis Nõmme
    非常感谢您的全面回答。 - Pablo

    39

    如果你想使用纯 Ruby(不使用 Rails),不想创建扩展方法(可能你只需要在一个或两个地方使用,不想在命名空间中大量污染方法),并且不想直接编辑散列(即,像我一样是函数式编程的粉丝),那么可以使用“select”:

    >> x = {:a => 1, :b => 2, :c => 3}
    => {:a=>1, :b=>2, :c=>3}
    >> x.select{|x| x != :a}
    => {:b=>2, :c=>3}
    >> x.select{|x| ![:a, :b].include?(x)}
    => {:c=>3}
    >> x
    => {:a=>1, :b=>2, :c=>3}
    

    35

    1
    SO需要一种方法来确保在代码库得到改进后,能够突出显示提供最佳答案的后来答案。在2023年,这个答案仍然排在第6位,真是太疯狂了。我会将此作为一个元问题提出。 - undefined

    30
    #in lib/core_extensions.rb
    class Hash
      #pass single or array of keys, which will be removed, returning the remaining hash
      def remove!(*keys)
        keys.each{|key| self.delete(key) }
        self
      end
    
      #non-destructive version
      def remove(*keys)
        self.dup.remove!(*keys)
      end
    end
    
    #in config/initializers/app_environment.rb (or anywhere in config/initializers)
    require 'core_extensions'
    
    我已经设置了这个函数,使得 .remove 返回一个去掉特定键值对后的哈希表副本,而 remove! 则会修改哈希表本身。这符合 Ruby 的惯例。例如,在控制台中:
    >> hash = {:a => 1, :b => 2}
    => {:b=>2, :a=>1}
    >> hash.remove(:a)
    => {:b=>2}
    >> hash
    => {:b=>2, :a=>1}
    >> hash.remove!(:a)
    => {:b=>2}
    >> hash
    => {:b=>2}
    >> hash.remove!(:a, :b)
    => {}
    

    27

    你可以使用 facets gem 中的 except!

    >> require 'facets' # or require 'facets/hash/except'
    => true
    >> {:a => 1, :b => 2}.except(:a)
    => {:b=>2}
    

    原始哈希值不会改变。

    编辑:正如Russel所说,facets存在一些隐藏问题,并且与ActiveSupport不完全兼容。 另一方面,ActiveSupport并不像facets那样完整。 最终,我会使用AS并在您的代码中处理边缘情况。


    只需使用 require 'facets/hash/except',就不会出现任何 "问题"(不确定除了不完全符合 AS API 之外还有什么问题)。如果您正在使用 AS 进行 Rails 项目,则使用它是有意义的;否则,Facets 的占用空间要小得多。 - trans
    ActiveSupport现在的占用空间也相当小,你可以只引入其中的部分。就像facets一样,但是有更多人关注它(所以我想它会得到更好的评价)。 - rewritten

    21

    如果您正在使用Ruby 2,而不是猴子补丁或需要不必要地包含大型库,您可以使用Refinements

    module HashExtensions
      refine Hash do
        def except!(*candidates)
          candidates.each { |candidate| delete(candidate) }
          self
        end
    
        def except(*candidates)
          dup.remove!(candidates)
        end
      end
    end
    

    您可以使用此功能,而不会影响程序的其他部分,也不需要引入大型外部库。

    class FabulousCode
      using HashExtensions
    
      def incredible_stuff
        delightful_hash.except(:not_fabulous_key)
      end
    end
    

    18

    纯 Ruby 中:

    {:a => 1, :b => 2}.tap{|x| x.delete(:a)}   # => {:b=>2}
    

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