将哈希表递归转换为OpenStruct

38

假设我有这个哈希表:

 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"

我该如何实现这个目标?


1
如果你喜欢OpenStruct,你一定会喜欢Hashie - tadman
@donato 我知道这是一个老问题。但请查看我在这里提供的答案https://dev59.com/hV8d5IYBdhLWcg3wcx5V#52745801,看看是否有所帮助。 - Abhilash Reddy
我已经为openstruct创建了一个PR https://github.com/ruby/ostruct/pull/5,以便将其包含在库中。如果您认为它会帮助像您一样的其他人,请支持它。 - Abhilash Reddy
这个回答解决了你的问题吗?将哈希转换为对象 - Jon Schneider
9个回答

52
你可以对 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'

查看将复杂嵌套的哈希转换为对象


8
使用 Ruby 已经有十多年了,我仍然对其优雅感到惊讶。 - chris finne
2
虽然这个方法可以解决问题,但是它会在项目中所有地方改变默认的Hash行为,可能会在未来出现意想不到的问题。因此,我更倾向于使用max_pleaner或Donato提供的JSON.parse(h.to_json, object_class: OpenStruct)方法,它似乎可以在原地解决问题而没有副作用。 - Wasif Hossain
2
FYI,这仅适用于简单值。当我将此方法应用于更复杂的对象(例如BigDecimal或自定义ActiveRecord对象)时,遇到了问题。 - Jack Kinsella
是的,它将符号的值更改为字符串。 - Piioo

33
我提出了这个解决方案:
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。


混乱但是能用。不过还有更好的方法。 - tadman
1
依赖关系?Openstruct和Json是标准库的一部分。 - Mikey T.K.
很好,这似乎工作得很好。我遇到的一个区别是我不得不调用 object.table.c.d - Tony Beninate
1
我认为仅为了解析而生成JSON并不是一种非常高效的方法。 - Joe Half Face
当一个值是符号时,将其转换为字符串。 - Piioo

29

就我个人而言,我使用recursive-open-struct宝石——这样做只需要RecursiveOpenStruct.new(<nested_hash>)就行了。

但是为了练习递归,我将向您展示一个崭新的解决方案:

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

编辑

Weihang Jian在这里稍微改进了一下 https://dev59.com/yVgQ5IYBdhLWcg3wl1B3#69311716

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

另请参见https://dev59.com/yVgQ5IYBdhLWcg3wl1B3#63264908,其中展示了如何处理数组。

注意:

之所以这比基于JSON的解决方案更好,是因为将其转换为JSON时可能会丢失一些数据。例如,如果您将Time对象转换为JSON,然后解析它,它将变成一个字符串。还有许多其他类似的例子:

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

是的...不是非常有用。你已经完全失去了对实际实例的引用。


1
我使用了你的答案,并且在哈希表中运行良好,如果数组值内部有哈希表怎么办,例如 { a: 'a', b: 'b', c: [{ d: 'd', e: 'e'}] },那么我可以执行 c.d => 'd'。谢谢 :) - aldrien.h
1
JSON.parse 的答案更加灵活和优雅。 - Tonči D.
1
@TončiD,这真的不是问题。问题在于并非所有内容都可以转换为JSON并保持其相同的Ruby类。例如,JSON.parse({now: Time.now}.to_json)["now"].class == String... 当您解析/取消解析时,Time.now不再是一个Time对象。 - max pleaner
@maxpleaner 同意。我对适合我的情况有偏见。 - Tonči D.
1
@maxpleaner 在 memo[key] = ... 下面添加以下代码即可处理数组:memo[key] = val.map { |v| v.is_a?(Hash) ? to_recursive_ostruct(v) : v } if val.is_a?(Array) - Medardas
显示剩余3条评论

9
这里有一个递归解决方案,避免将哈希转换为 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 已弃用,在我看来,这是最佳解决方案。 - Christiano Matos
1
@ChristianoMatos,你能提供一些关于弃用的参考资料吗?我没有看到任何证据。 - Weihang Jian

3

我基于max pleaner的回答并类似于Xavi的回答的解决方案:

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

3

我的解决方案比@max-pleaner的更加简洁快速。

我其实不知道为什么,但我不会实例化额外的哈希对象:

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

0
这里有一种方法可以覆盖初始化器,以便您可以执行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

0

基于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

无论您选择哪种解决方案,我建议使用以下哈希进行测试: < p > < code > people = { person1: { display: { first: 'John' } }, person2: { display: { last: 'Jingleheimer' } } }


0

如果你不介意猴子补丁 Hash 类,你可以这么做:

require 'ostruct'

module Structurizable
  def each_pair(&block)
    each do |k, v|
      v = OpenStruct.new(v) if v.is_a? Hash

      yield k, v
    end
  end
end

Hash.prepend Structurizable

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

puts OpenStruct.new(people).person1.display.first

理想情况下,我们应该能够使用一种改进而不是假装这样做,但出于某些我无法理解的原因,each_pair方法并没有起作用(另外,不幸的是,改进仍然非常有限)。

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