如何避免嵌套散列中缺少元素的 NoMethodError,而无需重复的 nil 检查?
Posted
技术标签:
【中文标题】如何避免嵌套散列中缺少元素的 NoMethodError,而无需重复的 nil 检查?【英文标题】:How to avoid NoMethodError for missing elements in nested hashes, without repeated nil checks? 【发布时间】:2011-05-21 06:42:39 【问题描述】:我正在寻找一种避免在深度嵌套的散列中检查每个级别的nil
的好方法。例如:
name = params[:company][:owner][:name] if params[:company] && params[:company][:owner] && params[:company][:owner][:name]
这需要三个检查,并且会产生非常丑陋的代码。有什么办法可以解决这个问题?
【问题讨论】:
在 groovy 中,您将使用?
运算符。实际上,我对等效的运算符感兴趣。您仍然可以扩展哈希类并添加运算符。
@Pasta Io 有类似的运算符,但 Ruby 没有。
【参考方案1】:
Ruby 2.3.0 在Hash
和Array
上都引入了a new method called dig
,完全解决了这个问题。
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
【讨论】:
如果params
是nil
,params.dig
将失败。考虑改用安全导航运算符或与.dig
结合使用:params&.dig(:company, :owner, :name)
或params&.company&.owner&.name
。
我之前的评论中安全导航运算符的哈希值语法不正确。正确的语法是:params&.[](:company)&.[](:owner)&.[](:name)
。【参考方案2】:
功能性和清晰度之间的最佳折衷 IMO 是 Raganwald 的 andand
。有了它,你会这样做:
params[:company].andand[:owner].andand[:name]
它类似于try
,但在这种情况下读起来要好得多,因为你仍然像往常一样发送消息,但在它之间有一个分隔符,提醒你注意你正在特别对待 nil 的事实。
【讨论】:
+1:我本来打算推荐可能的 Ick(也来自 Raganwald),这是同样的想法,你也可以在答案中包含一个链接:ick.rubyforge.org IMOandand
在语法上很恶心
@mpd:为什么?在概念上还是您只是不喜欢那个特定的词?
@chuck 我喜欢这个概念,但它看起来很不优雅。如果您不知道它的用途,这也会令人困惑,我的意思是 andand
只是没有意义(我理解对 &&
的引用)。我不认为它用它的名字正确地传达了它的含义。话虽如此,我喜欢它胜过try
【参考方案3】:
我不知道这是否是你想要的,但也许你可以这样做?
name = params[:company][:owner][:name] rescue nil
【讨论】:
很抱歉,但乱抢救是邪恶的,你可以掩盖这么多不相关的错误...... 是的,EEEEeeevil 带有大写字母“E”。 由于这里唯一发生的事情是带有符号的哈希查找,在我看来,这就像一个非常有区别的救援,而这正是我所做的。 您可以选择要捕获的异常,如下所示:***.com/questions/6224875/… @glennmcdonald 这段代码无法确保params
是一个哈希值。 rescue nil
仍然不行。这里发布了更好、更轻松的解决方案。没有理由冒险并尝试对此保持聪明。【参考方案4】:
您可能想研究一种将auto-vivification 添加到ruby 哈希的方法。以下***线程中提到了多种方法:
Ruby Autovivification ruby hash autovivification (facets)【讨论】:
谢谢斯蒂芬。我以前从未听说过 auto-vivification,但如果我定义散列就完美了。感谢您的回答! 如何编辑您的答案并使链接更加明显。很难说最后两个指向什么。【参考方案5】:相当于用户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
【讨论】:
这里有个稍微改进的方法:***.com/questions/6224875/… 这里有一个比“稍微改进”的方法稍有改进的方法:***.com/a/27498050/199685【参考方案6】:如果是轨道,使用
params.try(:[], :company).try(:[], :owner).try(:[], :name)
哦等等,那更难看。 ;-)
【讨论】:
我不会说它更丑。感谢凯尔的回复。【参考方案7】:如果你想进入猴子补丁,你可以做这样的事情
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)
【讨论】:
我喜欢这个解决方案,因为它很聪明,而且代码非常干净。但是男孩,这对我来说确实很危险。您永远不会知道整个应用程序中的数组是否实际上是 nil。 是的,这是这种方法的一个很大的缺点。但是,可以在方法定义中执行一些其他技巧,以在发生这种情况时向您发出警告。这实际上只是一个想法,可以根据程序员的需求量身定制。 这行得通,但有点危险,因为你正在猴子修补 Ruby 的一个非常基本的部分,以完全不同的方式工作。 是的,我还是很怕猴子补丁!【参考方案8】:我会这样写:
name = params[:company] && params[:company][:owner] && params[:company][:owner][:name]
它不如? operator in Io 干净,但Ruby 没有。 @ThiagoSilveira 的回答也不错,虽然会慢一些。
【讨论】:
【参考方案9】:你能避免使用多维散列,并使用
params[[:company, :owner, :name]]
或
params[[:company, :owner, :name]] if params.has_key?([:company, :owner, :name])
改为?
【讨论】:
感谢安德鲁的回复。我无法避免多维散列(不幸的是),因为散列是从外部库传递的。【参考方案10】:把丑写一次,然后隐藏
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
【讨论】:
如果返回值是链中的最后一项,我认为这对于 OP(和常见)需求可能会更好,更有用。【参考方案11】:(尽管这是一个非常古老的问题,但也许这个答案对于像我这样没有想到“开始救援”控制结构表达式的一些 *** 人来说很有用。)
我会使用 try catch 语句(开始用 ruby 语言进行救援):
begin
name = params[:company][:owner][:name]
rescue
#if it raises errors maybe:
name = 'John Doe'
end
【讨论】:
如果我打错了 name = parms[:company][:owner][:name] 怎么办?代码很乐意与“John Doe”一起使用,而我可能永远不会注意到。 没错,在救援案例中应该为零,因为这就是问题所使用的。我现在看到蒂亚戈·西尔维拉的回答正是我的想法,但更优雅。【参考方案12】:做:
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']
【讨论】:
【参考方案13】:您不需要访问原始哈希定义——您可以在使用 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 中所知道和喜爱的那样。
【讨论】:
【参考方案14】:危险但有效:
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”链相似的风格。
【讨论】:
【参考方案15】:TLDR; params&.dig(:company, :owner, :name)
从 Ruby 2.3.0 开始:
您也可以将&.
称为“安全导航运算符”,如:params&.[](:company)&.[](:owner)&.[](:name)
。这个绝对安全。
在params
上使用dig
实际上并不安全,因为如果params
为零,params.dig
将失败。
但是,您可以将两者组合为:params&.dig(:company, :owner, :name)
。
所以以下任何一种都可以安全使用:
params&.[](:company)&.[](:owner)&.[](:name)
params&.dig(:company, :owner, :name)
【讨论】:
【参考方案16】:只是为了在dig
上提供一个单,试试我写的KeyDial gem。这本质上是dig
的包装器,但重要的区别是它永远不会出错。
如果链中的对象属于某种本身不能是dig
ed 的类型,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
【讨论】:
以上是关于如何避免嵌套散列中缺少元素的 NoMethodError,而无需重复的 nil 检查?的主要内容,如果未能解决你的问题,请参考以下文章