พฤติกรรมแปลก ๆ ที่ไม่คาดคิด (หายไป / เปลี่ยนค่า) เมื่อใช้ค่าเริ่มต้นของแฮชเช่น Hash.new ([])


107

พิจารณารหัสนี้:

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

ไม่เป็นไร แต่:

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

ณ จุดนี้ฉันคาดว่าแฮชจะเป็น:

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

แต่มันอยู่ไกลจากนั้น เกิดอะไรขึ้นและฉันจะได้รับพฤติกรรมที่คาดหวังได้อย่างไร

คำตอบ:


164

ขั้นแรกโปรดทราบว่าลักษณะการทำงานนี้ใช้กับค่าเริ่มต้นที่มีการกลายพันธุ์ในภายหลัง (เช่นแฮชและสตริง) ไม่ใช่เฉพาะอาร์เรย์

TL; DR : ใช้Hash.new { |h, k| h[k] = [] }ถ้าคุณต้องการวิธีแก้ปัญหาที่เป็นสำนวนมากที่สุดและไม่สนใจว่าทำไม


อะไรไม่ได้ผล

ทำไมHash.new([])ไม่ทำงาน

มาดูรายละเอียดเพิ่มเติมเกี่ยวกับสาเหตุที่Hash.new([])ไม่ได้ผล:

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

เราจะเห็นว่าออบเจ็กต์เริ่มต้นของเราถูกนำมาใช้ซ้ำและกลายพันธุ์ (เนื่องจากถูกส่งผ่านเป็นค่าเริ่มต้นเพียงค่าเดียวแฮชจึงไม่มีทางได้รับค่าเริ่มต้นใหม่ที่สดใหม่) แต่ทำไมจึงไม่มีคีย์หรือค่า ในอาร์เรย์แม้จะh[1]ยังคงให้เราคุ้มค่า? นี่คือคำแนะนำ:

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

อาร์เรย์ที่ส่งคืนโดยการ[]เรียกแต่ละครั้งเป็นเพียงค่าเริ่มต้นซึ่งเราได้ทำการเปลี่ยนแปลงตลอดเวลาดังนั้นตอนนี้จึงมีค่าใหม่ของเรา เนื่องจาก<<ไม่ได้กำหนดให้กับแฮช (ไม่สามารถมอบหมายงานใน Ruby ได้หากไม่มี=ของขวัญ ) เราจึงไม่เคยใส่อะไรลงไปในแฮชจริงของเรา แต่เราต้องใช้<<=(ซึ่งจะเป็นไป<<ตาม+=นั้น+):

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

สิ่งนี้เหมือนกับ:

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

ทำไมHash.new { [] }ไม่ทำงาน

การใช้Hash.new { [] }ช่วยแก้ปัญหาในการนำกลับมาใช้ใหม่และการเปลี่ยนค่าเริ่มต้นเดิม (เนื่องจากบล็อกที่กำหนดจะถูกเรียกในแต่ละครั้งโดยส่งคืนอาร์เรย์ใหม่) แต่ไม่ใช่ปัญหาในการกำหนด:

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

ทำงานอะไร

วิธีการมอบหมาย

ถ้าเราจำที่จะใช้เสมอ<<=แล้วHash.new { [] } เป็นโซลูชั่นที่ทำงานได้ แต่มันเป็นบิตแปลกและไม่ใช่สำนวน (ผมไม่เคยเห็น<<=ใช้ในป่า) นอกจากนี้ยังมีแนวโน้มที่จะเกิดข้อบกพร่องเล็กน้อยหาก<<ใช้โดยไม่ได้ตั้งใจ

วิธีที่ไม่แน่นอน

เอกสารสำหรับHash.newรัฐ (เน้นของตัวเอง):

หากระบุบล็อกบล็อกจะถูกเรียกด้วยวัตถุแฮชและคีย์และควรส่งคืนค่าเริ่มต้น มันเป็นความรับผิดชอบของบล็อกในการจัดเก็บค่าในกัญชาได้ตามต้องการ

ดังนั้นเราต้องเก็บค่าเริ่มต้นในแฮชจากภายในบล็อกหากเราต้องการใช้<<แทน<<=:

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

