30

我正在寻找一种避免nil在深度嵌套的哈希中检查每个级别的好方法。例如:

name = params[:company][:owner][:name] if params[:company] && params[:company][:owner] && params[:company][:owner][:name]

这需要三项检查,并且代码非常难看。有什么办法可以解决这个问题?

4

16 回答 16

34

Ruby 2.3.0 引入了一种新方法,在这两者上都调用digHash了,Array它完全解决了这个问题。

name = params.dig(:company, :owner, :name)

nil如果密钥在任何级别丢失,它就会返回。

如果您使用的 Ruby 版本早于 2.3,您可以使用ruby​​_dig gem或自己实现它:

module RubyDig
  def dig(key, *rest)
    if value = (self[key] rescue nil)
      if rest.empty?
        value
      elsif value.respond_to?(:dig)
        value.dig(*rest)
      end
    end
  end
end

if RUBY_VERSION < '2.3'
  Array.send(:include, RubyDig)
  Hash.send(:include, RubyDig)
end
于 2016-01-06T00:25:48.557 回答
11

功能和清晰度之间的最佳折衷 IMO 是 Raganwald 的andand. 有了这个,你会做:

params[:company].andand[:owner].andand[:name]

它类似于try,但在这种情况下读起来要好得多,因为你仍然像往常一样发送消息,但在它之间有一个分隔符,提醒你注意你正在特别对待 nils 的事实。

于 2010-12-06T23:20:02.997 回答
7

我不知道这是否是你想要的,但也许你可以这样做?

name = params[:company][:owner][:name] rescue nil
于 2010-12-06T23:11:46.260 回答
4

相当于用户mpd建议的第二种解决方案,只是更惯用的 Ruby:

class Hash
  def deep_fetch *path
    path.inject(self){|acc, e| acc[e] if acc}
  end
end

hash = {a: {b: {c: 3, d: 4}}}

p hash.deep_fetch :a, :b, :c
#=> 3
p hash.deep_fetch :a, :b
#=> {:c=>3, :d=>4}
p hash.deep_fetch :a, :b, :e
#=> nil
p hash.deep_fetch :a, :b, :e, :f
#=> nil
于 2010-12-07T18:58:56.750 回答
4

您可能想研究一种将自动激活添加到 ruby​​ 哈希的方法。以下stackoverflow线程中提到了许多方法:

于 2010-12-07T00:39:15.967 回答
3

如果你想进入猴子补丁,你可以做这样的事情

class NilClass
  def [](anything)
    nil
  end
end

然后,如果任何时候嵌套散列之一为 nil,则调用params[:company][:owner][:name]将产生 nil。

编辑:如果你想要一条更安全的路线,也可以提供干净的代码,你可以做类似的事情

class Hash
  def chain(*args)
    x = 0
    current = self[args[x]]
    while current && x < args.size - 1
      x += 1
      current = current[args[x]]
    end
    current
  end
end

代码如下所示:params.chain(:company, :owner, :name)

于 2010-12-06T23:13:53.157 回答
3

如果是导轨,请使用

params.try(:[], :company).try(:[], :owner).try(:[], :name)

哦等等,那更难看。;-)

于 2010-12-06T23:07:04.743 回答
2

我会这样写:

name = params[:company] && params[:company][:owner] && params[:company][:owner][:name]

它不像? Io 中的运算符,但 Ruby 没有。@ThiagoSilveira 的回答也不错,虽然会慢一些。

于 2010-12-06T23:21:50.433 回答
1

写一次丑,然后隐藏

def check_all_present(hash, keys)
  current_hash = hash
  keys.each do |key|
    return false unless current_hash[key]
    current_hash = current_hash[key]
  end
  true
end
于 2010-12-07T01:30:34.943 回答
1

(即使这是一个非常古老的问题,也许这个答案对于像我这样没有想到“开始救援”控制结构表达式的一些 stackoverflow 人来说很有用。)

我会用 try catch 语句来做(开始用 ruby​​ 语言救援):

begin
    name = params[:company][:owner][:name]
rescue
    #if it raises errors maybe:
    name = 'John Doe'
end
于 2012-10-06T13:51:47.513 回答
1

您是否能够避免使用多维散列,并使用

