Nested hash defined?() [duplicate]

2020-07-18 10:57发布

问题:

What's the most concise way to determine if @hash[:key1][:key2] is defined, that does not throw an error if @hash or @hash[:key1] are nil?

defined?(@hash[:key1][:key2]) returns True if @hash[:key1] exists (it does not determine whether :key2 is defined)

回答1:

When using ActiveSupport (Rails) or Backports, you can use try:

@hash[:key1].try(:fetch, :key2)

You could even handle @hash being nil:

@hash.try(:fetch, :key1).try(:fetch, :key2)

If you want @hash to always return a hash for a missing key:

@hash = Hash.new { |h,k| h[k] = {} }
@hash[:foo] # => {}

You could also define this recursive:

def recursive_hash
  Hash.new { |h,k| h[k] = recursive_hash }
end

@hash = recursive_hash
@hash[:foo][:bar][:blah] = 10
@hash # => {:foo => {:bar => {:blah => 10}}}

But to answer your question:

module HasNestedKey
  Hash.send(:include, self)
  def has_nested_key?(*args)
    return false unless sub = self[args.shift]
    return true if args.empty?
    sub.respond_to?(:has_nested_key?) and sub.has_nested_key?(*args)
  end
end

@hash.has_nested_key? :key1, :key2


回答2:

Perhaps I am missing something, but if all you care about is concise...why not:

@hash && @hash[:key1] && @hash[:key1][:key2]

or if you want to save a few characters

@hash && (h = @hash[:key1]) && h[:key2]

if any part of this fails, it returns nil otherwise it returns the value associated with :key2 or true.

The reason the defined? returns true even if :key2 is not there is because it just checks whether the object you are referencing exists, which in that case is the method [] which is an alias for the method fetch which does exist on the hash @hash[:key1] but if that were to return nil, there is no fetch method on nil and it would return nil. That being said, if you had to go n deep into an embedded hash, at some point it would become more efficient to call:

defined?(@hash[:key1][:key2][:key3]) && @hash[:key1][:key2][:key3]


回答3:

Using Hash#fetch

You can use the Hash#fetch method with a default of {} so that it is safe to call has_key? even if the first level key doesn't exist. e.g.

!hash.nil? && hash.fetch(key1, {}).has_key?(key2)

Alternative

Alternatively you can use the conditional operator e.g.

!hash.nil? && (hash.has_key?(key1) ? hash[key1].has_key?(key2) : false)

i.e. if hash doesn't have key key1 then just return false without looking for the second level key. If it does have key1 then return the result of checking key1's value for key2.

Also, if you want to check that hash[key1]'s value has a has_key? method before calling it:

!hash.nil? && (hash.has_key?(key1) ? hash[key1].respond_to?(:has_key?) &&
   hash[key1].has_key?(key2) : false)


回答4:

@hash[:key1].has_key? :key2


回答5:

If you don't care about distinguishing nonexistent @hash[:key1][:key2] (at any of 3 levels) from @hash[:key1][:key2] == nil, this is quite clean and works for any depth:

[:key1,:key2].inject(hash){|h,k| h && h[k]}

If you want nil to be treated as existing, use this instead:

(hash[:key1].has_key?(:key2) rescue false)


回答6:

Another option, one that I just discovered, is to extend Hash with a seek method. Technique comes from Corey O'Daniel.

Stick this in an initializer:

class Hash
  def seek(*_keys_)
    last_level    = self
    sought_value  = nil

    _keys_.each_with_index do |_key_, _idx_|
      if last_level.is_a?(Hash) && last_level.has_key?(_key_)
        if _idx_ + 1 == _keys_.length
          sought_value = last_level[_key_]
        else                   
          last_level = last_level[_key_]
        end
      else 
        break
      end
    end

    sought_value
  end 
end

Then just call:

@key_i_need = @hash.seek :one, :two, :three

You'll get the value, or nil if it doesn't exist.