Hash varsayılan değeri kullanılırken garip, beklenmeyen davranış (kaybolan / değerlerin değişmesi), örneğin Hash.new ([])


107

Bu kodu düşünün:

h = Hash.new(0)  # New hash pairs will by default have 0 as values
h[1] += 1  #=> {1=>1}
h[2] += 2  #=> {2=>2}

Her şey yolunda, ama:

h = Hash.new([])  # Empty array as default value
h[1] <<= 1  #=> {1=>[1]}                  ← Ok
h[2] <<= 2  #=> {1=>[1,2], 2=>[1,2]}      ← Why did `1` change?
h[3] << 3   #=> {1=>[1,2,3], 2=>[1,2,3]}  ← Where is `3`?

Bu noktada hash'in şöyle olmasını bekliyorum:

{1=>[1], 2=>[2], 3=>[3]}

ama bundan çok uzak. Ne oluyor ve beklediğim davranışı nasıl elde edebilirim?

Yanıtlar:


164

İlk olarak, bu davranışın sadece diziler için değil, sonradan mutasyona uğramış herhangi bir varsayılan değer (örn. Karmalar ve dizeler) için geçerli olduğuna dikkat edin.

TL; DR : Hash.new { |h, k| h[k] = [] }En deyimsel çözümü istiyorsanız ve nedenini umursamıyorsanız kullanın.


Ne çalışmıyor

Neden Hash.new([])çalışmıyor

Neden Hash.new([])işe yaramadığına daha derinlemesine bakalım :

h = Hash.new([])
h[0] << 'a'  #=> ["a"]
h[1] << 'b'  #=> ["a", "b"]
h[1]         #=> ["a", "b"]

h[0].object_id == h[1].object_id  #=> true
h  #=> {}

