Trước tiên, hãy lưu ý rằng hành vi này áp dụng cho bất kỳ giá trị mặc định nào sau đó bị đột biến (ví dụ: băm và chuỗi), không chỉ mảng.
TL; DR : Sử dụng Hash.new { |h, k| h[k] = [] }
nếu bạn muốn giải pháp thành ngữ nhất và không quan tâm tại sao.
Những gì không hoạt động
Tại sao Hash.new([])
không hoạt động
Hãy cùng tìm hiểu sâu hơn về lý do tại sao Hash.new([])
không hoạt động:
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 #=> {}
Chúng ta có thể thấy rằng đối tượng mặc định của chúng ta đang được sử dụng lại và bị biến đổi (điều này là do nó được chuyển làm giá trị mặc định duy nhất, hàm băm không có cách nào nhận được giá trị mặc định mới, mới), nhưng tại sao không có khóa hoặc giá trị trong mảng, mặc dù h[1]
vẫn cho chúng ta một giá trị? Đây là một gợi ý:
h[42] #=> ["a", "b"]
Mảng được trả về bởi mỗi []
cuộc gọi chỉ là giá trị mặc định, mà chúng tôi đã thay đổi suốt thời gian qua nên bây giờ chứa các giá trị mới của chúng tôi. Vì <<
không gán cho băm (không bao giờ có thể gán trong Ruby mà không có =
quà † ), chúng tôi chưa bao giờ đưa bất cứ thứ gì vào hàm băm thực sự của mình. Thay vào đó chúng ta phải sử dụng <<=
(mà là <<
như +=
là +
):
h[2] <<= 'c' #=> ["a", "b", "c"]
h #=> {2=>["a", "b", "c"]}
Điều này giống như:
h[2] = (h[2] << 'c')
Tại sao Hash.new { [] }
không hoạt động
Việc sử dụng Hash.new { [] }
giải quyết vấn đề sử dụng lại và thay đổi giá trị mặc định ban đầu (vì khối đã cho được gọi mỗi lần, trả về một mảng mới), nhưng không giải quyết được vấn đề gán:
h = Hash.new { [] }
h[0] << 'a' #=> ["a"]
h[1] <<= 'b' #=> ["b"]
h #=> {1=>["b"]}
Làm việc gì
Cách phân công
Nếu chúng ta nhớ luôn sử dụng <<=
, thì đó Hash.new { [] }
là một giải pháp khả thi, nhưng nó hơi kỳ quặc và không thành ngữ (tôi chưa bao giờ thấy <<=
được sử dụng trong tự nhiên). Nó cũng dễ gặp các lỗi nhỏ nếu <<
vô tình được sử dụng.
Cách có thể thay đổi
Các tài liệu choHash.new
tiểu bang (nhấn mạnh của riêng tôi):
Nếu một khối được chỉ định, nó sẽ được gọi với đối tượng băm và khóa, và sẽ trả về giá trị mặc định. Khối có trách nhiệm lưu trữ giá trị trong hàm băm nếu được yêu cầu .
Vì vậy, chúng ta phải lưu trữ giá trị mặc định trong hàm băm từ bên trong khối nếu chúng ta muốn sử dụng <<
thay vì <<=
:
h = Hash.new { |h, k| h[k] = [] }
h[0] << 'a' #=> ["a"]
h[1] << 'b' #=> ["b"]
h #=> {0=>["a"], 1=>["b"]}
Điều này có hiệu quả chuyển nhiệm vụ từ các cuộc gọi riêng lẻ của chúng tôi (sẽ sử dụng <<=
) sang khối được chuyển đến Hash.new
, loại bỏ gánh nặng về hành vi không mong muốn khi sử dụng <<
.
Lưu ý rằng có một sự khác biệt về chức năng giữa phương pháp này và các phương pháp khác: cách này chỉ định giá trị mặc định khi đọc (vì việc gán luôn xảy ra bên trong khối). Ví dụ:
h1 = Hash.new { |h, k| h[k] = [] }
h1[:x]
h1 #=> {:x=>[]}
h2 = Hash.new { [] }
h2[:x]
h2 #=> {}
Con đường bất biến
Bạn có thể tự hỏi tại sao Hash.new([])
không hoạt động trong khi Hash.new(0)
hoạt động tốt. Điều quan trọng là các Số trong Ruby là bất biến, do đó, chúng ta không bao giờ kết thúc việc thay đổi chúng tại chỗ. Nếu chúng tôi coi giá trị mặc định của mình là bất biến, chúng tôi cũng có thể sử dụng Hash.new([])
tốt:
h = Hash.new([].freeze)
h[0] += ['a'] #=> ["a"]
h[1] += ['b'] #=> ["b"]
h[2] #=> []
h #=> {0=>["a"], 1=>["b"]}
Tuy nhiên, hãy lưu ý rằng ([].freeze + [].freeze).frozen? == false
. Vì vậy, nếu bạn muốn đảm bảo rằng tính bất biến được duy trì trong suốt, thì bạn phải cẩn thận để đóng băng lại đối tượng mới.
Phần kết luận
Trong tất cả các cách, cá nhân tôi thích "cách bất biến" - tính thay đổi thường làm cho lý luận về mọi thứ đơn giản hơn nhiều. Rốt cuộc, nó là phương pháp duy nhất không có khả năng ẩn hoặc hành vi bất ngờ tinh vi. Tuy nhiên, cách thành ngữ và phổ biến nhất là "cách có thể thay đổi".
Cuối cùng sang một bên, hành vi này của các giá trị mặc định Hash được ghi nhận trong Ruby Koans .
† Điều này không hoàn toàn đúng, các phương thức như instance_variable_set
bỏ qua điều này, nhưng chúng phải tồn tại để lập trình siêu thị vì giá trị l trong =
không thể là động.