Hành vi lạ, không mong muốn (biến mất / thay đổi giá trị) khi sử dụng giá trị mặc định của Hash, ví dụ: Hash.new ([])


107

Hãy xem xét mã này:

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

Đó là tất cả tốt, nhưng:

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

Tại thời điểm này, tôi mong đợi hàm băm là:

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

nhưng nó còn xa điều đó. Điều gì đang xảy ra và làm thế nào tôi có thể có được hành vi mà tôi mong đợi?

Câu trả lời:


164

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ư +=+):

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 { [] } 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_setbỏ 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.


1
Cần nhắc lại rằng việc sử dụng "cách có thể thay đổi" cũng có tác dụng khiến mọi tra cứu băm lưu trữ một cặp giá trị khóa (vì có một nhiệm vụ xảy ra trong khối), điều này có thể không phải lúc nào cũng mong muốn.
johncip

@johncip Không phải mọi tra cứu, chỉ tra cứu đầu tiên cho mỗi khóa. Nhưng tôi hiểu ý bạn, tôi sẽ thêm điều đó vào câu trả lời sau; cảm ơn!.
Andrew Marshall

Rất tiếc, cẩu thả. Tất nhiên, bạn nói đúng, đây là lần tra cứu đầu tiên của một khóa không xác định. Tôi gần như cảm thấy như { [] }với <<=có ít bất ngờ nhất, không phải vì thực tế là việc vô tình quên =có thể dẫn đến một phiên gỡ lỗi rất khó hiểu.
johncip

khá giải thích rõ ràng về sự khác biệt khi khởi băm với giá trị mặc định
cisolarix

23

Bạn đang chỉ định rằng giá trị mặc định cho hàm băm là một tham chiếu đến mảng cụ thể (ban đầu trống) đó.

Tôi nghĩ bạn muốn:

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

Điều đó đặt giá trị mặc định cho mỗi khóa thành một mảng mới .


Làm cách nào để sử dụng các phiên bản mảng riêng biệt cho mỗi hàm băm mới?
Valentin Vasilyev

5
Phiên bản khối đó cung cấp cho bạn các phiên bản mới Arraytrên mỗi lời gọi. Để wit: h = Hash.new { |hash, key| hash[key] = []; puts hash[key].object_id }; h[1] # => 16348490; h[2] # => 16346570. Ngoài ra: nếu bạn sử dụng phiên bản khối đặt giá trị ( {|hash,key| hash[key] = []}) thay vì phiên bản chỉ tạo ra giá trị ( { [] }), thì bạn chỉ cần <<, không phải <<=khi thêm các phần tử.
James A. Rosen

3

Toán tử +=khi được áp dụng cho các băm đó hoạt động như mong đợi.

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

Điều này có thể do foo[bar]+=bazlà đường cú pháp foo[bar]=foo[bar]+bazkhi foo[bar]ở bên phải của =được đánh giá, nó trả về giá trị mặc định đối tượng và +toán tử sẽ không thay đổi nó. Bên trái là đường cú pháp cho []=phương thức sẽ không thay đổi giá trị mặc định .

Lưu ý rằng điều này không áp dụng cho foo[bar]<<=bazvì nó sẽ tương đương foo[bar]=foo[bar]<<baz<< sẽ thay đổi giá trị mặc định .

Ngoài ra, tôi không tìm thấy sự khác biệt giữa Hash.new{[]}Hash.new{|hash, key| hash[key]=[];}. Ít nhất trên ruby ​​2.1.2.


Lời giải thích hay. Có vẻ như trên ruby ​​2.1.1 Hash.new{[]}cũng giống như Hash.new([])đối với tôi với việc thiếu <<hành vi mong đợi (mặc dù tất nhiên là Hash.new{|hash, key| hash[key]=[];}hoạt động). Những điều nhỏ nhặt Weird phá vỡ tất cả những điều: /
butterywombat

1

Khi bạn viết,

h = Hash.new([])

bạn chuyển tham chiếu mặc định của mảng cho tất cả các phần tử trong hàm băm. bởi vì tất cả các phần tử trong băm tham chiếu đến cùng một mảng.

nếu bạn muốn mỗi phần tử trong hàm băm tham chiếu đến mảng riêng biệt, bạn nên sử dụng

h = Hash.new{[]} 

để biết thêm chi tiết về cách nó hoạt động trong ruby, vui lòng truy cập trang này: http://ruby-doc.org/core-2.2.0/Array.html#method-c-new


Điều này là sai, Hash.new { [] }không không làm việc. Xem câu trả lời của tôi để biết chi tiết. Nó cũng đã là giải pháp được đề xuất trong một câu trả lời khác.
Andrew Marshall,
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.