【问题标题】:How to elegantly symbolize_keys for a 'nested' hash如何优雅地为“嵌套”哈希符号化密钥
【发布时间】:2014-09-15 15:37:18
【问题描述】:

考虑以下代码:

  hash1 = {"one" => 1, "two" => 2, "three" => 3}
  hash2 = hash1.reduce({}){ |h, (k,v)| h.merge(k => hash1) }
  hash3 = hash2.reduce({}){ |h, (k,v)| h.merge(k => hash2) }
  hash4 = hash3.reduce({}){ |h, (k,v)| h.merge(k => hash3) }

hash4 是一个“嵌套”哈希,即带有字符串键和类似“嵌套”哈希值的哈希。

Rails 中 Hash 的 'symbolize_keys' 方法让我们可以轻松地将字符串键转换为符号。但我正在寻找一种优雅的方法来将所有键(主键加上 hash4 中所有散列的键)转换为符号。

关键是要让自己摆脱我 (imo) 丑陋的解决方案:

  class Hash
    def symbolize_keys_and_hash_values
      symbolize_keys.reduce({}) do |h, (k,v)|
        new_val = v.is_a?(Hash) ? v.symbolize_keys_and_hash_values : v
        h.merge({k => new_val})
      end
    end
  end

  hash4.symbolize_keys_and_hash_values #=> desired result

仅供参考:设置为 Rails 3.2.17 和 Ruby 2.1.1

更新:

Rails 的答案是 hash4.deep_symbolize_keys

Rails > 5 的答案是 JSON.parse(JSON[hash4], symbolize_names: true)

【问题讨论】:

  • @jvnill - 把它写成答案...
  • @SyedHumzaShah 但是,请注意,您必须“深度符号化”表明您对哈希中的实际内容缺乏了解。避免使用该方法并使用您对如何构建数据结构的知识。如果它是完全动态的以至于您确实需要深度符号化,您可以考虑使用树对象,其遍历方法和其他与树相关的资产。
  • @BorisStitnicky 如果您的哈希不是由您生成的,例如一个你正在加载的配置文件,那么通常不知道哈希中的实际内容......

标签: ruby-on-rails ruby hash dry


【解决方案1】:

有几种方法可以做到这一点

  1. 有一个deep_symbolize_keys method in Rails

    hash.deep_symbolize_keys!

  2. 正如@chrisgeeq 所述,Rails 4 提供了一个deep_transform_keys 方法。

    hash.deep_transform_keys(&:to_sym)

    还有一个 bang! 版本来替换现有对象。

  3. 还有另一种方法称为with_indifferent_access。这允许您使用字符串或符号访问散列,例如 params 在控制器中的样子。此方法没有 bang 对应项。

    hash = hash.with_indifferent_access

  4. 最后一个是使用JSON.parse。我个人不喜欢这样,因为您正在进行 2 次转换 - 哈希到 json 然后 json 到哈希。

    JSON.parse(JSON[h], symbolize_names: true)

更新:

16/01/19 - 添加更多选项并注意弃用 deep_symbolize_keys

19/04/12 - 删除已弃用的注释。只有方法中使用的实现被弃用,而不是方法本身。

【讨论】:

  • 此方法在 rails >= 5.1 已弃用,而是使用了transform_keys see this
  • deep_symbolize_keys 方法根本不被弃用。 @chrisgeeq 的评论指向使用 transform_keys 而不是弃用的 symbolize_keys 方法的提交。请编辑此答案以更正此问题。
【解决方案2】:

您不能再将此方法用于 params 或任何其他 ActionController::Parameters 实例,因为出于安全原因,deep_symbolize_keys 方法在 Rails 5.0+ 中已弃用,并将在 Rails 5.1+ 中删除,因为 ActionController::Parameters 不再 继承自哈希

所以@Uri Agassi 的这种方法似乎是通用的

JSON.parse(JSON[h], symbolize_names: true)

但是,Rails Hash 对象仍然有它。

所以选项是:

  • 如果你不使用 Rails 或者只是不关心:

    JSON.parse(JSON[h], symbolize_names: true)
    
  • 使用 Rails 和 ActionController::Parameters:

    params.to_unsafe_h.deep_symbolize_keys
    
  • 使用 Rails 和普通哈希

    h.deep_symbolize_keys
    

【讨论】:

  • 关于它的弃用的任何信息? its documentation 上没有弃用通知,我在使用它时没有遇到任何弃用警告。
  • @FranklinYu 我已经编辑了答案,添加了一些细节。
  • 它在ActionController::Parameters 上已被弃用(正如this issue 所暗示的那样),但不是在Hash 上,正如我上面所说的。正如您的报价所说,ActionController::Parameters 不再继承自哈希。这个问题是关于Hash
【解决方案3】:

