2012-10-24 13 views
21

specificationによれば、ハッシュのキーとして使用される文字列は複製され、フリーズされます。他の変更可能なオブジェクトはそのような特別な考慮を持っていないようです。たとえば、配列キーを使用すると、次のことが可能です。ハッシュの文字列キーがフリーズするのはなぜですか?

a = [0] 
h = {a => :a} 
h.keys.first[0] = 1 
h # => {[1] => :a} 
h[[1]] # => nil 
h.rehash 
h[[1]] # => :a 

一方、同様のことを文字列キーで行うことはできません。

s = "a" 
h = {s => :s} 
h.keys.first.upcase! # => RuntimeError: can't modify frozen String 

なぜ文字列は、ハッシュキーに関しては他の可変オブジェクトと異なるように設計されていますか?この仕様が有用になるユースケースはありますか?この仕様には他にどのような影響がありますか?

実際には、文字列に関するこのような特別な指定がないと便利な場合があります。つまり、私はyaml gemで、ハッシュを記述する手動で書かれたYAMLファイルを読んでいます。キーは文字列かもしれません。元のYAMLファイルで大文字と小文字を区別しないようにしたいと思います。私は、ファイルを読んだとき、私はこのようなハッシュを取得する可能性があります:

h = {"foo" => :foo, "Bar" => :bar, "BAZ" => :baz} 

そして、私はこれを取得する場合を下げるために、キーを正常化したい:このような何かを行うことによって

h = {"foo" => :foo, "bar" => :bar, "baz" => :baz} 

を:

h.keys.each(&:downcase!) 

しかし、これは上記の理由でエラーを返します。

+0

私の目的のために、私ができる最高のものは 'h.keys.each {| s | h.store(s.downcase、h.delete(s))} '。 – sawa

+0

私は "なぜ"を推測できますか?文字列は配列よりも一般的なユースケースであるだけでなく、文字列のフリーズは実装が簡単であると思われます。私がPerlを知っていたら、RubyがPerlのハッシュ・ビヘイビアで一貫しているかどうかを見ていきたいと思います。もし私が日本語に堪能だったなら、キーのフリーズが実行されたことを見て、それがバグ報告やメーリングリストの議論の結果であるかどうかを見てみましょう。 –

+1

