1. 概述
在 Scala 中,不可变对象和数据结构是语言设计的一等公民。这种设计理念源于不可变性在分布式系统中能有效避免错误,并天然具备线程安全特性。当然,如果确实需要,我们也可以使用可变对象。
本篇文章将深入探讨 Scala 中的可变性,包括如何定义和使用不可变与可变对象,以及一些最佳实践。
2. 定义不可变与可变变量
在 Scala 中,使用 val
声明的是不可变变量,一旦赋值就不能再次赋值,否则会编译报错 ✅:
val pi = 3.14
尝试重新赋值会报错 ❌:
pi = 4 // 编译错误
如果需要可变变量,应使用 var
声明:
var myWeight = 60
assert(myWeight == 60)
然后就可以修改变量值了:
myWeight = 65
assert(myWeight == 65)
3. 类中的可变性
除了变量本身的可变性,类中的字段也可以使用 val
和 var
来定义其可变性。
来看一个包含不可变和可变字段的类:
class ImmutabilityCar(color: String, val wheels: Int, var engine: String) {
def call(): Unit = {
println(s"This is a car with $color paint, $wheels wheels, and $engine engine")
}
}
这个类中有三种字段:
color
是私有不可变字段wheels
是公开不可变字段engine
是公开可变字段
我们创建一个实例来验证一下:
val myCar = new ImmutabilityCar("blue", 4, "diesel")
myCar.call()
由于 color
和 wheels
是不可变的,下面的操作会报错 ❌:
myCar.color = "green" // 编译错误
myCar.wheels = 5 // 编译错误
但我们可以修改 engine
字段 ✅:
myCar.engine = "electric"
assert(myCar.engine == "electric")
myCar.call()
输出结果为:
This is a car with blue paint, 4 wheels, and electric engine
4. 集合的可变性
Scala 提供了两套集合库:不可变集合和可变集合。
4.1. 不可变集合
不可变集合位于 scala.collection.immutable
包下,但它们也在 scala
包中提供了别名,因此无需额外导入即可使用。
以 Seq
为例:
val pets = Seq("Cat", "Dog")
虽然这些集合提供了看似“修改”的操作,但其实都是返回新集合,原集合保持不变 ✅:
val myPets = pets :+ "Hamster"
val notPets = pets ++ List("Giraffe", "Elephant")
val yourPets = pets.updated(0, "Mice")
assert(pets == Seq("Cat", "Dog"))
assert(myPets == Seq("Cat", "Dog", "Hamster"))
assert(notPets == Seq("Cat", "Dog", "Giraffe", "Elephant"))
assert(yourPets == Seq("Mice", "Dog"))
可以看到,pets
没有被修改,而是产生了新的集合。
4.2. 可变集合
可变集合需要从 scala.collection.mutable
包中导入。
以 ArrayBuffer
为例:
import scala.collection.mutable.ArrayBuffer
val breakfasts = ArrayBuffer("Sandwich", "Salad")
我们可以对可变集合进行添加、更新、删除操作 ✅:
添加元素:
breakfasts += "Bagels"
assert(breakfasts == ArrayBuffer("Sandwich", "Salad", "Bagels"))
breakfasts ++= Seq("PB & J", "Pancake")
assert(breakfasts == ArrayBuffer("Sandwich", "Salad", "Bagels", "PB & J", "Pancake"))
更新元素:
breakfasts.update(2, "Steak")
assert(breakfasts == ArrayBuffer("Sandwich", "Salad", "Steak", "PB & J", "Pancake"))
删除元素(如果存在):
breakfasts -= "PB & J"
assert(breakfasts == ArrayBuffer("Sandwich", "Salad", "Steak", "Pancake"))
breakfasts -= "Fried rice" // 不存在,不会报错
assert(breakfasts == ArrayBuffer("Sandwich", "Salad", "Steak", "Pancake"))
⚠️ 注意:像 Array
这样的集合,虽然可变,但不能增删元素,只能更新已有元素:
val lunches = Array("Pasta", "Rice", "Hamburger")
lunches.update(0, "Noodles")
assert(lunches sameElements Array("Noodles", "Rice", "Hamburger"))
5. 可变性的最佳实践
在函数式编程中有一个重要概念叫 引用透明性(referential transparency)。
如果一个表达式可以被其值替换而不影响程序行为,那它就是引用透明的。这正是不可变性的优势所在 ✅。
不可变对象天然支持并发安全,因为多个线程访问同一个不可变对象时不会产生竞争条件。
⚠️ 但也要注意:过度使用不可变集合可能导致性能问题,因为每次“修改”都生成新对象,不如直接修改来的高效。
因此,在性能敏感场景下,可以合理使用可变集合,但要控制好作用域,避免并发问题。
6. 总结
本文主要讲解了 Scala 中的可变性,包括:
- 如何定义可变和不可变变量
- 类中字段的可变性控制
- 不可变与可变集合的使用方式
- 可变性的最佳实践
不可变性是 Scala 的核心特性之一,合理使用能提升程序的健壮性和并发安全性,但在性能敏感场景下也不必死守不可变,适度使用可变结构是完全可以接受的。
示例代码已上传至 GitHub 仓库。