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_set
melewati ini, tetapi mereka harus ada untuk metaprogramming karena nilai-l di =
tidak bisa dinamis.