Varsayılan nesnemizin yeniden kullanıldığını ve değiştirildiğini görebiliriz (bunun nedeni tek ve tek varsayılan değer olarak aktarılmasıdır, hash'in yeni, yeni bir varsayılan değer elde etme yolu yoktur), ancak neden anahtarlar veya değerler yoktur dizide, h[1]bize hala bir değer vermesine rağmen ? İşte bir ipucu:

h[42]  #=> ["a", "b"]

Her []çağrının döndürdüğü dizi sadece varsayılan değerdir, bunca zamandır değiştirdiğimiz ve şimdi yeni değerlerimizi içeriyor. Yana <<karma atamak değildir (bir olmadan Ruby atama söz konusu olamaz =günümüze ), bizim gerçek karma içine bir şey koymak hiç. Bunun yerine kullanılmak zorunda <<=(olmaktır <<olarak +=etmektir +):

h[2] <<= 'c'  #=> ["a", "b", "c"]
h             #=> {2=>["a", "b", "c"]}

Bu şununla aynıdır:

h[2] = (h[2] << 'c')

Neden Hash.new { [] }çalışmıyor

Kullanılması Hash.new { [] }, orijinal varsayılan değeri yeniden kullanma ve değiştirme sorununu çözer (verilen blok her seferinde çağrıldığı için, yeni bir dizi döndürür), ancak atama sorununu çözmez:

h = Hash.new { [] }
h[0] << 'a'   #=> ["a"]
h[1] <<= 'b'  #=> ["b"]
h             #=> {1=>["b"]}

Ne işe yarar

Atama yolu

Biz her zaman kullanmayı unutmayın Eğer <<=, o zaman Hash.new { [] } olduğu uygulanabilir bir çözüm, ama biraz garip ve (hiç görmedim olmayan deyimsel bulunuyor <<=vahşi kullanılır). Ayrıca <<yanlışlıkla kullanılırsa ince hatalara eğilimlidir .

Değişebilir yol

İçin dokümantasyonHash.new devletler (vurgu sahibi Gözat):

Bir blok belirtilirse, hash nesnesi ve anahtarla çağrılır ve varsayılan değeri döndürmelidir. Gerekirse değeri hash'de saklamak bloğun sorumluluğundadır .

Bu nedenle, eğer <<yerine kullanmak istiyorsak, hash'deki varsayılan değeri blok içinden saklamalıyız <<=:

h = Hash.new { |h, k| h[k] = [] }
h[0] << 'a'  #=> ["a"]
h[1] << 'b'  #=> ["b"]
h            #=> {0=>["a"], 1=>["b"]}

Bu, atamayı etkili bir şekilde bireysel çağrılarımızdan (kullanacak olan <<=) geçirilen bloğa taşır ve kullanım Hash.newsırasında beklenmedik davranış yükünü ortadan kaldırır <<.

Bu yöntemle diğerleri arasında bir işlevsel fark olduğuna dikkat edin: bu yol, okuma üzerine varsayılan değeri atar (atama her zaman blok içinde gerçekleştiği için). Örneğin:

h1 = Hash.new { |h, k| h[k] = [] }
h1[:x]
h1  #=> {:x=>[]}

h2 = Hash.new { [] }
h2[:x]
h2  #=> {}

Değişmez yol

Hash.new([])Çalışırken neden işe yaramadığını merak ediyor olabilirsiniz Hash.new(0). Anahtar, Ruby'deki Numerics'in değişmez olmasıdır, bu nedenle doğal olarak onları asla yerinde mutasyona uğratmayız. Varsayılan değerimizi değişmez olarak ele alırsak, biz de Hash.new([])gayet iyi kullanabiliriz :

h = Hash.new([].freeze)
h[0] += ['a']  #=> ["a"]
h[1] += ['b']  #=> ["b"]
h[2]           #=> []
h              #=> {0=>["a"], 1=>["b"]}

Ancak, bunu unutmayın ([].freeze + [].freeze).frozen? == false. Bu nedenle, değişmezliğin baştan sona korunmasını sağlamak istiyorsanız, yeni nesneyi yeniden dondurmaya özen göstermelisiniz.


Sonuç

Tüm yollar arasında, kişisel olarak "değişmez yolu" tercih ederim - değişmezlik, genellikle şeyler hakkında akıl yürütmeyi çok daha basit hale getirir. Sonuçta, gizli veya belirsiz beklenmedik davranış olasılığı olmayan tek yöntemdir. Bununla birlikte, en yaygın ve deyimsel yol "değişebilir yol" dur.

Son olarak, Hash varsayılan değerlerinin bu davranışı Ruby Koans'ta belirtilmiştir .


Bu kesinlikle doğru değildir, bunu instance_variable_setatlama gibi yöntemler , ancak metaprogramlama için mevcut olmalıdır, çünkü içindeki l değeri =dinamik olamaz.


1
"Değişebilir yol" kullanımının aynı zamanda her hash aramasının bir anahtar değer çiftini depolamasına neden olma etkisine sahip olduğunu (blokta gerçekleşen bir atama olduğundan) ve her zaman istenmeyebileceğinden bahsetmek gerekir.
johncip

@johncip Her aramada değil , her tuşa yalnızca ilk arama. Ama ne demek istediğini anlıyorum, bunu cevaba daha sonra ekleyeceğim; Teşekkürler!.
Andrew Marshall

Hay aksi, baştan savma. Haklısın, elbette, bilinmeyen bir anahtarın ilk araması. Neredeyse gibi hissediyorum { [] }ile <<=yanlışlıkla unutarak gerçeği olmasaydı, en az sürprizler vardır =çok kafa karıştırıcı hata ayıklama oturumu yol açabilir.
johncip

farklar hakkında oldukça açık açıklamalar varsayılan değerlerle karma başlatılırken
cisolarix

23

Karma için varsayılan değerin o belirli (başlangıçta boş) diziye bir başvuru olduğunu belirtiyorsunuz.

Sanırım istiyorsun:

h = Hash.new { |hash, key| hash[key] = []; }
h[1]<<=1 
h[2]<<=2 

Bu, her anahtar için varsayılan değeri yeni bir diziye ayarlar .


Her yeni karma için ayrı dizi örneklerini nasıl kullanabilirim?
Valentin Vasilyev

5
Bu blok versiyonu size Arrayher çağrıda yeni örnekler verir . Zekâ için: h = Hash.new { |hash, key| hash[key] = []; puts hash[key].object_id }; h[1] # => 16348490; h[2] # => 16346570. Ayrıca: basitçe value ( ) üreten yerine value ( ) 'yi ayarlayan blok sürümünü kullanırsanız , o zaman sadece eleman eklerken ihtiyacınız yoktur . {|hash,key| hash[key] = []}{ [] }<<<<=
James A. Rosen

3

Operatör +=bu karmalara uygulandığında beklendiği gibi çalışır.

[1] pry(main)> foo = Hash.new( [] )
=> {}
[2] pry(main)> foo[1]+=[1]
=> [1]
[3] pry(main)> foo[2]+=[2]
=> [2]
[4] pry(main)> foo
=> {1=>[1], 2=>[2]}
[5] pry(main)> bar = Hash.new { [] }
=> {}
[6] pry(main)> bar[1]+=[1]
=> [1]
[7] pry(main)> bar[2]+=[2]
=> [2]
[8] pry(main)> bar
=> {1=>[1], 2=>[2]}

Bu olabileceğinden foo[bar]+=baziçin sözdizimsel şeker sağında döndürür değerlendirilir varsayılan değer nesne ve bunu değiştirmek olmaz operatörü. Sol el, varsayılan değeri değiştirmeyecek olan yöntem için sözdizimsel şekerdir .foo[bar]=foo[bar]+bazfoo[bar]=+[]=

Varsayılan değeri değiştireceği ve foo[bar]<<=bazeşdeğer olacağı için bunun geçerli olmadığını unutmayın .foo[bar]=foo[bar]<<baz<<

Ayrıca, Hash.new{[]}ve arasında hiçbir fark bulamadım Hash.new{|hash, key| hash[key]=[];}. En azından yakut 2.1.2.


Güzel açıklama. Ruby 2.1.1'deki gibi görünüyor , beklenen davranış eksikliği ile benim Hash.new{[]}için aynı (elbette işe yarasa da). Her şeyi bozan garip küçük şeyler: /Hash.new([])<<Hash.new{|hash, key| hash[key]=[];}
butterywombat

1

Yazarken

h = Hash.new([])

dizi varsayılan referansını hash'deki tüm öğelere iletirsiniz. çünkü hash'deki tüm elemanlar aynı diziyi ifade eder.

karma içindeki her bir öğenin ayrı bir diziye başvurmasını istiyorsanız, kullanmalısınız

h = Hash.new{[]} 

Ruby'de nasıl çalıştığına dair daha fazla ayrıntı için lütfen şunu gözden geçirin: http://ruby-doc.org/core-2.2.0/Array.html#method-c-new


Bu yanlış, Hash.new { [] } yok değil çalışır. Bkz Cevabımı detayları için. Aynı zamanda başka bir cevapta önerilen çözüm zaten.
Andrew Marshall
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.