在 Rails 中,您可以创建 HashWithIndifferentAccess 类。创建这个类的一个实例,将你的哈希传递给它的构造函数,然后使用符号或字符串的键(如控制器操作的参数)访问它:

hash = {'a' => {'b' => [{c: 3}]}}

hash = hash.with_indifferent_access
# equal to:
# hash = ActiveSupport::HashWithIndifferentAccess.new(hash)

hash[:a][:b][0][:c]

=> 3

【讨论】:

    【解决方案4】:

    我可以提出这样的建议:

    class Object
      def deep_symbolize_keys
        self
      end
    end
    
    class Hash
      def deep_symbolize_keys
        symbolize_keys.tap { |h| h.each { |k, v| h[k] = v.deep_symbolize_keys } }
      end
    end
    
    {'a'=>1, 'b'=>{'c'=>{'d'=>'d'}, e:'f'}, 'g'=>1.0, 'h'=>nil}.deep_symbolize_keys
    # => {:a=>1, :b=>{:c=>{:d=>"d"}, :e=>"f"}, :g=>1.0, :h=>nil} 
    

    您还可以轻松扩展它以支持Arrays

    class Array
      def deep_symbolize_keys
        map(&:deep_symbolize_keys)
      end
    end
    
    {'a'=>1, 'b'=>[{'c'=>{'d'=>'d'}}, {e:'f'}]}.deep_symbolize_keys
    # => {:a=>1, :b=>[{:c=>{:d=>"d"}}, {:e=>"f"}]}
    

    【讨论】:

    • 感谢您的回复,乌里。显然,Rails 附带了一个内置的“deep_symbolize_keys”方法。我已经阅读了 Ruby 文档,但愚蠢的是,没有阅读 Rails API 文档。 jvnill(上图)能够为我指明正确的方向。
    • @SyedHumzaShah - 哈哈,我也没有意识到这一点。但它不支持数组:P
    【解决方案5】:

    我可以建议:

    JSON.parse(hash_value.to_json)
    

    【讨论】:

      【解决方案6】:

      你可以使用:

      • Hash#to_s 将哈希转换为字符串;
      • String#gsub 使用正则表达式将键从字符串转换为符号表示;然后
      • Kernel#eval 将字符串转换回哈希。

      这是解决您的问题的简单方法,但是,只有在您确信 eval 不会产生令人讨厌的事情时,您才应该考虑使用它。如果您可以控制要转换的哈希的内容,那应该不是问题。

      这种方法可用于其他类型的嵌套对象,例如同时包含数组和哈希的对象。

      代码

      def symbolize_hash(h)
        eval(h.to_s.gsub(/\"(\w+)\"(?==>)/, ':\1'))
      end
      

      示例

      symbolize_hash(hash4)
        #=> {:one=>{:one=>  {:one=>  {:one=>1, :two=>2, :three=>3},
        #                    :two=>  {:one=>1, :two=>2, :three=>3},
        #                    :three=>{:one=>1, :two=>2, :three=>3}},
        #           :two=>  {:one=>  {:one=>1, :two=>2, :three=>3},
        #                    :two=>  {:one=>1, :two=>2, :three=>3},
        #                    :three=>{:one=>1, :two=>2, :three=>3}},
        #           :three=>{:one=>  {:one=>1, :two=>2, :three=>3},
        #                    :two=>  {:one=>1, :two=>2, :three=>3},
        #                    :three=>{:one=>1, :two=>2, :three=>3}}},
        #    :two=>{:one=>  {:one=>  {:one=>1, :two=>2, :three=>3},
        #    ...
        #    :three=>{:one=>{:one=>  {:one=>1, :two=>2, :three=>3},
        #    ...
        #                    :three=>{:one=>1, :two=>2, :three=>3}}}}
      
      symbolize_hash({'a'=>1, 'b'=>[{'c'=>{'d'=>'d'}}, {e:'f'}]})
        #=> {:a=>1, :b=>[{:c=>{:d=>"d"}}, {:e=>"f"}]}
      

      说明

      (?==>) 在正则表达式中是一个零宽度的正向前瞻。 ?= 表示积极的前瞻; => 是必须紧跟\"(\w+)\" 匹配的字符串。 \1 in ':\1'(或者我可以写成":\\1")是一个以冒号开头的字符串,后跟对捕获组#1 内容的反向引用,键匹配\w+(不带引号)。

      【讨论】:

      • 如果你要序列化和解析——你不觉得 JSON 更合适吗:JSON.parse(JSON[h], symbolize_names: true)?
      • 你可能是对的,@Uri。我一直想找时间看看 JSON 是什么。
      • 这里关于 JSON.parse 具有“symbolize_names”选项的这一点是一个隐藏的宝石 - 谢谢。
      猜你喜欢
      • 1970-01-01
      • 2012-05-08
      • 2014-02-24
      • 2014-08-10
      • 1970-01-01
      • 2013-10-21
      • 2021-12-22
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多