将 case-类 用于可变状态(真的)不好吗?

Is it (really that) bad to use case-classes for mutable state?

考虑以下代码:

case class Vector3(var x: Float, var y: Float, var z: Float)
{
  def add(v: Vector3): Unit =
  {
    this.x += v.x
    this.y += v.y
    this.z += v.z
  }
}

如您所见,case class 保持可变状态。非常不鼓励这样做,通常我会同意并绝对坚持这一点 "rule",但这是整个故事。

我正在使用 Scala 从 scratch 编写一个小的 3d 游戏引擎。所以首先我考虑使用(更多)函数式风格,但垃圾收集器会经常启动。

想一想:我在一个测试游戏中有几十个实体。它们都有一个位置 (Vector3)、一个方向 (Vector3)、一个比例 (Vector3) 和一大堆矩阵。如果我要在这些 classes(Vector3 和 Matrix4)中发挥作用并使它们不可变,我将每帧 return 数百个新对象,导致巨大的 fps 损失,因为,让我们面对现实吧, GC 有它的用处,但在游戏引擎和 OpenGL 中……没那么多。

Vector3 以前是 class,但现在是 class,因为在代码的某处我需要对其进行模式匹配。

那么, 使用保存可变状态的 case-class 真的不好吗?

不要把它变成关于"Why do you even use Scala for a project such as that?"的讨论我知道可能有更好的选择,但我对用 C++ 编写(又一个)引擎不感兴趣,我也不太急于钻研 Rust(还)。

我会说使用具有可变状态的 case classes 是不好的,但这只是因为它们会覆盖您的 equalshashCode 方法。在您的代码中的某处,您可能会检查 a == b 是否相等并发现它们相等。后来它们可能会有所不同,因为它们是可变的。至少,将它们与基于散列的集合结合使用是危险的。

但是,您似乎并不需要案例 class 提供的所有功能。您真正需要的似乎是用于模式匹配的提取器,那么为什么不定义它呢?此外,静态工厂 apply 和可读的 toString 表示可能很方便,因此您可以实现它们。

怎么样:

class Vector (var x: Float, var y: Float, var z: Float) {
  override def toString = s"Vector($x, $y, $z)"
}

object Vector {
  def apply(x: Float, y: Float, z: Float) = new Vector(x, y, z)

  def unapply(v: Vector): Option[(Float, Float, Float)] = Some((v.x, v.y, v.z))
}