การเปลี่ยนแปลงไม่ได้จะคุ้มค่ามากหรือไม่เมื่อไม่มีการเห็นพ้องด้วย?


53

ดูเหมือนว่าความปลอดภัยของเธรดมักถูกกล่าวถึงบ่อยครั้งว่าเป็นประโยชน์หลักของการใช้ประเภทที่ไม่เปลี่ยนรูปและโดยเฉพาะอย่างยิ่งคอลเลกชัน

ฉันมีสถานการณ์ที่ฉันต้องการตรวจสอบให้แน่ใจว่าวิธีการจะไม่แก้ไขพจนานุกรมสตริง (ซึ่งไม่เปลี่ยนรูปใน C #) ฉันต้องการ จำกัด สิ่งต่าง ๆ ให้มากที่สุด

อย่างไรก็ตามฉันไม่แน่ใจว่าการเพิ่มการพึ่งพาแพ็คเกจใหม่ (Microsoft Immutable Collections) นั้นคุ้มค่าหรือไม่ ประสิทธิภาพไม่ใช่ปัญหาใหญ่เช่นกัน

ดังนั้นฉันเดาว่าคำถามของฉันคือการแนะนำให้สะสมอย่างไม่เปลี่ยนรูปแบบเมื่อไม่มีข้อกำหนดเรื่องประสิทธิภาพการทำงานที่หนักหน่วงและไม่มีปัญหาเรื่องความปลอดภัยของเธรดหรือไม่? พิจารณาความหมายที่มีค่า (ตามตัวอย่างของฉัน) อาจจะใช่หรือไม่ใช่ข้อกำหนดก็ได้เช่นกัน


1
การแก้ไขพร้อมกันไม่จำเป็นต้องหมายถึงเธรด เพียงแค่ดูชื่อ aptly ConcurrentModificationExceptionซึ่งมักจะเกิดจากเธรดเดียวกันการกลายคอลเลกชันในเธรดเดียวกันในเนื้อความของforeachวนรอบคอลเลกชันเดียวกัน

1
ฉันหมายความว่าคุณไม่ผิด แต่นั่นแตกต่างจากสิ่งที่ OP ขอ ข้อยกเว้นนั้นถูกโยนเนื่องจากไม่อนุญาตให้ทำการแก้ไขในระหว่างการแจงนับ ยกตัวอย่างเช่นการใช้ ConcurrentDictionary ยังคงมีข้อผิดพลาดนี้อยู่
edthethird

13
บางทีคุณควรถามคำถามตรงข้ามกับตัวเองว่าเมื่อใดความแปรปรวนที่มีค่าควรแก่เมื่อ
จอร์โจ

2
ใน Java หากความไม่แน่นอนส่งผลกระทบต่อhashCode()หรือequals(Object)ผลการเปลี่ยนแปลงอาจทำให้เกิดข้อผิดพลาดเมื่อใช้Collections(ตัวอย่างเช่นในHashSetวัตถุที่ถูกเก็บไว้ใน "ถัง" และหลังจากกลายพันธุ์ควรไปที่อื่น)
SJuan76

2
@ DavorŽdraloตราบใดที่ abstractions ของภาษาระดับสูงดำเนินไปความไม่เปลี่ยนแปลงที่แพร่หลายจึงค่อนข้างเชื่อง มันเป็นเพียงส่วนขยายตามธรรมชาติของสิ่งที่เป็นนามธรรม (ปัจจุบันแม้กระทั่งใน C) ของการสร้างและทิ้ง "ค่าชั่วคราว" อย่างเงียบ ๆ บางทีคุณอาจจะพูดว่ามันเป็นวิธีที่ไม่มีประสิทธิภาพในการใช้ CPU แต่ข้อโต้แย้งนั้นมีข้อบกพร่องเช่นกัน: ภาษาที่มีความสุขที่ไม่แน่นอน แต่มีพลวัตมักจะทำงานได้แย่กว่าภาษาที่เปลี่ยนแปลงไม่ได้ แต่มีเพียงส่วนหนึ่ง เทคนิคเพื่อเพิ่มประสิทธิภาพโปรแกรมที่เล่นปาหี่ข้อมูลที่ไม่เปลี่ยนรูปแบบ: ประเภทลิ

คำตอบ:


101

เปลี่ยนไม่ได้ช่วยลดความยุ่งยากปริมาณของข้อมูลที่คุณต้องติดตามจิตใจเมื่อมีการอ่านรหัสในภายหลัง สำหรับตัวแปรที่ไม่แน่นอนและโดยเฉพาะอย่างยิ่งสมาชิกชั้นที่ไม่แน่นอนมันยากมากที่จะรู้ว่าพวกเขาจะอยู่ในสถานะใดในบรรทัดที่คุณกำลังอ่านโดยไม่ต้องเรียกใช้โค้ดด้วยดีบักเกอร์ ข้อมูลที่ไม่เปลี่ยนรูปนั้นง่ายต่อการให้เหตุผล - มันจะเหมือนกันเสมอ หากคุณต้องการเปลี่ยนคุณจะต้องสร้างค่าใหม่

ฉันอยากจะทำให้สิ่งต่าง ๆ ไม่เปลี่ยนไปโดยปริยายแล้วเปลี่ยนมันให้กลายเป็นที่ไม่แน่นอนซึ่งมันพิสูจน์แล้วว่าพวกเขาจำเป็นต้องเป็นไม่ว่าจะเป็นสิ่งที่คุณต้องการประสิทธิภาพหรืออัลกอริธึมที่คุณไม่เข้าใจ


23
+1 การเห็นพ้องด้วยกันคือการกลายพันธุ์พร้อมกัน แต่การกลายพันธุ์ที่แพร่กระจายไปตามกาลเวลาอาจเป็นเรื่องยากที่จะให้เหตุผล
guillaume31

20
ในการขยายนี้: ฟังก์ชั่นที่อาศัยตัวแปรที่ไม่แน่นอนสามารถคิดได้ว่าเป็นการโต้แย้งที่ซ่อนอยู่พิเศษซึ่งเป็นค่าปัจจุบันของตัวแปรที่ไม่แน่นอน ฟังก์ชั่นใด ๆ ที่เปลี่ยนตัวแปรที่ไม่แน่นอนสามารถคิดเช่นเดียวกันกับการสร้างค่าตอบแทนพิเศษเช่นค่าใหม่ของรัฐที่ไม่แน่นอน เมื่อมองไปที่ชิ้นส่วนของรหัสคุณไม่ทราบว่าขึ้นอยู่กับหรือเปลี่ยนแปลงสถานะที่ไม่แน่นอนดังนั้นคุณต้องค้นหาและติดตามการเปลี่ยนแปลงทางจิตใจ สิ่งนี้ยังแนะนำการมีเพศสัมพันธ์ระหว่างโค้ดสองชิ้นที่แบ่งเป็นสถานะที่ไม่แน่นอนและการมีเพศสัมพันธ์ไม่ดี
Doval

29
@ Mehrdad ผู้คนก็สามารถค้นพบโปรแกรมขนาดใหญ่ในการชุมนุมมานานหลายทศวรรษ จากนั้นเราก็ทำสองสามทศวรรษของ C.
Doval

11
@ Mehrdad การคัดลอกวัตถุทั้งหมดไม่ใช่ตัวเลือกที่ดีเมื่อวัตถุมีขนาดใหญ่ ฉันไม่เห็นสาเหตุที่ลำดับความสำคัญเกี่ยวข้องกับการปรับปรุง คุณจะลดการปรับปรุงลง 20% (หมายเหตุ: ตัวเลขโดยพลการ) ในการเพิ่มประสิทธิภาพเพียงเพราะมันไม่ใช่การปรับปรุงสามหลัก? Immutability เป็นค่าเริ่มต้นสติ; คุณสามารถหลงทางจากมัน แต่คุณต้องการเหตุผล
Doval

9
@Giorgio Scala ทำให้ฉันรู้ว่าคุณต้องทำให้ค่าที่ไม่แน่นอนไม่บ่อยนัก เมื่อใดก็ตามที่ฉันใช้ภาษาที่ผมทำทุกอย่างและเฉพาะในมากโอกาสที่หายากมากฉันจะหาว่าผมต้องเปลี่ยนอะไรบางอย่างที่เป็นval var'ตัวแปร' จำนวนมากที่ฉันกำหนดไว้ในภาษาใดก็ตามมีไว้เพื่อเก็บค่าที่เก็บผลลัพธ์ของการคำนวณบางอย่างและไม่จำเป็นต้องได้รับการอัปเดต
KChaloux

22

รหัสของคุณควรแสดงเจตนาของคุณ หากคุณไม่ต้องการให้วัตถุถูกแก้ไขเมื่อสร้างขึ้นทำให้ไม่สามารถแก้ไขได้

Immutability มีประโยชน์หลายประการ:

  • ความตั้งใจของผู้เขียนต้นฉบับจะแสดงออกมาดีกว่า

    คุณจะรู้ได้อย่างไรว่าในรหัสต่อไปนี้การแก้ไขชื่อจะทำให้แอปพลิเคชันสร้างข้อยกเว้นในภายหลัง

    public class Product
    {
        public string Name { get; set; }
    
        ...
    }
    
  • ง่ายกว่าเพื่อให้แน่ใจว่าวัตถุจะไม่ปรากฏในสถานะที่ไม่ถูกต้อง

    คุณต้องควบคุมสิ่งนี้ในคอนสตรัคเตอร์และที่นั่นเท่านั้น ในทางกลับกันหากคุณมี setters จำนวนมากและวิธีการที่ปรับเปลี่ยนวัตถุตัวควบคุมดังกล่าวอาจกลายเป็นเรื่องยากโดยเฉพาะอย่างยิ่งโดยเฉพาะเมื่อตัวอย่างเช่นสองเขตข้อมูลควรเปลี่ยนแปลงในเวลาเดียวกันเพื่อให้วัตถุนั้นถูกต้อง

    ตัวอย่างเช่นวัตถุนั้นถูกต้องหากที่อยู่ไม่ใช่null หรือพิกัด GPS ไม่nullถูกต้อง แต่จะไม่ถูกต้องหากระบุทั้งที่อยู่และพิกัด GPS คุณลองจินตนาการถึงนรกที่จะตรวจสอบสิ่งนี้ได้ไหมถ้าทั้งที่อยู่และพิกัด GPS มีตัวตั้งค่าหรือทั้งสองอย่างนั้นไม่แน่นอน

  • เห็นพ้องด้วย

ในกรณีของคุณคุณไม่จำเป็นต้องมีแพ็คเกจของบุคคลที่สาม .NET Framework รวมReadOnlyDictionary<TKey, TValue>คลาสแล้ว


1
+1 โดยเฉพาะอย่างยิ่งสำหรับ "คุณต้องควบคุมสิ่งนี้ในตัวสร้างและเฉพาะที่นั่น" IMO นี่เป็นข้อได้เปรียบอย่างมาก
จอร์โจ

10
ประโยชน์อีกประการหนึ่ง: การคัดลอกวัตถุนั้นฟรี เพียงแค่ตัวชี้
Robert Grant

1
@MainMa ขอบคุณสำหรับคำตอบของคุณ แต่เท่าที่ฉันเข้าใจ ReadOnlyDictionary ไม่รับประกันใด ๆ ที่คนอื่นจะไม่เปลี่ยนพจนานุกรมพื้นฐาน (แม้จะไม่มีการทำงานพร้อมกันฉันอาจต้องการบันทึกการอ้างอิงไปยังพจนานุกรมต้นฉบับภายในวัตถุที่เป็นวิธีการ เพื่อใช้ในภายหลัง) ReadOnlyDictionary ถูกประกาศในเนมสเปซที่แปลก: System.Collections.ObjectModel
Den

2
@Den: ที่เกี่ยวข้องกับหนึ่งในสัตว์เลี้ยงของฉัน peeves: คนที่เกี่ยวข้องกับ "อ่านอย่างเดียว" และ "ไม่เปลี่ยนรูป" เป็นคำพ้อง หากวัตถุถูกห่อหุ้มใน wrapper แบบอ่านอย่างเดียวและไม่มีการอ้างอิงอื่นใดอยู่หรือถูกเก็บไว้ที่ใดก็ได้ในจักรวาลการห่อวัตถุนั้นจะทำให้มันไม่เปลี่ยนรูปและการอ้างอิงถึง wrapper อาจใช้เป็นชวเลขเพื่อห่อหุ้มสถานะของ วัตถุที่อยู่ในนั้น อย่างไรก็ตามไม่มีกลไกใดที่รหัสสามารถตรวจสอบได้ว่าเป็นกรณีนี้หรือไม่ ไปในทางตรงกันข้ามเพราะเสื้อคลุมซ่อนชนิดของวัตถุห่อห่อวัตถุที่ไม่เปลี่ยนรูป ...
SuperCat

2
... จะทำให้เป็นไปไม่ได้ที่โค้ดจะทราบว่าเสื้อคลุมที่ส่งออกอาจถูกพิจารณาว่าไม่เปลี่ยนรูปได้อย่างปลอดภัย
supercat

13

มีเหตุผลหลายเธรดเดียวที่ใช้ไม่ได้ ตัวอย่างเช่น

วัตถุ A มีวัตถุ B

โค้ดภายนอกสอบถามวัตถุ B ของคุณแล้วส่งคืน

ตอนนี้คุณมีสามสถานการณ์ที่เป็นไปได้:

  1. B ไม่เปลี่ยนรูปไม่มีปัญหา
  2. B ไม่แน่นอนคุณทำสำเนาการป้องกันและกลับมาที่ ประสิทธิภาพเยี่ยมชม แต่ไม่มีความเสี่ยง
  3. B ไม่แน่นอนคุณส่งคืนมัน

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


9

การเปลี่ยนรูปไม่ได้ยังสามารถทำให้การใช้งานของตัวรวบรวมขยะง่ายขึ้นมาก จากวิกิของ GHC :

[... ] การเปลี่ยนแปลงข้อมูลไม่ได้บังคับให้เราสร้างข้อมูลชั่วคราวจำนวนมาก แต่มันยังช่วยในการเก็บรวบรวมขยะอย่างรวดเร็ว เคล็ดลับคือข้อมูลที่ไม่เปลี่ยนรูปไม่เคยชี้ไปยังค่าที่อายุน้อยกว่า อันที่จริงค่าอายุน้อยกว่ายังไม่มีอยู่ในเวลาที่มีการสร้างค่าเก่าดังนั้นจึงไม่สามารถชี้ได้ตั้งแต่เริ่มต้น และเนื่องจากค่าไม่เคยถูกแก้ไขจึงไม่สามารถชี้ไปยังภายหลังได้ นี่คือคุณสมบัติที่สำคัญของข้อมูลที่ไม่เปลี่ยนรูป

สิ่งนี้ทำให้การรวบรวมขยะ (GC) ง่ายขึ้นอย่างมาก ทุกครั้งที่เราสามารถสแกนค่าสุดท้ายที่สร้างขึ้นและปลดปล่อยค่าที่ไม่ได้ชี้ไปจากชุดเดียวกัน (แน่นอนว่ารากที่แท้จริงของลำดับชั้นของค่าสดนั้นจะใช้งานจริงในสแต็ก) [... ] ดังนั้นมันจึงมีพฤติกรรมต่อต้านการใช้งานง่าย: เปอร์เซ็นต์ที่มากกว่าของคุณคือขยะ - ยิ่งทำงานได้เร็วขึ้น [ ... ]


5

ขยายสิ่งที่KChalouxสรุปได้เป็นอย่างดี ...

เป็นการดีที่คุณมีเขตข้อมูลสองประเภทและด้วยเหตุนี้จึงใช้รหัสสองประเภท ฟิลด์ใดฟิลด์หนึ่งไม่เปลี่ยนรูปและโค้ดไม่จำเป็นต้องคำนึงถึงความไม่แน่นอน หรือเขตข้อมูลไม่แน่นอนและเราจำเป็นต้องเขียนโค้ดที่ใช้ snapshot ( int x = p.x) หรือจัดการกับการเปลี่ยนแปลงดังกล่าว

จากประสบการณ์ของฉันรหัสส่วนใหญ่อยู่ระหว่างทั้งสองเป็นรหัสในแง่ดี : มันอ้างอิงข้อมูลที่ไม่แน่นอนโดยสมมติว่าการโทรครั้งแรกp.xจะมีผลเช่นเดียวกับการโทรครั้งที่สอง และส่วนใหญ่แล้วนี่เป็นเรื่องจริงยกเว้นเมื่อปรากฎว่าไม่มีอีกต่อไป อุ่ย

ดังนั้นให้หันคำถามนั้นไปที่: เหตุผลของฉันในการทำให้เกิดความไม่แน่นอนนี้คืออะไร?

  • การลดการจัดสรรหน่วยความจำ / ฟรี
  • ไม่แน่นอนโดยธรรมชาติ? (เช่นตัวนับ)
  • บันทึกโมเดอเรเตอร์, เสียงแนวนอนหรือไม่ (const / สุดท้าย)
  • ทำให้บางรหัสสั้นลง / ง่ายขึ้นไหม? (ค่าเริ่มต้น init อาจเขียนทับได้หลังจาก)

คุณเขียนรหัสป้องกันหรือไม่ การเปลี่ยนไม่ได้จะช่วยให้คุณประหยัดเวลาในการคัดลอก คุณเขียนโค้ดในแง่ดีหรือไม่? การเปลี่ยนไม่ได้จะช่วยให้คุณเสียความบ้าของข้อผิดพลาดที่แปลกและเป็นไปไม่ได้


3

ข้อดีอีกอย่างของการไม่เปลี่ยนรูปก็คือมันเป็นขั้นตอนแรกในการปัดเศษวัตถุที่ไม่เปลี่ยนรูปเหล่านี้ให้กลายเป็นพูล จากนั้นคุณสามารถจัดการพวกเขาเพื่อที่คุณจะไม่สร้างวัตถุหลาย ๆ อันที่มีแนวคิดและความหมายแทนสิ่งเดียวกัน ตัวอย่างที่ดีคือสตริงของ Java

เป็นปรากฏการณ์ที่รู้จักกันดีในภาษาศาสตร์ซึ่งมีคำไม่กี่คำที่ปรากฏขึ้นมากมายอาจปรากฏในบริบทอื่นเช่นกัน ดังนั้นแทนที่จะสร้างหลายStringวัตถุคุณสามารถใช้สิ่งที่เปลี่ยนไม่ได้ แต่คุณต้องให้ผู้จัดการกลุ่มดูแลวัตถุที่ไม่เปลี่ยนรูปเหล่านี้

วิธีนี้จะช่วยให้คุณประหยัดหน่วยความจำได้มาก นี่เป็นบทความที่น่าสนใจที่จะอ่านเช่นกัน: http://en.wikipedia.org/wiki/Zipf%27s_law


1

ใน Java, C # และภาษาอื่นที่คล้ายคลึงกันอาจใช้ฟิลด์ประเภทคลาสเพื่อระบุวัตถุหรือห่อหุ้มคุณค่าหรือสถานะในวัตถุเหล่านั้น แต่ภาษาไม่ได้แยกความแตกต่างระหว่างการใช้งานดังกล่าว สมมติว่าวัตถุชั้นมีด้านการพิมพ์George char[] chars;ฟิลด์นั้นอาจสรุปลำดับอักขระใน:

  1. อาร์เรย์ที่จะไม่ถูกแก้ไขหรือเปิดเผยรหัสใด ๆ ที่อาจแก้ไข แต่อาจมีการอ้างอิงภายนอก

  2. อาร์เรย์ที่ไม่มีการอ้างอิงภายนอกอยู่ แต่ George อาจปรับเปลี่ยนได้อย่างอิสระ

  3. อาร์เรย์ที่ George เป็นเจ้าของ แต่อาจมีมุมมองภายนอกซึ่งคาดว่าจะเป็นตัวแทนสถานะปัจจุบันของ George

นอกจากนี้ตัวแปรอาจแทนที่ลำดับอักขระที่ห่อหุ้มห่อหุ้มมุมมองสดให้กับลำดับอักขระที่เป็นเจ้าของโดยวัตถุอื่น ๆ

หากcharsแค็ปซูลลำดับอักขระ [ลม] และจอร์จต้องการที่charsจะแค็ปซูลลำดับอักขระ [ไม้กายสิทธิ์] มีหลายสิ่งที่จอร์จสามารถทำได้:

A. สร้างอาร์เรย์ใหม่ที่มีอักขระ [ไม้กายสิทธิ์] และเปลี่ยนcharsเพื่อระบุอาร์เรย์นั้นแทนที่จะเป็นอาร์เรย์เก่า

B. ระบุอาเรย์ตัวละครที่มีอยู่แล้วซึ่งจะถือตัวอักษร [ไม้กายสิทธิ์] และเปลี่ยนcharsเพื่อระบุอาเรย์นั้นมากกว่าแบบเก่า

ซีเปลี่ยนอักขระตัวที่สองของอาร์เรย์ระบุไปยังcharsa

ในกรณีที่ 1 (A) และ (B) เป็นวิธีที่ปลอดภัยเพื่อให้ได้ผลลัพธ์ที่ต้องการ ในกรณี (2), (A) และ (C) มีความปลอดภัย แต่ (B) จะไม่ [มันจะไม่ทำให้เกิดปัญหาทันที แต่เนื่องจากจอร์จจะสมมติว่ามันมีกรรมสิทธิ์ในอาร์เรย์มันจะถือว่ามันเป็น สามารถเปลี่ยนอาร์เรย์ได้ตามต้องการ] ในกรณี (3) ตัวเลือก (A) และ (B) จะแยกมุมมองภายนอกใด ๆ และทำให้ตัวเลือก (C) ถูกต้องเท่านั้น ดังนั้นการรู้วิธีแก้ไขลำดับอักขระที่ห่อหุ้มโดยฟิลด์นั้นจำเป็นต้องรู้ชนิดของฟิลด์ที่มีความหมาย

หากแทนที่จะใช้เขตข้อมูลชนิดchar[]ซึ่ง encapsulate ลำดับอักขระที่อาจไม่แน่นอนรหัสมีชนิดที่ใช้Stringซึ่ง encapsulate ลำดับอักขระที่ไม่เปลี่ยนรูปปัญหาทั้งหมดข้างต้นหายไป ฟิลด์ทุกประเภทประเภทStringแค็ปซูลลำดับของตัวละครโดยใช้วัตถุที่สามารถแบ่งปันได้ซึ่งจะไม่เปลี่ยนแปลง ดังนั้นถ้าประเภทของเขตข้อมูลStringห่อหุ้ม "ลม" วิธีเดียวที่จะทำให้มันห่อหุ้ม "ไม้กายสิทธิ์" คือการทำให้มันระบุวัตถุต่าง ๆ - หนึ่งซึ่งถือ "ไม้กายสิทธิ์" ในกรณีที่โค้ดเก็บการอ้างอิงไปยังวัตถุเพียงอย่างเดียวการกลายพันธุ์วัตถุนั้นอาจมีประสิทธิภาพมากกว่าการสร้างขึ้นใหม่ แต่เมื่อใดก็ตามที่คลาสไม่สามารถเปลี่ยนแปลงได้จำเป็นต้องแยกความแตกต่างระหว่างวิธีต่างๆที่สามารถห่อหุ้มคุณค่าได้ โดยส่วนตัวแล้วฉันคิดว่าแอป Hungarian ควรจะใช้สำหรับสิ่งนี้ (ฉันจะพิจารณาการใช้ทั้งสี่ประเภทchar[]เพื่อให้มีความแตกต่างทางความหมายถึงแม้ว่าระบบประเภทจะถือว่าพวกเขาเหมือนกัน - เป็นประเภทของสถานการณ์ที่ Apps Hungarian ส่องสว่าง) แต่เนื่องจาก ไม่ใช่วิธีที่ง่ายที่สุดในการหลีกเลี่ยงความคลุมเครือเช่นนั้นคือการออกแบบประเภทที่ไม่เปลี่ยนรูปแบบซึ่งห่อหุ้มคุณค่าทางเดียวเท่านั้น


ดูเหมือนคำตอบที่สมเหตุสมผล แต่ค่อนข้างยากที่จะอ่านและเข้าใจ
Den

1

มีตัวอย่างที่ดีอยู่ที่นี่ แต่ฉันต้องการที่จะข้ามไปกับบางคนที่ไม่สามารถช่วยได้ตัน ในกรณีของฉันฉันเริ่มออกแบบโครงสร้างข้อมูลพร้อมกันที่ไม่เปลี่ยนรูปได้โดยหวังว่าจะสามารถรันโค้ดอย่างมั่นใจควบคู่ไปกับการอ่านและเขียนที่ทับซ้อนกันและไม่ต้องกังวลเกี่ยวกับสภาพการแข่งขัน มีการพูดคุยกัน John Carmack ให้แรงบันดาลใจแบบนั้นกับฉันที่เขาพูดถึงความคิดเช่นนั้น มันเป็นโครงสร้างพื้นฐานที่สวยงามและใช้งานได้ง่ายเช่นนี้:

ป้อนคำอธิบายรูปภาพที่นี่

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

แต่ผมไม่ได้พบว่ามันว่ามีประโยชน์สำหรับวัตถุประสงค์หลายเธรด ท้ายที่สุดก็ยังมีปัญหาทางความคิดที่ว่าระบบฟิสิกส์ใช้ฟิสิกส์พร้อมกันในขณะที่ผู้เล่นพยายามเคลื่อนย้ายองค์ประกอบรอบโลก คุณทำสำเนาข้อมูลแปลงที่แปลงไม่ได้อันไหนเครื่องเล่นแปลงหรือแปลงระบบฟิสิกส์? ดังนั้นฉันจึงไม่พบวิธีที่ดีและเรียบง่ายสำหรับปัญหาแนวคิดพื้นฐานนี้ยกเว้นมีโครงสร้างข้อมูลที่ไม่แน่นอนซึ่งเพิ่งล็อคในวิธีที่ชาญฉลาดและกีดกันการทับซ้อนกันอ่านและเขียนลงในส่วนเดียวกันของบัฟเฟอร์เพื่อหลีกเลี่ยงเธรดที่ค้างอยู่ นั่นคือสิ่งที่ John Carmack ดูเหมือนจะรู้วิธีแก้ไขในเกมของเขา อย่างน้อยเขาก็พูดถึงมันเหมือนว่าเขาเกือบจะได้เห็นวิธีแก้ปัญหาโดยไม่ต้องเปิดรถเวิร์ม ฉันไม่ได้ไปไกลถึงเขาในเรื่องนั้น ทั้งหมดที่ฉันเห็นคือคำถามการออกแบบที่ไม่มีที่สิ้นสุดหากฉันพยายามทำขนานทุกอย่างรอบ ๆ สิ่งที่ไม่เปลี่ยนแปลง ฉันหวังว่าฉันจะใช้เวลาหนึ่งวันเพื่อเก็บสมองของเขาเพราะความพยายามส่วนใหญ่ของฉันเริ่มต้นจากความคิดที่เขาขว้างออกไป

อย่างไรก็ตามฉันพบมูลค่ามหาศาลของโครงสร้างข้อมูลที่ไม่เปลี่ยนรูปแบบนี้ในพื้นที่อื่น ๆ ฉันยังใช้ตอนนี้เพื่อจัดเก็บรูปภาพที่แปลกจริง ๆ และทำการเข้าถึงแบบสุ่มต้องมีคำแนะนำเพิ่มเติม (เลื่อนขวาและ bitwise andพร้อมกับเลเยอร์ของตัวชี้ทิศทาง) แต่ฉันจะครอบคลุมประโยชน์ด้านล่าง

เลิกทำการระบบ

หนึ่งในสถานที่ที่ฉันพบว่าได้รับประโยชน์มากที่สุดจากเรื่องนี้คือระบบเลิกทำ เลิกทำรหัสระบบเคยเป็นหนึ่งในสิ่งที่ผิดพลาดได้ง่ายที่สุดในพื้นที่ของฉัน (อุตสาหกรรมภาพ FX) และไม่เพียง แต่ในผลิตภัณฑ์ที่ฉันทำงาน แต่ในผลิตภัณฑ์คู่แข่ง (ระบบเลิกทำของพวกเขายังเป็นขุย) เพราะมีหลาย ๆ ชนิดของข้อมูลที่ต้องกังวลเกี่ยวกับการเลิกทำและทำซ้ำอย่างถูกต้อง (ระบบคุณสมบัติการเปลี่ยนแปลงข้อมูลตาข่ายการเปลี่ยนแปลง shader ที่ไม่ได้อิงตามคุณสมบัติเช่นการแลกเปลี่ยนกับการเปลี่ยนแปลงลำดับชั้นของฉากเช่นการเปลี่ยนผู้ปกครองของเด็กการเปลี่ยนแปลงภาพ / พื้นผิว ฯลฯ ฯลฯ ฯลฯ )

ดังนั้นจำนวนรหัสที่ต้องเลิกทำจึงมีขนาดใหญ่มากมักจะเป็นการแข่งขันกับจำนวนของรหัสที่ใช้ระบบซึ่งระบบเลิกทำต้องบันทึกการเปลี่ยนแปลงสถานะ ด้วยการพึ่งพาโครงสร้างข้อมูลนี้ฉันสามารถทำให้ระบบเลิกทำได้เป็นอย่างนี้:

on user operation:
    copy entire application state to undo entry
    perform operation

on undo/redo:
    swap application state with undo entry

โดยปกติโค้ดข้างต้นจะไม่มีประสิทธิภาพมหาศาลเมื่อข้อมูลฉากของคุณครอบคลุมกิกะไบต์เพื่อคัดลอกทั้งหมด แต่โครงสร้างข้อมูลนี้จะคัดลอกเฉพาะสิ่งที่ไม่ได้เปลี่ยนไปเท่านั้นและทำให้ราคาถูกเพียงพอที่จะเก็บสำเนาที่ไม่เปลี่ยนรูปของสถานะแอปพลิเคชันทั้งหมด ดังนั้นตอนนี้ฉันสามารถใช้ระบบเลิกทำอย่างง่ายดายเช่นเดียวกับรหัสด้านบนและเพียงแค่มุ่งเน้นการใช้โครงสร้างข้อมูลที่ไม่เปลี่ยนรูปแบบนี้เพื่อทำการคัดลอกส่วนที่ไม่เปลี่ยนแปลงของสถานะแอปพลิเคชันที่ถูกกว่าและถูกกว่า ตั้งแต่ฉันเริ่มใช้โครงสร้างข้อมูลนี้โครงการส่วนบุคคลทั้งหมดของฉันมีระบบเลิกทำที่ใช้รูปแบบเรียบง่ายนี้

ตอนนี้ยังมีค่าใช้จ่ายอยู่ที่นี่ ครั้งสุดท้ายที่ฉันวัดมันประมาณ 10 กิโลไบต์เพียงเพื่อคัดลอกสถานะแอปพลิเคชันทั้งหมดโดยไม่ทำการเปลี่ยนแปลงใด ๆ (ซึ่งเป็นอิสระจากความซับซ้อนของฉากเนื่องจากฉากถูกจัดเรียงในลำดับชั้นดังนั้นหากไม่มีอะไรใต้รากที่เปลี่ยนแปลงเพียงราก ถูกคัดลอกตื้นโดยไม่ต้องลงไปในเด็ก) ที่อยู่ห่างจาก 0 ไบต์เป็นสิ่งจำเป็นสำหรับระบบเลิกทำที่จัดเก็บเดลตาเท่านั้น แต่ที่ 10 กิโลไบต์ของค่าใช้จ่ายในการเลิกทำต่อการดำเนินการนั้นยังคงเป็นเพียงเมกะไบต์ต่อการดำเนินงานของผู้ใช้ 100 ราย รวมทั้งฉันยังสามารถสควอชที่ลงต่อไปในอนาคตหากจำเป็น

ข้อยกเว้นความปลอดภัย

ข้อยกเว้นด้านความปลอดภัยด้วยแอปพลิเคชันที่ซับซ้อนนั้นไม่สำคัญ อย่างไรก็ตามเมื่อสถานะแอปพลิเคชันของคุณไม่เปลี่ยนรูปและคุณเพียงแค่ใช้วัตถุชั่วคราวในการพยายามทำธุรกรรมการเปลี่ยนแปลงของอะตอมมันก็มีข้อยกเว้นที่ปลอดภัยโดยเนื้อแท้เพราะถ้าส่วนใดส่วนหนึ่งของรหัสพ่นรหัสชั่วคราวจะถูกโยนทิ้งไป . ดังนั้นสิ่งเล็กน้อยที่สุดสิ่งหนึ่งที่ยากที่สุดที่ฉันพบอยู่เสมอเพื่อให้ถูกต้องในรหัสฐาน C ++ ที่ซับซ้อน

บ่อยครั้งที่คนจำนวนมากมักใช้ทรัพยากรที่สอดคล้องกับ RAII ใน C ++ และคิดว่าเพียงพอที่จะเป็นข้อยกเว้นที่ปลอดภัย บ่อยครั้งที่มันไม่ได้เป็นเพราะฟังก์ชั่นโดยทั่วไปสามารถทำให้เกิดผลข้างเคียงไปยังรัฐนอกเหนือจากที่อยู่ในขอบเขต โดยทั่วไปคุณต้องเริ่มจัดการกับขอบเขตของขอบเขตและตรรกะการย้อนกลับที่ซับซ้อนในกรณีเหล่านั้น โครงสร้างข้อมูลนี้สร้างขึ้นบ่อยครั้งฉันไม่จำเป็นต้องกังวลเพราะฟังก์ชั่นไม่ได้ก่อให้เกิดผลข้างเคียง พวกเขากำลังส่งคืนสำเนาของสถานะแอปพลิเคชันที่เปลี่ยนรูปไม่ได้แทนการเปลี่ยนสถานะของแอปพลิเคชัน

การแก้ไขแบบไม่ทำลาย

ป้อนคำอธิบายรูปภาพที่นี่

การแก้ไขแบบไม่ทำลายนั้นเป็นการทำเลเยอร์ / สแต็ค / เชื่อมต่อเข้าด้วยกันโดยไม่ต้องสัมผัสข้อมูลของผู้ใช้เดิม (เพียงแค่ข้อมูลอินพุตและข้อมูลเอาต์พุตโดยไม่ต้องสัมผัสอินพุต) โดยทั่วไปแล้วมันเป็นเรื่องเล็กน้อยที่จะนำไปใช้กับแอปพลิเคชั่นภาพอย่างง่ายเช่น Photoshop และอาจไม่ได้รับประโยชน์มากนักจากโครงสร้างข้อมูลนี้เนื่องจากการดำเนินการหลายอย่างอาจต้องการเปลี่ยนทุกพิกเซลของภาพทั้งหมด

อย่างไรก็ตามด้วยการแก้ไขแบบ non-destructive mesh การดำเนินการจำนวนมากมักต้องการแปลงเฉพาะส่วนของ mesh การดำเนินการหนึ่งอาจต้องการย้ายจุดยอดที่นี่ อีกอันอาจต้องการแบ่งรูปหลายเหลี่ยมที่นั่น ที่นี่โครงสร้างข้อมูลที่เปลี่ยนไม่ได้ช่วยให้ตันในการหลีกเลี่ยงความต้องการที่จะต้องทำสำเนาทั้งหมดของตาข่ายทั้งหมดเพียงเพื่อส่งกลับตาข่ายรุ่นใหม่ที่มีการเปลี่ยนแปลงเล็ก ๆ น้อย ๆ

การลดผลข้างเคียง

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

ตัวอย่างเช่นโดยทั่วไปสิ่งล่อใจที่จะแปลงกลุ่มตำแหน่งอาจเป็นการยอมรับเมทริกซ์และรายการของวัตถุและแปลงค่าเหล่านั้นในแบบที่ไม่แน่นอน วันนี้ฉันพบว่าตัวเองเพิ่งกลับรายการของวัตถุใหม่

เมื่อคุณมีฟังก์ชั่นอื่น ๆ เพิ่มเติมเช่นนี้ในระบบของคุณที่ไม่มีผลข้างเคียงแน่นอนทำให้ง่ายขึ้นที่จะให้เหตุผลเกี่ยวกับความถูกต้องของมันและทดสอบความถูกต้องของมัน

ประโยชน์ของการถ่ายเอกสารราคาถูก

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

ฉันยังมีโค้ดจำนวนมากที่ทำงานกับ mutables (พบว่ามันเป็นความจำเป็นในทางปฏิบัติอย่างน้อยสำหรับโค้ดระดับต่ำ) แต่สถานะแอปพลิเคชันหลักคือลำดับชั้นที่ไม่เปลี่ยนรูปเจาะลึกลงไปจากฉากที่ไม่เปลี่ยนรูป ส่วนประกอบที่ถูกกว่าบางส่วนยังคงถูกคัดลอกไว้อย่างสมบูรณ์ แต่ส่วนประกอบที่มีราคาแพงที่สุดเช่นตาข่ายและรูปภาพใช้โครงสร้างที่เปลี่ยนแปลงไม่ได้เพื่อให้สามารถคัดลอกชิ้นส่วนราคาถูกเหล่านั้นได้เพียงบางส่วนเท่านั้นที่จำเป็นต้องเปลี่ยน


0

มีคำตอบที่ดีอยู่แล้วจำนวนมาก นี่เป็นเพียงข้อมูลเพิ่มเติมที่ค่อนข้างเฉพาะเจาะจงกับ. NET ฉันกำลังขุดผ่านโพสต์บล็อกเก่า ๆ ของ. NET และพบว่ามีข้อดีจากมุมมองของผู้พัฒนา Microsoft Immutable Collections:

  1. Snapshot semantics ช่วยให้คุณแบ่งปันคอลเลกชันของคุณในแบบที่ผู้รับสามารถวางใจได้ว่าจะไม่เปลี่ยนแปลง

  2. ความปลอดภัยของเธรดโดยนัยในแอ็พพลิเคชันแบบมัลติเธรด (ไม่จำเป็นต้องล็อคเพื่อเข้าถึงคอลเลกชัน)

  3. ทุกครั้งที่คุณมีสมาชิกชั้นเรียนที่ยอมรับหรือส่งกลับประเภทคอลเลกชันและคุณต้องการรวมซีแมนทิกส์แบบอ่านอย่างเดียวในสัญญา

  4. ฟังก์ชั่นการเขียนโปรแกรมที่เป็นมิตร

  5. อนุญาตให้แก้ไขคอลเลกชันในระหว่างการแจงนับในขณะที่มั่นใจว่าคอลเลกชันดั้งเดิมจะไม่เปลี่ยนแปลง

  6. พวกเขาใช้อินเทอร์เฟซ IReadOnly * เดียวกับที่รหัสของคุณเกี่ยวข้องกับการโยกย้ายจึงเป็นเรื่องง่าย

ถ้ามีคนส่ง ReadOnlyCollection ให้คุณ IReadOnlyList หรือ IEnumerable การรับประกันเพียงอย่างเดียวคือคุณไม่สามารถเปลี่ยนข้อมูลได้ - ไม่มีการรับประกันว่าคนที่ส่งให้คุณจะไม่เปลี่ยนแปลง แต่คุณต้องมีความมั่นใจอยู่เสมอว่ามันจะไม่เปลี่ยนแปลง ประเภทเหล่านี้ไม่ได้เสนอกิจกรรมเพื่อแจ้งให้คุณทราบเมื่อมีการเปลี่ยนแปลงเนื้อหาและหากมีการเปลี่ยนแปลงอาจเกิดขึ้นในเธรดอื่นอาจเป็นไปได้ไหมในขณะที่คุณระบุเนื้อหา พฤติกรรมดังกล่าวจะนำไปสู่ความเสียหายของข้อมูลและ / หรือข้อยกเว้นแบบสุ่มในแอปพลิเคชันของคุณ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.