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. 类中的可变性

除了变量本身的可变性,类中的字段也可以使用 valvar 来定义其可变性。

来看一个包含不可变和可变字段的类:

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()

由于 colorwheels 是不可变的,下面的操作会报错 ❌:

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 仓库


原始标题:Mutability in Scala

« 上一篇: Scala Futures 指南