params[[:company, :owner, :name]]

或者

params[[:company, :owner, :name]] if params.has_key?([:company, :owner, :name])

反而?

于 2010-12-06T23:50:25.140 回答
1

做:

params.fetch('company', {}).fetch('owner', {})['name']

同样在每一步,如果它是数组、字符串或数字,您可以使用内置的适当方法NilClass来逃避。nil只需添加to_hash到此列表的清单并使用它。

class NilClass; def to_hash; {} end end
params['company'].to_hash['owner'].to_hash['name']
于 2011-04-01T13:48:57.647 回答
0

您不需要访问原始哈希定义——您可以在使用 h.instance_eval 获取它后即时覆盖 [] 方法,例如

h = {1 => 'one'}
h.instance_eval %q{
  alias :brackets :[]
  def [] key
    if self.has_key? key
      return self.brackets(key)
    else
      h = Hash.new
      h.default = {}
      return h
    end
  end
}

但这对您拥有的代码没有帮助,因为您依赖于未找到的值来返回错误值(例如,nil),并且如果您执行任何与您上方相关的“正常”自动激活内容' 最终会得到一个未找到值的空散列,其评估结果为“真”。

你可以做这样的事情——它只检查定义的值并返回它们。你不能这样设置它们,因为我们无法知道调用是否在任务的 LHS 上。

module AVHash
  def deep(*args)
    first = args.shift
    if args.size == 0
      return self[first]
    else
      if self.has_key? first and self[first].is_a? Hash
        self[first].send(:extend, AVHash)
        return self[first].deep(*args)
      else
        return nil
      end
    end
  end
end      

h = {1=>2, 3=>{4=>5, 6=>{7=>8}}}
h.send(:extend, AVHash)
h.deep(0) #=> nil
h.deep(1) #=> 2
h.deep(3) #=> {4=>5, 6=>{7=>8}}
h.deep(3,4) #=> 5
h.deep(3,10) #=> nil
h.deep(3,6,7) #=> 8

不过,同样,您只能用它检查值——不能分配它们。所以它不是真正的自动激活,正如我们在 Perl 中所知道和喜爱的那样。

于 2010-12-07T05:09:11.177 回答
0

只是为了提供一个dig,试试我写的KeyDial gem。这本质上是一个包装器,dig但重要的区别是它永远不会出错。

digdig如果链中的对象属于某种本身不能被编辑的类型,仍然会吐出错误。

hash = {a: {b: {c: true}, d: 5}}

hash.dig(:a, :d, :c) #=> TypeError: Integer does not have #dig method

在这种情况下dig对您没有帮助,您不仅需要返回,hash[:a][:d].nil? &&还需要hash[:a][:d].is_a?(Hash)检查。KeyDial 允许您在没有此类检查或错误的情况下执行此操作:

hash.call(:a, :d, :c) #=> nil
hash.call(:a, :b, :c) #=> true
于 2019-01-09T01:27:32.997 回答
0

危险但有效:

class Object
        def h_try(key)
            self[key] if self.respond_to?('[]')
        end    
end

我们可以新做

   user = { 
     :first_name => 'My First Name', 
     :last_name => 'my Last Name', 
     :details => { 
        :age => 3, 
        :birthday => 'June 1, 2017' 
      } 
   }

   user.h_try(:first_name) # 'My First Name'
   user.h_try(:something) # nil
   user.h_try(:details).h_try(:age) # 3
   user.h_try(:details).h_try(:nothing).h_try(:doesnt_exist) #nil

“h_try”链遵循与“try”链相似的风格。

于 2017-07-26T17:05:03.857 回答
0

TLDR;params&.dig(:company, :owner, :name)

从 Ruby 2.3.0 开始:

您还可以使用&.称为“安全导航运算符”的方式:params&.[](:company)&.[](:owner)&.[](:name)。这个绝对安全。

使用digonparams实际上并不安全,如果为 nilparams.dig则会失败。params

但是,您可以将两者结合为:params&.dig(:company, :owner, :name).

因此,以下任何一种都可以安全使用:

params&.[](:company)&.[](:owner)&.[](:name)

params&.dig(:company, :owner, :name)

于 2017-09-11T15:22:38.477 回答