ได้อย่างมีประสิทธิภาพย้ายที่ได้รับมอบหมายจากสายของเราแต่ละคน (ซึ่งจะใช้<<=) เพื่อกระชากผ่านไปเอาภาระของพฤติกรรมที่ไม่คาดคิดเมื่อใช้Hash.new<<

โปรดทราบว่ามีความแตกต่างของการทำงานอย่างหนึ่งระหว่างวิธีนี้กับวิธีอื่น: วิธีนี้จะกำหนดค่าเริ่มต้นเมื่ออ่าน (เนื่องจากการกำหนดจะเกิดขึ้นภายในบล็อกเสมอ) ตัวอย่างเช่น:

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

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

วิธีที่ไม่เปลี่ยนรูป

คุณอาจสงสัยว่าทำไมHash.new([])ไม่ทำงานในขณะที่Hash.new(0)ทำงานได้ดี กุญแจสำคัญคือ Numerics ใน Ruby นั้นไม่เปลี่ยนรูปดังนั้นเราจึงไม่ต้องกลายพันธุ์ตามธรรมชาติ หากเราถือว่าค่าเริ่มต้นของเราไม่เปลี่ยนรูปเราก็สามารถใช้ได้Hash.new([])เช่นกัน:

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

([].freeze + [].freeze).frozen? == falseอย่างไรก็ตามโปรดทราบว่า ดังนั้นหากคุณต้องการให้แน่ใจว่าความไม่เปลี่ยนรูปจะถูกรักษาไว้ตลอดเวลาคุณต้องระมัดระวังในการตรึงวัตถุใหม่อีกครั้ง


สรุป

จากทุกวิธีโดยส่วนตัวแล้วฉันชอบ "วิธีที่ไม่เปลี่ยนรูป" - โดยทั่วไปแล้วความไม่เปลี่ยนแปลงทำให้การหาเหตุผลเกี่ยวกับสิ่งต่างๆนั้นง่ายกว่ามาก ท้ายที่สุดแล้วเป็นวิธีการเดียวที่ไม่มีความเป็นไปได้ของพฤติกรรมที่ไม่คาดคิดที่ซ่อนอยู่หรือละเอียดอ่อน อย่างไรก็ตามวิธีที่ใช้กันทั่วไปและเป็นสำนวนคือ“ วิธีที่ไม่แน่นอน”

ในฐานะที่เป็นครั้งสุดท้ายกันพฤติกรรมของค่าเริ่มต้นกัญชานี้ถูกบันทึกไว้ในทับทิม koans


นี่ไม่เป็นความจริงอย่างเคร่งครัดวิธีการเช่นinstance_variable_setข้ามสิ่งนี้ แต่ต้องมีอยู่สำหรับการเขียนโปรแกรมเมตาเนื่องจากค่า l ใน=ไม่สามารถเป็นไดนามิกได้


1
มีการกล่าวถึงว่าการใช้ "วิธีที่ไม่แน่นอน" ยังมีผลในการทำให้การค้นหาแฮชทุกครั้งเพื่อจัดเก็บคู่ค่าคีย์ (เนื่องจากมีงานมอบหมายเกิดขึ้นในบล็อก) ซึ่งอาจไม่เป็นที่ต้องการเสมอไป
johncip

@johncip ไม่ใช่ทุกการค้นหาเพียงคนแรกของแต่ละคีย์ แต่ฉันเข้าใจว่าคุณหมายถึงอะไรฉันจะเพิ่มคำตอบนั้นในภายหลัง ขอบคุณ!.
Andrew Marshall

อ๊ะเลอะเทอะ คุณพูดถูกแน่นอนมันเป็นการค้นหาคีย์ที่ไม่รู้จักครั้งแรก ฉันเกือบจะรู้สึกเหมือน{ [] }มี<<=มีความผิดน้อยที่สุดไม่ได้มันสำหรับความจริงที่ว่าตั้งใจลืม=อาจนำไปสู่เซสชั่นการแก้จุดบกพร่องทำให้เกิดความสับสนมาก
johncip