@AndrewGrimm [Here](http://doc.ruby-lang.org/ja/1.9.2/class/Hash.html)は、配列とハッシュは、変更が可能であるため、ハッシュのための良いキーを作るのではなく、文字列がフリーズされるので、再ハッシュを呼び出す必要はありません。 steenslagの答えと一貫しています。 – sawa

答えて

20

要するに、Rubyだけがうまくいきます。

ハッシュにキーを入力すると、キーのhashメソッドを使用して特別な数値が計算されます。 Hashオブジェクトはこの番号を使用してキーを取得します。たとえば、h['a']の値が何であるかを調べると、Hashはhashメソッドの文字列 'a'を呼び出し、その数値に値が格納されているかどうかを確認します。問題は、誰か(あなた)が文字列オブジェクトを変更したときに発生するので、文字列 'a'は今他のものになりました。 'aa'としましょう。ハッシュは 'aa'のハッシュ番号を見つけることができません。

ハッシュの最も一般的なタイプのキーは、文字列、記号、および整数です。記号と整数は不変ですが、文字列はありません。 Rubyは、文字列キーを使用してフリーズすることによって、上記の混乱する動作からあなたを保護しようとします。私はそれが厄介なパフォーマンスの副作用(大規模な配列を考える)があるかもしれないので、他のタイプのために行われていないと思います。

+0

質問の理論的部分にお返事いただきありがとうございます。 –

4

説明についてはthis thread on the ruby-core mailing listを参照してください(私のメールアプリケーションにメーリングリストを開いたときに私が見つけた最初のメールとなってしまいました!)。

私はあなたの質問の最初の部分については考えてきませんが、ここでは時間は、第二の部分のための実用的な答えである。この種のコードの順列の

new_hash = {} 
    h.each_pair do |k,v| 
    new_hash.merge!({k.downcase => v}) 
    end 

    h.replace new_hash 

ありますたくさんの、

Hash[ h.map{|k,v| [k.downcase, v] } ] 

は別であること(そして、あなたはおそらく、これらの認識しているが、時にはそれが実用的なルートを取るのがベストです:)

+1

ありがとうございます!非常に便利 – Bretticus

2

あなたがアスクンされています2つの異なる質問:理論的かつ実用的。レインは答えることが最初でしたが、私は私があなたの実用的な質問への適切、lazierソリューションを検討するものを提供したいと思います:

Hash.new { |hsh, key| # this block get's called only if a key is absent 
    downcased = key.to_s.downcase 
    unless downcased == key # if downcasing makes a difference 
    hsh[key] = hsh[downcased] if hsh.has_key? downcased # define a new hash pair 
    end # (otherways just return nil) 
} 
Hash.newコンストラクタで使用されるブロックは、実際にあるものを行方不明のキーのために呼び出され

要求された。上記の解決法はシンボルも受け入れます。

3

不変キーは、そのハッシュコードが安定するため一般的に意味があります。文字列はMRIコードのこの部分では、特別に変換される理由

これは:文字列キー場合、一言で言えば

if (RHASH(hash)->ntbl->type == &identhash || rb_obj_class(key) != rb_cString) { 
    st_insert(RHASH(hash)->ntbl, key, val); 
} 
else { 
    st_insert2(RHASH(hash)->ntbl, key, val, copy_str_key); 
} 

st_insert2トリガする関数へのポインタが渡されます。 dupとfreeze。

我々は理論的にはハッシュキーとして不変のリストと不変のハッシュをサポートしたいのであれば、我々はこのような何かにそのコードを修正することができます:

freeze_objのように定義されるだろう
VALUE key_klass; 
key_klass = rb_obj_class(key); 
if (key_klass == rb_cArray || key_klass == rb_cHash) { 
    st_insert2(RHASH(hash)->ntbl, key, val, freeze_obj); 
} 
else if (key_klass == rb_cString) { 
    st_insert2(RHASH(hash)->ntbl, key, val, copy_str_key); 
} 
else { 
    st_insert(RHASH(hash)->ntbl, key, val); 
} 

static st_data_t 
freeze_obj(st_data_t obj) 
{ 
    return (st_data_t)rb_obj_freeze((VALUE) obj); 
} 

これは、アレイキーが変更可能であった特定の不一致を解決します。しかし、実際に一貫するためには、より多くの種類のオブジェクトを不変にする必要があります。

なしすべてタイプです。たとえば、Fixnumのような即時オブジェクトをフリーズする必要はありません。なぜなら、事実上、各整数値に対応するFixnumのインスタンスが1つしかないからです。このため、FixnumSymbolではなく、Stringのみ特殊なケースにする必要があります。

文字列が非常によくハッシュキーとして使用されるため、文字列はRubyプログラマーの便宜のための特別な例外です。

逆に、他のオブジェクトタイプが明らかに矛盾した行動につながる、このようないを凍結していることの理由は、主にエッジケースをサポートしないようにマッツ&会社の都合です。実際には、比較的少数の人々が配列やハッシュのようなコンテナオブジェクトをハッシュキーとして使用します。あなたがそうするならば、挿入する前に凍結するのはあなた次第です。

非即時オブジェクトをフリーズする動作は、すべてのオブジェクトに存在するbasic.flagsビットフィールドのFL_FREEZEビットを反転するだけのため、これはパフォーマンスに関するものではありません。それはもちろん安い操作です。

また、パフォーマンスについては、コードのパフォーマンスに重大な部分にある文字列キーを使用する場合は、挿入を行う前に文字列をフリーズすることをお勧めします。そうしないと、dupがトリガーされ、これはより高価な操作です。

更新 @sawaは、単に凍結残して、あなたの配列、キーは元の配列はまた、(不愉快な驚き可能性があり、キーの使用状況、予想外不変外であるかもしれない意味していることを指摘し、それはあなたを右役立つであろう大藤が、配列をハッシュ・キーとして使用する場合)。したがって、dup + freezeがその抜け道であると推測すれば、実際には顕著なパフォーマンスコストが発生します。三番目の手で、それを完全に凍らせないでおくと、あなたはOPのオリジナルの奇妙さを得る。周りの奇妙さMatzらがこれらのエッジケースをプログラマに遅らせるもう一つの理由。

+1

元のキーを複製せずにフリーズすると、混乱することがあります。鍵が自動的に凍結される場合は、複製が必須となります。凍結が安い場合でも、配列の複製などは高価なので、やはり性能の問題であるようです。あなたの最後の段落は有益です。文字列が最初から凍結されている場合、ハッシュキーとして使用すると文字列は重複しませんか? – sawa

+1

これが動作するかどうかを確かめるために、現在ここにあるrb_str_new_frozen()の先頭に 'if(OBJ_FROZEN(orig))return orig;'と表示されます:github.com/ruby/ ruby/blob/trunk/string.c#L673 – manzoid

+1

ハッシュキーを設定するための一貫した振る舞いがすべて凍っていたら、 "冗長化する必要があります"と必ずしも同意する必要はありません。配列をキーとして使用しようとした後で突然変更すると、更新の試行が大声で失敗したときにその使用法が機能しないことがすぐに分かります。一貫性はおそらく役に立つかもしれません。今、どこから来ているのかはっきりとわかります...一貫性、パフォーマンス、奇妙なことの結果からプログラマーを守るなど、最適化するべきものについては議論の余地があるようです。 – manzoid

関連する問題