Ruby-Access 多维散列并避免访问 nil 对象

可能的复制品:
Ruby: IF 语句中的 Nils
有没有一种干净的方法可以避免在嵌套的 params hash 中对 nil 调用方法?

假设我试图访问这样的 hash:

my_hash['key1']['key2']['key3']

如果 key1、 key2和 key3存在于 hash (es)中,这很好,但是如果,例如 key1不存在呢?

然后我会得到 NoMethodError: undefined method [] for nil:NilClass。没有人喜欢这样。

到目前为止,我是这样处理这个条件的:

if my_hash['key1'] && my_hash['key1']['key2']...

这样做合适吗? 还有其他鲁比的方法吗?

31760 次浏览

Conditions my_hash['key1'] && my_hash['key1']['key2'] don't feel DRY.

Alternatives:

1) autovivification magic. From that post:

def autovivifying_hash
Hash.new {|ht,k| ht[k] = autovivifying_hash}
end

Then, with your example:

my_hash = autovivifying_hash
my_hash['key1']['key2']['key3']

It's similar to the Hash.fetch approach in that both operate with new hashes as default values, but this moves details to the creation time. Admittedly, this is a bit of cheating: it will never return 'nil' just an empty hash, which is created on the fly. Depending on your use case, this could be wasteful.

2) Abstract away the data structure with its lookup mechanism, and handle the non-found case behind the scenes. A simplistic example:

def lookup(model, key, *rest)
v = model[key]
if rest.empty?
v
else
v && lookup(v, *rest)
end
end
#####


lookup(my_hash, 'key1', 'key2', 'key3')
=> nil or value

3) If you feel monadic you can take a look at this, Maybe

You could also use Object#andand.

my_hash['key1'].andand['key2'].andand['key3']

There are many approaches to this.

If you use Ruby 2.3 or above, you can use dig

my_hash.dig('key1', 'key2', 'key3')

Plenty of folks stick to plain ruby and chain the && guard tests.

You could use stdlib Hash#fetch too:

my_hash.fetch('key1', {}).fetch('key2', {}).fetch('key3', nil)

Some like chaining ActiveSupport's #try method.

my_hash.try(:[], 'key1').try(:[], 'key2').try(:[], 'key3')

Others use andand

myhash['key1'].andand['key2'].andand['key3']

Some people think egocentric nils are a good idea (though someone might hunt you down and torture you if they found you do this).

class NilClass
def method_missing(*args); nil; end
end


my_hash['key1']['key2']['key3']

You could use Enumerable#reduce (or alias inject).

['key1','key2','key3'].reduce(my_hash) {|m,k| m && m[k] }

Or perhaps extend Hash or just your target hash object with a nested lookup method

module NestedHashLookup
def nest *keys
keys.reduce(self) {|m,k| m && m[k] }
end
end


my_hash.extend(NestedHashLookup)
my_hash.nest 'key1', 'key2', 'key3'

Oh, and how could we forget the maybe monad?

Maybe.new(my_hash)['key1']['key2']['key3']