val-mutable กับ var-immutable ใน Scala


100

มีแนวทางใดบ้างใน Scala เกี่ยวกับเวลาที่จะใช้ val กับคอลเลกชันที่เปลี่ยนแปลงได้กับการใช้ var กับคอลเล็กชันที่ไม่เปลี่ยนรูปหรือไม่ หรือคุณควรมุ่งเป้าไปที่ val ด้วยคอลเลกชันที่ไม่เปลี่ยนรูป?

ความจริงที่ว่ามีคอลเลกชันทั้งสองประเภททำให้ฉันมีทางเลือกมากมายและบ่อยครั้งที่ฉันไม่รู้ว่าจะเลือกอย่างไร


คำตอบ:


105

คำถามที่พบบ่อยคำถามนี้ สิ่งที่ยากคือการค้นหารายการที่ซ้ำกัน

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

ตามความเป็นจริงแล้ววิธีการในพื้นที่varปลอดภัยที่สุดvarที่มีอยู่เนื่องจากมันไม่ได้หนีจากวิธีการ ถ้าวิธีสั้นยิ่งดี. หากไม่เป็นเช่นนั้นให้พยายามลดโดยการแยกวิธีอื่น

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

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


39
นีซ, ภาพใหญ่ใหม่ในใจของฉัน: ชอบimmutable valมากกว่าimmutable varมากกว่ามากกว่าmutable val mutable varโดยเฉพาะอย่างยิ่งimmutable varกว่าmutable val!
Peter Schmitz

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

1
tl; dr: ชอบvar x: Set[Int] มากกว่า val x: mutable.Set[Int]เนื่องจากถ้าคุณส่งผ่านxไปยังฟังก์ชันอื่น ๆ ในกรณีก่อนหน้านี้คุณแน่ใจว่าฟังก์ชันนั้นไม่สามารถกลายพันธุ์xให้คุณได้
pathikrit

17

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

โดยทั่วไปตรวจสอบให้แน่ใจว่าคุณไม่สับสนระหว่างการอ้างอิงและวัตถุ vals คือการอ้างอิงที่ไม่เปลี่ยนรูป (พอยน์เตอร์คงที่ใน C) นั่นคือเมื่อคุณใช้val x = new MutableFoo()คุณจะสามารถเปลี่ยนวัตถุที่xชี้ไป แต่คุณจะไม่สามารถเปลี่ยนวัตถุที่ xชี้ไปได้ var x = new ImmutableFoo()ตรงข้ามถือถ้าคุณใช้ รับคำแนะนำเริ่มต้นของฉัน: หากคุณไม่ต้องการเปลี่ยนวัตถุใดเป็นจุดอ้างอิงให้ใช้vals


1
var immutable = something(); immutable = immutable.update(x)เอาชนะวัตถุประสงค์ของการใช้คอลเล็กชันที่ไม่เปลี่ยนรูป คุณได้เลิกใช้ความโปร่งใสในการอ้างอิงแล้วและโดยปกติคุณจะได้รับผลเช่นเดียวกันจากคอลเล็กชันที่ไม่แน่นอนซึ่งมีความซับซ้อนของเวลาที่ดีกว่า จากความเป็นไปได้ทั้งสี่ ( valและvarเปลี่ยนแปลงได้และไม่เปลี่ยนรูป) สิ่งนี้มีความหมายน้อยที่สุด val mutableฉันมักจะใช้
Jim Pivarski

3
@JimPivarski ฉันไม่เห็นด้วยเช่นเดียวกับคนอื่น ๆ ดูคำตอบของ Daniel และความคิดเห็นของ Peter หากคุณต้องการอัปเดตโครงสร้างข้อมูลการใช้ var ที่ไม่เปลี่ยนรูปแทนค่าที่เปลี่ยนแปลงได้จะมีข้อได้เปรียบที่คุณสามารถรั่วไหลการอ้างอิงไปยังโครงสร้างโดยไม่เสี่ยงต่อการถูกแก้ไขโดยผู้อื่นในลักษณะที่ทำลายสมมติฐานในท้องถิ่นของคุณ ข้อเสียสำหรับ "คนอื่น" เหล่านี้คืออาจอ่านข้อมูลเก่า
Malte Schwerhoff

ฉันเปลี่ยนใจและเห็นด้วยกับคุณ (ฉันทิ้งความคิดเห็นเดิมไว้เป็นประวัติการณ์) ตั้งแต่นั้นมาฉันก็ใช้สิ่งนี้โดยเฉพาะอย่างยิ่งvar list: List[X] = Nil; list = item :: list; ...และฉันก็ลืมไปแล้วว่าฉันเคยเขียนต่างกัน
Jim Pivarski

@MalteSchwerhoff: "ข้อมูลเก่า" เป็นที่ต้องการจริงๆขึ้นอยู่กับว่าคุณออกแบบโปรแกรมของคุณอย่างไรหากความสอดคล้องเป็นสิ่งสำคัญ นี่เป็นตัวอย่างหนึ่งในหลักการพื้นฐานที่สำคัญในการทำงานพร้อมกันใน Clojure
Erik Kaplun

