更新 2016/02/25:
虽然我在下面写的答案仍然足够,但也值得参考关于案例类的伴随对象的另一个相关答案。即,如何准确地重现编译器生成的隐式伴随对象,这种对象仅在定义案例类本身时发生。对我来说,结果证明是反直觉的。
摘要:
您可以在将案例类参数的值存储到案例类之前非常简单地更改它的值,同时它仍然是有效的(经过验证的)ADT(抽象数据类型)。虽然解决方案相对简单,但发现细节更具挑战性。
详细信息:
如果您想确保只能实例化您的案例类的有效实例,这是 ADT(抽象数据类型)背后的基本假设,您必须做很多事情。
例如,copy
默认情况下在案例类上提供编译器生成的方法。因此,即使您非常小心地确保仅通过显式伴随对象的apply
方法创建实例,该方法保证它们只能包含大写值,以下代码仍会生成具有小写值的案例类实例:
val a1 = A("Hi There") //contains "HI THERE"
val a2 = a1.copy(s = "gotcha") //contains "gotcha"
此外,案例类实现java.io.Serializable
. 这意味着您可以使用简单的文本编辑器和反序列化来颠覆您仅使用大写实例的谨慎策略。
因此,对于可以使用案例类的所有各种方式(善意和/或恶意),以下是您必须采取的行动:
- 对于您的显式伴随对象:
- 使用与案例类完全相同的名称创建它
- 为您的案例类创建一个
apply
签名与主构造函数完全相同的方法
- 提供一个实现,使用运算符获取案例类的实例
new
并提供一个空实现{}
- 现在,这将严格按照您的条件实例化案例类
{}
必须提供空实现,因为声明了案例类abstract
(参见步骤 2.1)
- 对于您的案例类:
- 声明它
abstract
- 防止 Scala 编译器
apply
在伴随对象中生成导致“方法被定义两次...”编译错误的方法(上面的步骤 1.2)
- 将主构造函数标记为
private[A]
- 主构造函数现在仅可用于案例类本身及其伴生对象(我们在上面的步骤 1.1 中定义的那个)
- 创建一个
readResolve
方法
- 使用 apply 方法提供一个实现(上面的步骤 1.2)
- 创建一个
copy
方法
- 将其定义为与案例类的主构造函数具有完全相同的签名
- 对于每个参数,使用相同的参数名称添加一个默认值(例如
s: String = s
:)
- 使用 apply 方法提供一个实现(下面的步骤 1.2)
这是使用上述操作修改的代码:
object A {
def apply(s: String, i: Int): A =
new A(s.toUpperCase, i) {} //abstract class implementation intentionally empty
}
abstract case class A private[A] (s: String, i: Int) {
private def readResolve(): Object = //to ensure validation and possible singleton-ness, must override readResolve to use explicit companion object apply method
A.apply(s, i)
def copy(s: String = s, i: Int = i): A =
A.apply(s, i)
}
这是实现 require (在@ollekullberg 答案中建议)并确定放置任何类型缓存的理想位置之后的代码:
object A {
def apply(s: String, i: Int): A = {
require(s.forall(_.isUpper), s"Bad String: $s")
//TODO: Insert normal instance caching mechanism here
new A(s, i) {} //abstract class implementation intentionally empty
}
}
abstract case class A private[A] (s: String, i: Int) {
private def readResolve(): Object = //to ensure validation and possible singleton-ness, must override readResolve to use explicit companion object apply method
A.apply(s, i)
def copy(s: String = s, i: Int = i): A =
A.apply(s, i)
}
如果此代码将通过 Java 互操作使用(隐藏案例类作为实现并创建一个防止派生的最终类),则此版本更安全/更健壮:
object A {
private[A] abstract case class AImpl private[A] (s: String, i: Int)
def apply(s: String, i: Int): A = {
require(s.forall(_.isUpper), s"Bad String: $s")
//TODO: Insert normal instance caching mechanism here
new A(s, i)
}
}
final class A private[A] (s: String, i: Int) extends A.AImpl(s, i) {
private def readResolve(): Object = //to ensure validation and possible singleton-ness, must override readResolve to use explicit companion object apply method
A.apply(s, i)
def copy(s: String = s, i: Int = i): A =
A.apply(s, i)
}
虽然这直接回答了您的问题,但除了实例缓存之外,还有更多方法可以围绕案例类扩展此路径。为了我自己的项目需求,我创建了一个更广泛的解决方案,我在 CodeReview(StackOverflow 姊妹网站)上记录了该解决方案。如果您最终查看、使用或利用我的解决方案,请考虑给我留下反馈、建议或问题,并且在合理范围内,我将尽我所能在一天内做出回应。