如何避免嵌套哈希中缺少元素的 NoMethodError,而不需要重复的 nil 检查?

发布于 2024-10-06 11:51:23 字数 258 浏览 5 评论 0原文

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

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

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

I'm looking for a good way to avoid checking for nil at each level in deeply nested hashes. For example:

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

This requires three checks, and makes for very ugly code. Any way to get around this?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(16

oО清风挽发oО 2024-10-13 11:51:24

危险但有效:

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”链类似的风格。

Dangerous but works:

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

We can new do

   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

The "h_try" chain follows similar style to a "try" chain.

千笙结 2024-10-13 11:51:24

太长了; params&.dig(:company, :owner, :name)

从 Ruby 2.3.0 开始:

您还可以使用称为“安全导航运算符”的 &. 作为:params&.[](:公司)&.[](:所有者)&.[](:名称)。这个是绝对安全的。

params 上使用 dig 实际上并不安全,因为如果 params 为 nil,params.dig 将失败。

不过,您可以将两者组合为:params&.dig(:company, :owner, :name)

因此,以下任一选项都可以安全使用:

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

params&.挖掘(:公司,:所有者,:名称)

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

As of Ruby 2.3.0:

You can also use &. called the "safe navigation operator" as: params&.[](:company)&.[](:owner)&.[](:name). This one is perfectly safe.

Using dig on params is not actually safe as params.dig will fail if params is nil.

However you may combine the two as: params&.dig(:company, :owner, :name).

So either of the following is safe to use:

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

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

め可乐爱微笑 2024-10-13 11:51:24

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

如果链中的对象属于某种本身无法被挖掘的类型,那么挖掘仍然会抛出错误。

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

Just to offer a one-up on dig, try the KeyDial gem which I wrote. This is essentially a wrapper for dig but with the important difference that it will never hit you with an error.

dig will still spit out an error if an object in the chain is of some type that can't itself be diged.

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

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

In this situation dig does not help you, and you need to go back not only to hash[:a][:d].nil? && but also hash[:a][:d].is_a?(Hash) checks. KeyDial lets you do this without such checks or errors:

hash.call(:a, :d, :c) #=> nil
hash.call(:a, :b, :c) #=> true
枫林﹌晚霞¤ 2024-10-13 11:51:23

Ruby 2.3.0 引入了名为 dig 的方法HashArray 上。

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

如果任何级别的密钥丢失,它都会返回nil

如果您使用的 Ruby 版本早于 2.3,则可以安装 ruby​​_dig 或 hash_dig_and_collect 等 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

Ruby 2.3.0 introduced a method called dig on both Hash and Array.

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

It returns nil if the key is missing at any level.

If you are using a version of Ruby older than 2.3, you can install a gem such as ruby_dig or hash_dig_and_collect, or implement the functionality yourself:

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
眼眸里的快感 2024-10-13 11:51:23

在我看来,功能性和清晰度之间的最佳折衷方案是 Raganwald 的 andand。这样,您就可以这样做:

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

它与 try 类似,但在这种情况下读起来更好,因为您仍然像平常一样发送消息,但之间有一个分隔符,以引起人们注意这样一个事实:我们正在特别对待尼尔斯。

The best compromise between functionality and clarity IMO is Raganwald's andand. With that, you would do:

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

It's similar to try, but reads a lot better in this case since you're still sending messages like normal, but with a delimiter between that calls attention to the fact that you're treating nils specially.

忆悲凉 2024-10-13 11:51:23

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

name = params[:company][:owner][:name] rescue nil

I don't know if that's what you want, but maybe you could do this?

name = params[:company][:owner][:name] rescue nil
怂人 2024-10-13 11:51:23

您可能想研究将 auto-vivification 添加到 ruby​​ 哈希的方法之一。以下 stackoverflow 线程中提到了多种方法:

You may want to look into one of the ways to add auto-vivification to ruby hashes. There are a number of approaches mentioned in the following stackoverflow threads:

不离久伴 2024-10-13 11:51:23

相当于用户 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

Equivalent to the second solution that user mpd suggested, only more idiomatic 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
暮年 2024-10-13 11:51:23

更新:这个答案已经过时了。按照当前接受的答案建议使用 dig

如果是Rails,请使用

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

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

Update: This answer is way out-of-date. Use dig as the current accepted answer suggests.

If it's Rails, use

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

Oh wait, that's even uglier. ;-)

橘香 2024-10-13 11:51:23

如果你想进入猴子补丁,你可以这样做
<代码>

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)

If you wanna get into monkeypatching you could do something like this

class NilClass
  def [](anything)
    nil
  end
end

Then a call to params[:company][:owner][:name] will yield nil if at any point one of the nested hashes is nil.

EDIT:
If you want a safer route that also provides clean code you could do something like

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

The code would look like this: params.chain(:company, :owner, :name)

习ぎ惯性依靠 2024-10-13 11:51:23

我会这样写:

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

它不像 那样干净? Io 中的运算符,但 Ruby 没有。 @ThiagoSilveira 的答案也很好,尽管会慢一些。

I would write this as:

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

It's not as clean as the ? operator in Io, but Ruby doesn't have that. The answer by @ThiagoSilveira is also good, though it will be slower.

玩世 2024-10-13 11:51:23

您是否能够避免使用多维哈希并使用

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

or

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

代替?

Are you able to avoid using a multi-dimensional hash, and use

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

or

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

instead?

灯角 2024-10-13 11:51:23

把丑陋写一次,然后隐藏起来

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

Write the ugliness once, then hide it

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
御弟哥哥 2024-10-13 11:51:23

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

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

begin
    name = params[:company][:owner][:name]
rescue
    #if it raises errors maybe:
    name = 'John Doe'
end

(Even though it's a really old question maybe this answer will be useful for some stackoverflow people like me that did not think of the "begin rescue" control structure expression.)

I would do it with a try catch statement (begin rescue in ruby language):

begin
    name = params[:company][:owner][:name]
rescue
    #if it raises errors maybe:
    name = 'John Doe'
end
马蹄踏│碎落叶 2024-10-13 11:51:23

做:

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']

Do:

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

Also at each step, you can use an appropriate method built in NilClass to escape from nil, if it were array, string, or numeric. Just add to_hash to the inventory of this list and use it.

class NilClass; def to_hash; {} end end
params['company'].to_hash['owner'].to_hash['name']
-黛色若梦 2024-10-13 11:51:23

您不需要访问原始哈希定义 - 您可以在使用 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 中所知道和喜欢的真正的自动激活。

You don't need access to the original hash definition -- you can override the [] method on the fly after you get it using h.instance_eval, e.g.

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
}

But that's not going to help you with the code you have, because you're relying on an unfound value to return a false value (e.g., nil) and if you do any of the "normal" auto-vivification stuff linked to above you're going to end up with an empty hash for unfound values, which evaluates as "true".

You could do something like this -- it only checks for defined values and returns them. You can't set them this way, because we've got no way of knowing if the call is on the LHS of an assignment.

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

Again, though, you can only check values with it -- not assign them. So it's not real auto-vivification as we all know and love it in Perl.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文