Ruby 如何一行获取嵌套 Hash 数据?

linjunhalida · 2012年08月30日 · 最后由 linjunhalida 回复于 2012年10月04日 · 5877 次阅读

很多时候, 需要取这样的数据:

value = data[:row][:column][:field]

但是数据源并不是完整的, 如果其中一层没有数据, 需要返回空, 如果一行行验证:

return unless data[:row]
return unless data[:row][:column]

value = data[:row][:column][:field]

有点繁琐,

也可以这样:

value = data[:row][:column][:field] rescue nil

不过我觉得是对 rescue 的滥用。

请问大家有什么好的方案没有?

data = Hash.new({})

恩, 不成:

[1] pry(main)> data = Hash.new({})
=> {}
[2] pry(main)> data['a']['b']
=> nil
[3] pry(main)> data['a']['b']['c']
NoMethodError: undefined method `[]' for nil:NilClass

这么多层。。。。 用 try 吧

如果在 rails 环境下, 贡献一个更丑的方式:

1.9.3p194 :001 > data = {a: 1, b:2}
 => {:a=>1, :b=>2} 
1.9.3p194 :002 > data.try(:[], :a)
 => 1 
1.9.3p194 :003 > data.try(:[], :c)
 => nil

恩, 我想到了一个魔法:


class Hash
  def loop_get *args
    v = self
    args.each do |arg|
      return unless v[arg]
      v = v[arg]
    end
    v
  end
end

data = {}
data.loop_get :a, :b, :c # => nil

如果你觉得给 Nil 打 monkey patch 无所谓的话:

class NilClass
  def [] key
  end
end
module NullableHash
  class NullObject
    def method_missing(*args)
      self
    end

    def nil?
      true
    end
  end

  def [](sym)
    super(sym) || NullObject.new
  end
end

data.extend(NullableHash)
data[:sfsda][:bsdf][:csdf][:dsdf][:afsdfasdf].nil?
=> true

data['a']['b']['c] rescue nil

@knwang 最好还是不要用 method_missing 吧,需要查找两遍方法链

自己写一个

module Kernel
  def try(&block)
    block.call rescue nil
  end
end

value = try { data[:row][:column][:field] }

用 fetch 貌似管用。。hash.fetch(:a){{}}

lz 那个就不错啊 或者

def nil.[] k
end

#10 楼 @tumayun 在这个情况下也可以写成 def []

写成 method_missing 的 NullObject 可以对任何对象扩展。 性能的话如果是 Rails App 没所谓, 因为 Rails 里面已经是遍地 method_missing; 即使大型的 Ruby app 也很少是 cpu 瓶颈的;如果真的是性能关键可以考虑别的语言写

@knwang 不到不得已的情况不赞成写 method_missing, rails 里面很少这样直接 method_missing 的,一般会在 method_missing 里面定义方法,不用每次都到 method_missing

#15 楼 @tumayun

如果要写一般的 Null Object 是要用 method_missing 的, 因为需要对任何的 call 返回 self

http://en.wikipedia.org/wiki/Null_Object_pattern http://devblog.avdi.org/2011/05/30/null-objects-and-falsiness/

@knwang 恩,是的,我是想说有更好的选择

函数式解决方案: (其实和 6 楼的差不多,本质上也是 Null Object)

class Maybe < Struct.new(:o)
  def chain m, *xs
    v = o
    xs.each do |x|
      return if v.nil?
      v = v.send m, x
    end
    v
  end
end

value = Maybe[data].chain :[], :row, :column, :field

没污染,不仅适用于 [],也适用于其他方法的 nil 链,data 为 nil 时也能工作

匿名 #20 2012年09月03日

嘎嘎,尽量做两层就好,嘿嘿

a = {} a.fetch(:a, {}).fetch(:b, {}).fetch(:c, nil)

h = Hash.new{|h,k| Hash.new(&h.default_proc)} puts h['usa']['ny']['nyc'].inspect #=> {}

我大多数时候是用的 try,但是如果嵌套太深一路 try 过去也不是很舒服。 我很喜欢 #8 楼 @knwang 的方案... 不改变调用的写法和创建 Hash 的写法很重要...

如果要改写法,那就用 try 就可以了...

#18 楼 @luikore 我喜欢这个方案,少了不必要的循环,不过不太 OO,我改成这样的 mokey patch——

class Object
  def trys *arg
    arg.inject(self) do |result, x|
      return if result.nil?
      result = result.send :x
    end
  end
end
nil.trys :a, :b, :c, :d

@poshboytl try 最好不要用, 从 python 的经验看, 异常捕获和抛出异常开销非常大, 不适合这种跑很多次的方法。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号