คำอธิบายที่ค่อนข้างชัดเจนเกี่ยวกับความแตกต่างเมื่อเริ่มต้นแฮชด้วยค่าเริ่มต้น
cisolarix

23

คุณกำลังระบุว่าค่าดีฟอลต์สำหรับแฮชเป็นการอ้างอิงไปยังอาร์เรย์ (ว่างเปล่าในตอนแรก) นั้น

ฉันคิดว่าคุณต้องการ:

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

ซึ่งตั้งค่าเริ่มต้นสำหรับแต่ละคีย์เป็นอาร์เรย์ใหม่


ฉันจะใช้อินสแตนซ์อาร์เรย์แยกต่างหากสำหรับแฮชใหม่แต่ละรายการได้อย่างไร
Valentin Vasilyev

5
เวอร์ชันบล็อกนั้นให้Arrayอินสแตนซ์ใหม่แก่คุณในการเรียกแต่ละครั้ง เพื่อปัญญา: h = Hash.new { |hash, key| hash[key] = []; puts hash[key].object_id }; h[1] # => 16348490; h[2] # => 16346570. นอกจากนี้: หากคุณใช้เวอร์ชันบล็อกที่ตั้งค่า ( {|hash,key| hash[key] = []}) แทนที่จะเป็นเวอร์ชันที่สร้างค่า ( { [] }) เพียงอย่างเดียวคุณจะต้องการเท่านั้น<<ไม่ใช่<<=เมื่อเพิ่มองค์ประกอบ
James A. Rosen

3

ตัวดำเนินการ+=เมื่อนำไปใช้กับแฮชเหล่านั้นทำงานตามที่คาดไว้

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

อาจเป็นเพราะfoo[bar]+=bazเป็นน้ำตาลเชิงไวยากรณ์foo[bar]=foo[bar]+bazเมื่อมีการประเมินfoo[bar]ทางด้านขวามือ=จะส่งคืนวัตถุค่าเริ่มต้นและตัว+ดำเนินการจะไม่เปลี่ยนแปลง ซ้ายมือเป็นน้ำตาลประโยคสำหรับ[]=วิธีการที่จะไม่เปลี่ยนค่าเริ่มต้น

หมายเหตุว่านี้ไม่ได้นำไปใช้foo[bar]<<=bazเป็นมันจะเทียบเท่ากับfoo[bar]=foo[bar]<<bazและ<< จะเปลี่ยนค่าเริ่มต้น

นอกจากนี้ผมพบว่าไม่มีความแตกต่างระหว่างและHash.new{[]} Hash.new{|hash, key| hash[key]=[];}อย่างน้อยในทับทิม 2.1.2


คำอธิบายที่ดี ดูเหมือนว่าใน Ruby 2.1.1 Hash.new{[]}จะเหมือนกับHash.new([])สำหรับฉันที่ไม่มี<<พฤติกรรมที่คาดหวัง(แม้ว่าจะได้Hash.new{|hash, key| hash[key]=[];}ผล) สิ่งเล็ก ๆ แปลก ๆ ทำลายทุกสิ่ง: /
butterywombat

1

เมื่อคุณเขียน

h = Hash.new([])

คุณส่งต่อการอ้างอิงเริ่มต้นของอาร์เรย์ไปยังองค์ประกอบทั้งหมดในแฮช เนื่องจากองค์ประกอบทั้งหมดในแฮชจึงอ้างถึงอาร์เรย์เดียวกัน

หากคุณต้องการให้แต่ละองค์ประกอบในแฮชอ้างถึงอาร์เรย์ที่แยกจากกันคุณควรใช้

h = Hash.new{[]} 

สำหรับรายละเอียดเพิ่มเติมเกี่ยวกับการทำงานของทับทิมโปรดไปที่ http://ruby-doc.org/core-2.2.0/Array.html#method-c-new


นี่คือผิดHash.new { [] }ไม่ได้ทำงาน ดูคำตอบของฉันสำหรับรายละเอียด มันเป็นคำตอบที่เสนอในคำตอบอื่นอยู่แล้ว
Andrew Marshall
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.