【问题标题】:Convert Hash to OpenStruct recursively以递归方式将 Hash 转换为 OpenStruct
【发布时间】:2022-05-04 13:08:32
【问题描述】:

鉴于我有这个哈希:

 h = { a: 'a', b: 'b', c: { d: 'd', e: 'e'} }

然后我转换为 OpenStruct:

o = OpenStruct.new(h)
 => #<OpenStruct a="a", b="b", c={:d=>"d", :e=>"e"}> 
o.a
 => "a" 
o.b
 => "b" 
o.c
 => {:d=>"d", :e=>"e"} 
2.1.2 :006 > o.c.d
NoMethodError: undefined method `d' for {:d=>"d", :e=>"e"}:Hash

我希望所有嵌套的键也是方法。所以我可以这样访问d

o.c.d
=> "d"

我怎样才能做到这一点?

【问题讨论】:

标签: ruby


【解决方案1】:

您可以对 Hash 类进行猴子补丁

class Hash
  def to_o
    JSON.parse to_json, object_class: OpenStruct
  end
end

那你可以说

h = { a: 'a', b: 'b', c: { d: 'd', e: 'e'} }
o = h.to_o
o.c.d # => 'd'

Convert a complex nested hash to an object

【讨论】:

  • 使用 Ruby 已经 10 多年了,我仍然对它的优雅感到惊讶。
  • 虽然它应该可以解决问题,但是这个过程有一些副作用,比如它会改变项目中所有地方的默认Hash行为,这可能会在未来几天产生无法预料的问题.所以我宁愿选择 max_pleaner 或 Donato JSON.parse(h.to_json, object_class: OpenStruct),这似乎可以在没有副作用的情况下解决问题。
  • 仅供参考,这只适用于简单的值。当我将此方法应用于更复杂的对象(例如 BigDecimal 或自定义 ActiveRecord 对象)时,我遇到了问题
【解决方案2】:

我想出了这个解决方案:

h = { a: 'a', b: 'b', c: { d: 'd', e: 'e'} }
json = h.to_json
=> "{\"a\":\"a\",\"b\":\"b\",\"c\":{\"d\":\"d\",\"e\":\"e\"}}" 
object = JSON.parse(json, object_class:OpenStruct)
object.c.d
 => "d" 

所以为了让它工作,我必须做一个额外的步骤:将它转换为 json。

【讨论】:

  • 凌乱,但实用。不过还有更好的方法。
  • 依赖项? Openstruct 和 Json 是标准库的一部分。
  • 很好,这似乎运作良好。我遇到的一个区别是我必须打电话给object.table.c.d
  • 我相信生成json只是为了解析它不是很有效的方法
【解决方案3】:

我个人使用recursive-open-struct gem - 然后就像RecursiveOpenStruct.new(&lt;nested_hash&gt;) 一样简单

但是为了递归练习,我给你展示一个新的解决方案:

require 'ostruct'

def to_recursive_ostruct(hash)
  result = hash.each_with_object({}) do |(key, val), memo|
    memo[key] = val.is_a?(Hash) ? to_recursive_ostruct(val) : val
  end
  OpenStruct.new(result)
end

puts to_recursive_ostruct(a: { b: 1}).a.b
# => 1

编辑

这比基于 JSON 的解决方案更好的原因是,当您转换为 JSON 时可能会丢失一些数据。例如,如果您将 Time 对象转换为 JSON 然后解析它,它将是一个字符串。这样的例子还有很多:

class Foo; end
JSON.parse({obj: Foo.new}.to_json)["obj"]
# => "#<Foo:0x00007fc8720198b0>"

是的...不是超级有用。您已经完全失去了对实际实例的引用。

【讨论】:

  • 我使用了您的答案并且与哈希一起工作得很好,如果数组值与哈希内部例如{ a: 'a', b: 'b', c: [{ d: 'd', e: 'e'}] },所以我可以做 c.d => 'd' 谢谢 :)
  • @aldrien.h 如果你有{c: [{d: 'd'}]},你为什么要使用c.d?如果数组中有多个哈希值,这将如何工作?
  • JSON.parse 答案更加灵活和优雅。
  • @TončiD。真的不是。问题是并非所有内容都可以转换为 JSON 并保持其相同的 Ruby 类。例如JSON.parse({now: Time.now}.to_json)["now"].class == String ... Time.now 不再是 Time 对象,当您解析/取消解析它时。
  • @maxpleaner 在memo[key] = ... 下面添加这一行将处理数组:memo[key] = val.map { |v| v.is_a?(Hash) ? to_recursive_ostruct(v) : v } if val.is_a?(Array)
【解决方案4】:

这是一个避免将哈希转换为 json 的递归解决方案:

def to_o(obj)
  if obj.is_a?(Hash)
    return OpenStruct.new(obj.map{ |key, val| [ key, to_o(val) ] }.to_h)
  elsif obj.is_a?(Array)
    return obj.map{ |o| to_o(o) }
  else # Assumed to be a primitive value
    return obj
  end
end

【讨论】:

  • 因为 each_with_object 已被弃用,对我来说这是最好的解决方案
  • @ChristianoMatos 你能提供一些关于弃用的参考吗?我没有看到任何证据。
【解决方案5】:

我的解决方案,基于max pleaner's answer,类似于Xavi's answer

require 'ostruct'

def initialize_open_struct_deeply(value)
  case value
  when Hash
    OpenStruct.new(value.transform_values { |hash_value| send __method__, hash_value })
  when Array
    value.map { |element| send __method__, element }
  else
    value
  end
end

【讨论】:

    【解决方案6】:

    这是覆盖初始化程序的一种方法,因此您可以执行OpenStruct.new({ a: "b", c: { d: "e", f: ["g", "h", "i"] }})

    另外,这个类在你require 'json'时包含,所以一定要在require之后做这个补丁。

    class OpenStruct
      def initialize(hash = nil)
        @table = {}
        if hash
          hash.each_pair do |k, v|
            self[k] = v.is_a?(Hash) ? OpenStruct.new(v) : v
          end
        end
      end
    
      def keys
        @table.keys.map{|k| k.to_s}
      end
    end
    

    【讨论】:

      【解决方案7】:

      我的解决方案比@max-pleaner 的解决方案更干净、更快捷。

      我实际上不知道为什么,但我没有实例化额外的 Hash 对象:

      def dot_access(hash)
        hash.each_with_object(OpenStruct.new) do |(key, value), struct|
          struct[key] = value.is_a?(Hash) ? dot_access(value) : value
        end
      end
      

      以下是基准供您参考:

      require 'ostruct'
      
      def dot_access(hash)
        hash.each_with_object(OpenStruct.new) do |(key, value), struct|
          struct[key] = value.is_a?(Hash) ? dot_access(value) : value
        end
      end
      
      def to_recursive_ostruct(hash)
        result = hash.each_with_object({}) do |(key, val), memo|
          memo[key] = val.is_a?(Hash) ? to_recursive_ostruct(val) : val
        end
        OpenStruct.new(result)
      end
      
      require 'benchmark/ips'
      Benchmark.ips do |x|
        hash = { a: 1, b: 2, c: { d: 3 } }
        x.report('dot_access') { dot_access(hash) }
        x.report('to_recursive_ostruct') { to_recursive_ostruct(hash) }
      end
      
      Warming up --------------------------------------
                dot_access     4.843k i/100ms
      to_recursive_ostruct     5.218k i/100ms
      Calculating -------------------------------------
                dot_access     51.976k (± 5.0%) i/s -    261.522k in   5.044482s
      to_recursive_ostruct     50.122k (± 4.6%) i/s -    250.464k in   5.008116s
      

      【讨论】:

        【解决方案8】:

        基于 OpenStruct 的转换可以正常工作,直到不能正常工作。例如,这里的其他答案都不能正确处理这些简单的哈希:

        people = { person1: { display: { first: 'John' } } }
        creds = { oauth: { trust: true }, basic: { trust: false } }
        

        下面的方法适用于这些哈希,修改输入哈希而不是返回一个新对象。

        def add_indifferent_access!(hash)
          hash.each_pair do |k, v|
            hash.instance_variable_set("@#{k}", v.tap { |v| send(__method__, v) if v.is_a?(Hash) } )
            hash.define_singleton_method(k, proc { hash.instance_variable_get("@#{k}") } )
          end
        end
        

        然后

        add_indifferent_access!(people)
        people.person1.display.first # => 'John'
        

        或者,如果您的上下文需要更内联的调用结构:

        creds.yield_self(&method(:add_indifferent_access!)).oauth.trust # => true
        

        或者,您可以将其混入:

        module HashExtension
          def very_indifferent_access!
            each_pair do |k, v|
              instance_variable_set("@#{k}", v.tap { |v| v.extend(HashExtension) && v.send(__method__) if v.is_a?(Hash) } )
              define_singleton_method(k, proc { self.instance_variable_get("@#{k}") } )
            end
          end
        end
        

        并应用于单个哈希:

        favs = { song1: { title: 'John and Marsha', author: 'Stan Freberg' } }
        favs.extend(HashExtension).very_indifferent_access!
        favs.song1.title
        

        如果您选择这样做,这是猴子修补哈希的一种变体:

        class Hash
          def with_very_indifferent_access!
            each_pair do |k, v|
              instance_variable_set("@#{k}", v.tap { |v| v.send(__method__) if v.is_a?(Hash) } )
              define_singleton_method(k, proc { instance_variable_get("@#{k}") } )
            end
          end
        end
        # Note the omission of "v.extend(HashExtension)" vs. the mix-in variation.
        

        对其他答案的评论表达了保留类类型的愿望。这个解决方案可以满足这一点。

        people = { person1: { created_at: Time.now } }
        people.with_very_indifferent_access!
        people.person1.created_at.class # => Time
        

        无论您选择哪种解决方案,我都建议使用此哈希进行测试:

        people = { person1: { display: { first: 'John' } }, person2: { display: { last: 'Jingleheimer' } } }

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多