Perilaku aneh dan tidak terduga (nilai menghilang / berubah) saat menggunakan nilai default Hash, misalnya Hash.new ([])

107

Pertimbangkan kode ini:

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

Tidak apa-apa, tapi:

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`?

Pada titik ini saya mengharapkan hash menjadi:

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

tapi jauh dari itu. Apa yang terjadi dan bagaimana saya bisa mendapatkan perilaku yang saya harapkan?

Valentin Vasilyev
sumber

Jawaban:

164

Pertama, perhatikan bahwa perilaku ini berlaku untuk nilai default apa pun yang kemudian dimutasi (mis. Hash dan string), bukan hanya array.

TL; DR : Gunakan Hash.new { |h, k| h[k] = [] }jika Anda menginginkan solusi yang paling idiomatis dan tidak peduli mengapa.


Apa yang tidak berhasil

Mengapa Hash.new([])tidak berhasil

Mari kita lihat lebih dalam mengapa Hash.new([])tidak berhasil:

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  #=> {}

Kita dapat melihat bahwa objek default kita sedang digunakan kembali dan dimutasi (ini karena ia diteruskan sebagai satu-satunya nilai default, hash tidak memiliki cara untuk mendapatkan nilai default baru yang segar), tetapi mengapa tidak ada kunci atau nilai dalam array, meski h[1]masih memberi kita nilai? Berikut petunjuknya:

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

Array yang dikembalikan oleh setiap []panggilan hanyalah nilai default, yang telah kita mutasi selama ini jadi sekarang berisi nilai baru kita. Karena <<tidak menetapkan hash (tidak akan pernah ada tugas di Ruby tanpa =hadiah ), kami tidak pernah memasukkan apa pun ke dalam hash kami yang sebenarnya. Sebagai gantinya kita harus menggunakan <<=(yaitu <<apa +=adanya +):

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

Ini sama dengan:

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

Mengapa Hash.new { [] }tidak berhasil

Menggunakan Hash.new { [] }memecahkan masalah penggunaan kembali dan mutasi nilai default asli (karena blok yang diberikan dipanggil setiap kali, mengembalikan array baru), tetapi bukan masalah penugasan:

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

Apa yang berhasil

Cara penugasan

Jika kita ingat untuk selalu menggunakan <<=, maka itu Hash.new { [] } adalah solusi yang layak, tetapi ini agak aneh dan non-idiomatik (saya belum pernah melihat <<=digunakan di alam liar). Ini juga rentan terhadap bug halus jika <<digunakan secara tidak sengaja.

Cara yang bisa berubah

The dokumentasi untukHash.new negara (penekanan saya sendiri):

Jika sebuah blok ditentukan, itu akan dipanggil dengan objek hash dan kunci, dan harus mengembalikan nilai default. Merupakan tanggung jawab blok untuk menyimpan nilai dalam hash jika diperlukan .

Jadi kita harus menyimpan nilai default di hash dari dalam blok jika kita ingin menggunakan <<alih-alih <<=:

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

Ini secara efektif memindahkan tugas dari panggilan individu kami (yang akan digunakan <<=) ke blok yang diteruskan ke Hash.new, menghilangkan beban perilaku tak terduga saat menggunakan <<.

Perhatikan bahwa ada satu perbedaan fungsional antara metode ini dan yang lain: cara ini menetapkan nilai default saat membaca (karena penugasan selalu terjadi di dalam blok). Sebagai contoh:

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

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

Cara yang tidak bisa diubah

Anda mungkin bertanya-tanya mengapa Hash.new([])tidak berhasil sementara Hash.new(0)berfungsi dengan baik. Kuncinya adalah bahwa Numerik di Ruby tidak dapat diubah, jadi secara alami kami tidak akan pernah memutasinya di tempat. Jika kami memperlakukan nilai default kami sebagai tidak dapat diubah, kami juga dapat menggunakan dengan Hash.new([])baik:

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

Namun, perhatikan itu ([].freeze + [].freeze).frozen? == false. Jadi, jika Anda ingin memastikan bahwa keabadian dipertahankan seluruhnya, Anda harus berhati-hati untuk membekukan kembali objek baru tersebut.


Kesimpulan

Dari semua cara, saya pribadi lebih suka "cara yang kekal" —mengubah secara umum membuat penalaran tentang hal-hal menjadi lebih sederhana. Bagaimanapun, ini adalah satu-satunya metode yang tidak memiliki kemungkinan perilaku tak terduga yang tersembunyi atau halus. Namun, cara yang paling umum dan idiomatis adalah "cara yang bisa berubah".

Sebagai penutup terakhir, perilaku nilai default Hash ini dicatat di Ruby Koans .


Ini tidak sepenuhnya benar, metode seperti instance_variable_setmelewati ini, tetapi mereka harus ada untuk metaprogramming karena nilai-l di =tidak bisa dinamis.

Andrew Marshall
sumber
1
Perlu disebutkan bahwa menggunakan "cara yang bisa berubah" juga memiliki efek menyebabkan setiap pencarian hash menyimpan pasangan nilai kunci (karena ada tugas yang terjadi di dalam blok), yang mungkin tidak selalu diinginkan.
johncip
@johncip Tidak setiap pencarian, hanya pencarian pertama untuk setiap kunci. Tapi saya mengerti maksud Anda, saya akan menambahkannya ke jawabannya nanti; Terima kasih!.
Andrew Marshall
Ups, ceroboh. Anda benar, tentu saja, ini pencarian pertama dari kunci yang tidak diketahui. Saya hampir merasa { [] }with <<=memiliki kejutan paling sedikit, bukan karena fakta bahwa secara tidak sengaja melupakannya =dapat menyebabkan sesi debugging yang sangat membingungkan.
johncip
penjelasan yang cukup jelas tentang perbedaan saat menginisialisasi hash dengan nilai default
cisolarix
23

Anda menentukan bahwa nilai default untuk hash adalah referensi ke larik tertentu (awalnya kosong).

Saya pikir Anda ingin:

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

Itu menetapkan nilai default untuk setiap kunci ke array baru .

Matthew Flaschen
sumber
Bagaimana saya bisa menggunakan instance array terpisah untuk setiap hash baru?
Valentin Vasilyev
5
Versi blok itu memberi Anda Arraycontoh baru di setiap pemanggilan. Untuk wit: h = Hash.new { |hash, key| hash[key] = []; puts hash[key].object_id }; h[1] # => 16348490; h[2] # => 16346570. Juga: jika Anda menggunakan versi blok yang menetapkan nilai ( {|hash,key| hash[key] = []}) daripada yang hanya menghasilkan nilai ( { [] }), maka Anda hanya perlu <<, bukan <<=saat menambahkan elemen.
James A. Rosen
3

Operator +=saat diterapkan ke hash tersebut bekerja seperti yang diharapkan.

[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]}

Ini mungkin karena foo[bar]+=bazgula sintaksis foo[bar]=foo[bar]+bazketika foo[bar]di sisi kanan =dievaluasi, ia mengembalikan objek nilai default dan +operator tidak akan mengubahnya. Tangan kiri adalah gula sintaksis untuk []=metode yang tidak akan mengubah nilai default .

Perhatikan bahwa ini tidak berlaku foo[bar]<<=bazkarena akan sama dengan foo[bar]=foo[bar]<<bazdan << akan mengubah nilai default .

Juga, saya tidak menemukan perbedaan antara Hash.new{[]}dan Hash.new{|hash, key| hash[key]=[];}. Setidaknya di ruby ​​2.1.2.

Daniel Ribeiro Moreira
sumber
Penjelasan yang bagus. Sepertinya ruby ​​2.1.1 Hash.new{[]}sama dengan Hash.new([])saya dengan kurangnya <<perilaku yang diharapkan (meskipun tentu saja Hash.new{|hash, key| hash[key]=[];}berhasil). Hal-hal kecil yang aneh merusak semua hal: /
butterywombat
1

Saat Anda menulis,

h = Hash.new([])

Anda meneruskan referensi default array ke semua elemen dalam hash. Karena itu semua elemen dalam hash merujuk pada array yang sama.

jika Anda ingin setiap elemen dalam hash merujuk ke array terpisah, Anda harus menggunakan

h = Hash.new{[]} 

untuk detail lebih lanjut tentang cara kerjanya di ruby, silakan buka ini: http://ruby-doc.org/core-2.2.0/Array.html#method-c-new

Ganesh Sagare
sumber
Ini salah, Hash.new { [] }tidak tidak bekerja. Lihat jawaban saya untuk detailnya. Itu juga sudah merupakan solusi yang diajukan dalam jawaban lain.
Andrew Marshall