@ErikAllik ฉันจะไม่บอกว่าข้อมูลเก่าเป็นที่พึงปรารถนาต่อ se แต่ฉันยอมรับว่ามันสามารถใช้ได้อย่างสมบูรณ์แบบขึ้นอยู่กับการรับประกันที่คุณต้องการ / จำเป็นต้องให้กับลูกค้า หรือคุณมีตัวอย่างที่ข้อเท็จจริงเพียงอย่างเดียวของการอ่านข้อมูลเก่าเป็นข้อได้เปรียบ? ฉันไม่ได้หมายถึงผลของการยอมรับข้อมูลเก่าซึ่งอาจเป็นประสิทธิภาพที่ดีกว่าหรือ API ที่ง่ายกว่า
Malte Schwerhoff

9

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

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

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

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


1

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


-2

var immutable เทียบกับ val mutable

นอกเหนือจากคำตอบที่ยอดเยี่ยมมากมายสำหรับคำถามนี้ นี่คือตัวอย่างง่ายๆที่แสดงให้เห็นถึงอันตรายที่อาจเกิดขึ้นจากval mutable:

ออบเจ็กต์ที่เปลี่ยนแปลงได้สามารถแก้ไขได้ภายในเมธอดซึ่งใช้เป็นพารามิเตอร์ในขณะที่ไม่อนุญาตให้กำหนดใหม่

import scala.collection.mutable.ArrayBuffer

object MyObject {
    def main(args: Array[String]) {

        val a = ArrayBuffer(1,2,3,4)
        silly(a)
        println(a) // a has been modified here
    }

    def silly(a: ArrayBuffer[Int]): Unit = {
        a += 10
        println(s"length: ${a.length}")
    }
}

ผลลัพธ์:

length: 5
ArrayBuffer(1, 2, 3, 4, 10)

สิ่งนี้ไม่สามารถเกิดขึ้นได้var immutableเนื่องจากไม่อนุญาตให้มอบหมายใหม่:

object MyObject {
    def main(args: Array[String]) {
        var v = Vector(1,2,3,4)
        silly(v)
        println(v)
    }

    def silly(v: Vector[Int]): Unit = {
        v = v :+ 10 // This line is not valid
        println(s"length of v: ${v.length}")
    }
}

ผลลัพธ์ใน:

error: reassignment to val

เนื่องจากพารามิเตอร์ฟังก์ชันได้รับการปฏิบัติเนื่องจากvalไม่อนุญาตให้มีการกำหนดใหม่


สิ่งนี้ไม่ถูกต้อง สาเหตุที่คุณได้รับข้อผิดพลาดนั้นเป็นเพราะคุณใช้ Vector ในตัวอย่างที่สองของคุณซึ่งโดยค่าเริ่มต้นจะไม่เปลี่ยนรูป หากคุณใช้ ArrayBuffer คุณจะเห็นว่ามันรวบรวมได้ดีและทำสิ่งเดียวกับที่มันเพิ่งเพิ่มในองค์ประกอบใหม่และพิมพ์บัฟเฟอร์ที่กลายพันธุ์ออกมา pastebin.com/vfq7ytaD
EdgeCaseBerg

@EdgeCaseBerg ฉันตั้งใจใช้ Vector ในตัวอย่างที่สองของฉันเพราะฉันพยายามแสดงให้เห็นว่าพฤติกรรมของตัวอย่างแรกmutable valไม่สามารถทำได้ด้วยไฟล์immutable var. อะไรที่ไม่ถูกต้อง?
Akavall

คุณกำลังเปรียบเทียบแอปเปิ้ลกับส้มที่นี่ เวกเตอร์ไม่มี+=วิธีการเหมือนอาร์เรย์บัฟเฟอร์ คำตอบของคุณบ่งบอกว่า+=เหมือนกับคำตอบที่x = x + yไม่ใช่ คำสั่งของคุณที่ว่าพารามิเตอร์ของฟังก์ชันถือว่าเป็น vals นั้นถูกต้องและคุณได้รับข้อผิดพลาดที่คุณกล่าวถึง แต่เป็นเพราะคุณใช้=เท่านั้น คุณจะได้รับข้อผิดพลาดเดียวกันกับ ArrayBuffer ดังนั้นความไม่แน่นอนของคอลเลกชันที่นี่จึงไม่เกี่ยวข้องจริงๆ ดังนั้นจึงไม่ใช่คำตอบที่ดีเพราะไม่ได้รับสิ่งที่ OP กำลังพูดถึง แม้ว่าจะเป็นตัวอย่างที่ดีของอันตรายจากการส่งคอลเลกชันที่ไม่แน่นอนไปรอบ ๆ หากคุณไม่ได้ตั้งใจ
EdgeCaseBerg

@EdgeCaseBerg แต่คุณไม่สามารถทำซ้ำพฤติกรรมที่ฉันได้รับArrayBufferโดยใช้Vectorไฟล์. คำถามของ OP นั้นกว้างมาก แต่พวกเขากำลังมองหาคำแนะนำว่าควรใช้เมื่อใดดังนั้นฉันจึงเชื่อว่าคำตอบของฉันมีประโยชน์เพราะมันแสดงให้เห็นถึงอันตรายของการส่งผ่านคอลเลกชันที่ไม่แน่นอน (ความจริงที่valไม่ได้ช่วย) ปลอดภัยกว่าimmutable var mutable val
Akavall
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.