私はRubyで書かれた小さなユーティリティを使っています。これはネストされたハッシュを広範囲に使用します。現在、ネストされたハッシュ要素へのアクセスを次のようにチェックしています。
structure = { :a => { :b => 'foo' }}
# I want structure[:a][:b]
value = nil
if structure.has_key?(:a) && structure[:a].has_key?(:b) then
value = structure[:a][:b]
end
これを行うためのより良い方法はありますか?言いたいことがあります:
value = structure[:a][:b]
:aがnil
などのキーでない場合、structure
を取得します。
最近私がこれを行う方法は次のとおりです。
h = Hash.new { |h,k| h[k] = {} }
これにより、不足しているキーのエントリとして新しいハッシュを作成するハッシュが得られますが、キーの第2レベルではnilが返されます。
h['foo'] -> {}
h['foo']['bar'] -> nil
これをネストして、この方法で対処できる複数のレイヤーを追加できます。
h = Hash.new { |h, k| h[k] = Hash.new { |hh, kk| hh[kk] = {} } }
h['bar'] -> {}
h['tar']['zar'] -> {}
h['scar']['far']['mar'] -> nil
default_proc
メソッドを使用して無期限にチェーンすることもできます。
h = Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }
h['bar'] -> {}
h['tar']['star']['par'] -> {}
上記のコードは、デフォルトプロシージャが同じデフォルトプロシージャで新しいハッシュを作成するハッシュを作成します。そのため、不可視のキーの検索が発生したときにデフォルト値として作成されたハッシュは、同じデフォルトの動作になります。
編集:詳細
Rubyハッシュを使用すると、新しいキーの検索が発生したときのデフォルト値の作成方法を制御できます。指定した場合、この動作はProc
オブジェクトとしてカプセル化され、 default_proc
および default_proc=
メソッドを介して到達可能です。デフォルトのprocは、ブロックを Hash.new
に渡すことでも指定できます。
このコードを少し分解してみましょう。これは慣用的なRubyではありませんが、複数行に分けるのは簡単です:
1. recursive_hash = Hash.new do |h, k|
2. h[k] = Hash.new(&h.default_proc)
3. end
行1は、変数recursive_hash
を新しいHash
として宣言し、ブロックを開始してrecursive_hash
のdefault_proc
にします。ブロックには2つのオブジェクトが渡されます。h
(キールックアップが実行されるHash
インスタンス)と、k
(ルックアップされるキー)です。
行2は、ハッシュのデフォルト値を新しいHash
インスタンスに設定します。このハッシュのデフォルトの動作は、ルックアップが発生しているハッシュのdefault_proc
から作成されたProc
を渡すことで提供されます。すなわち、ブロック自体が定義しているデフォルトのプロシージャ。
IRBセッションの例を次に示します。
irb(main):011:0> recursive_hash = Hash.new do |h,k|
irb(main):012:1* h[k] = Hash.new(&h.default_proc)
irb(main):013:1> end
=> {}
irb(main):014:0> recursive_hash[:foo]
=> {}
irb(main):015:0> recursive_hash
=> {:foo=>{}}
recursive_hash[:foo]
のハッシュが作成されると、default_proc
はrecursive_hash
のdefault_proc
によって提供されました。これには2つの効果があります。
recursive_hash[:foo]
のデフォルトの動作は、recursive_hash
と同じです。recursive_hash[:foo]
のdefault_proc
によって作成されたハッシュのデフォルトの動作は、recursive_hash
と同じです。したがって、IRBを継続すると、次の結果が得られます。
irb(main):016:0> recursive_hash[:foo][:bar]
=> {}
irb(main):017:0> recursive_hash
=> {:foo=>{:bar=>{}}}
irb(main):018:0> recursive_hash[:foo][:bar][:zap]
=> {}
irb(main):019:0> recursive_hash
=> {:foo=>{:bar=>{:zap=>{}}}}
Ruby 2.3.0では、この問題を完全に解決するDig
とHash
の両方に Array
と呼ばれる新しいメソッド が導入されました。
value = structure.Dig(:a, :b)
いずれかのレベルでキーが欠落している場合、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
最も読みやすい解決策の1つは Hashie を使用していると思います:
require 'hashie'
myhash = Hashie::Mash.new({foo: {bar: "blah" }})
myhash.foo.bar
=> "blah"
myhash.foo?
=> true
# use "underscore dot" for multi-level testing
myhash.foo_.bar?
=> true
myhash.foo_.huh_.what?
=> false
value = structure[:a][:b] rescue nil
ソリューション1
前に私の質問でこれを提案しました:
class NilClass; def to_hash; {} end end
Hash#to_hash
はすでに定義されており、自己を返します。その後、次のことができます。
value = structure[:a].to_hash[:b]
to_hash
は、前のキー検索が失敗したときに空のハッシュを取得することを保証します。
Solution2
この解決策は、サブクラスを使用するという点でmuの精神に似ていますが、それでも多少異なります。特定のキーに値がない場合、デフォルト値を使用せず、空のハッシュの値を作成するため、DigitalRossの回答が指摘しているように、混乱の問題を抱えていませんmuが短すぎます。
class NilFreeHash < Hash
def [] key; key?(key) ? super(key) : self[key] = NilFreeHash.new end
end
structure = NilFreeHash.new
structure[:a][:b] = 3
p strucrture[:a][:b] # => 3
ただし、質問で指定された仕様からは逸脱しています。未定義のキーが指定されると、nil
の空のハッシュインスタンスが返されます。
p structure[:c] # => {}
このNilFreeHashのインスタンスを最初から構築し、Key-Valueを割り当てると機能しますが、ハッシュをこのクラスのインスタンスに変換する場合は問題になる可能性があります。
require 'xkeys'
structure = {}.extend XKeys::Hash
structure[:a, :b] # nil
structure[:a, :b, :else => 0] # 0 (contextual default)
structure[:a] # nil, even after above
structure[:a, :b] = 'foo'
structure[:a, :b] # foo
このHash用のモンキーパッチ関数は(少なくとも私にとっては)最も簡単なはずです。また、構造を変更しません。つまり、nil
を{}
に変更します。生のソースからツリーを読んでいる場合でも、それはまだ適用されます。 JSON。また、空のハッシュオブジェクトを生成したり、文字列を解析したりする必要もありません。 rescue nil
は、このような低リスクに対して十分な勇気を持っているので、実際には私にとって良い簡単な解決策でしたが、本質的にパフォーマンスに欠点があることがわかりました。
class ::Hash
def recurse(*keys)
v = self[keys.shift]
while keys.length > 0
return nil if not v.is_a? Hash
v = v[keys.shift]
end
v
end
end
例:
> structure = { :a => { :b => 'foo' }}
=> {:a=>{:b=>"foo"}}
> structure.recurse(:a, :b)
=> "foo"
> structure.recurse(:a, :x)
=> nil
また、保存された配列を使用して再生できることも優れています。
> keys = [:a, :b]
=> [:a, :b]
> structure.recurse(*keys)
=> "foo"
> structure.recurse(*keys, :x1, :x2)
=> nil
途中で適切なチェックを行って掘り下げるための特別な可変メソッドを備えたHashサブクラスを構築することができます。このようなもの(もちろん、より良い名前で):
class Thing < Hash
def find(*path)
path.inject(self) { |h, x| return nil if(!h.is_a?(Thing) || h[x].nil?); h[x] }
end
end
次に、ハッシュの代わりにThing
sを使用します。
>> x = Thing.new
=> {}
>> x[:a] = Thing.new
=> {}
>> x[:a][:b] = 'k'
=> "k"
>> x.find(:a)
=> {:b=>"k"}
>> x.find(:a, :b)
=> "k"
>> x.find(:a, :b, :c)
=> nil
>> x.find(:a, :c, :d)
=> nil
私は現在これを試しています:
# --------------------------------------------------------------------
# System so that we chain methods together without worrying about nil
# values (a la Objective-c).
# Example:
# params[:foo].try?[:bar]
#
class Object
# Returns self, unless NilClass (see below)
def try?
self
end
end
class NilClass
class MethodMissingSink
include Singleton
def method_missing(meth, *args, &block)
end
end
def try?
MethodMissingSink.instance
end
end
私はtry
に対する引数を知っていますが、たとえばparams
のようなものを調べるときに役立ちます。
私の場合、各セルがアイテムのリストである2次元マトリックスが必要でした。
私はこのテクニックを見つけました。 OPで機能する場合があります。
$all = Hash.new()
def $all.[](k)
v = fetch(k, nil)
return v if v
h = Hash.new()
def h.[](k2)
v = fetch(k2, nil)
return v if v
list = Array.new()
store(k2, list)
return list
end
store(k, h)
return h
end
$all['g1-a']['g2-a'] << '1'
$all['g1-a']['g2-a'] << '2'
$all['g1-a']['g2-a'] << '3'
$all['g1-a']['g2-b'] << '4'
$all['g1-b']['g2-a'] << '5'
$all['g1-b']['g2-c'] << '6'
$all.keys.each do |group1|
$all[group1].keys.each do |group2|
$all[group1][group2].each do |item|
puts "#{group1} #{group2} #{item}"
end
end
end
出力は次のとおりです。
$ Ruby -v && Ruby t.rb
Ruby 1.9.2p0 (2010-08-18 revision 29036) [x86_64-linux]
g1-a g2-a 1
g1-a g2-a 2
g1-a g2-a 3
g1-a g2-b 4
g1-b g2-a 5
g1-b g2-c 6
andand gemを使用できますが、次第に注意を払っています。
>> structure = { :a => { :b => 'foo' }} #=> {:a=>{:b=>"foo"}}
>> require 'andand' #=> true
>> structure[:a].andand[:b] #=> "foo"
>> structure[:c].andand[:b] #=> nil
これを行うにはかわいいが間違った方法があります。 NilClass
を返す[]
メソッドを追加するには、nil
をモンキーパッチします。他のソフトウェアが別のバージョンを作成した可能性があるか、またはRubyの将来のバージョンでどの動作が変更される可能性があるかわからないため、これは間違ったアプローチです。
より良いアプローチは、nil
に非常によく似ているが、この動作をサポートする新しいオブジェクトを作成することです。この新しいオブジェクトをハッシュのデフォルトの戻り値にします。そして、それはちょうど動作します。
または、ハッシュとキーを渡す単純な「ネストされたルックアップ」関数を作成し、ハッシュを順番にトラバースし、可能な場合にブレークアウトすることができます。
個人的には、後者の2つのアプローチのいずれかを好みます。最初のものがRuby言語に統合されていればかわいいと思います。しかし、モンキーパッチは悪い考えです。それをしないでください。あなたは。)
私がそれをするというわけではありませんが、NilClass#[]
:
> structure = { :a => { :b => 'foo' }}
#=> {:a=>{:b=>"foo"}}
> structure[:x][:y]
NoMethodError: undefined method `[]' for nil:NilClass
from (irb):2
from C:/Ruby/bin/irb:12:in `<main>'
> class NilClass; def [](*a); end; end
#=> nil
> structure[:x][:y]
#=> nil
> structure[:a][:y]
#=> nil
> structure[:a][:b]
#=> "foo"
@DigitalRossの回答をご覧ください。はい、タイピングはより多くなりますが、それはより安全だからです。