我正在考虑创建一个值类,该值类对如何实例化有一些保护。例如,假设我想要一个非负整数:
class NonNegInt private (val value: Int) extends AnyVal
object NonNegInt {
def apply(value: Int): Try[NonNegInt] = Try {
if (value >= 0) new NonNegInt(value) else throw new IllegalArgumentException("non-negative integers only!")
}
}
我唯一担心的是私有构造函数可能会使 scala 编译器无法将NonNegInt
视为原始int
。这是真的吗?
如果这里的"视为原语"意味着"避免分配",那么这确实不起作用,但不是因为私有构造函数。
如价值等级指南中所述
此规则的另一个实例是将值类用作类型参数时。例如,即使调用标识,也必须创建实际的 Meter 实例。
def identity[T](t: T): T = t
identity(Meter(5.0))
基本上,由于 identity[T] 是参数化的,因此在值类型上调用它需要分配实例。Try[T] 是相同的情况:Try { ... }
"块"是对参数化函数的调用,Try.apply[T]
T NonNegInt
。此调用将需要分配NonNegInt
实例。
提示:
scala> implicit class X private (val i: Int) extends AnyVal { def doubled = 2 * i }
<console>:7: error: constructor X in class X cannot be accessed in object $iw
implicit class X private (val i: Int) extends AnyVal { def doubled = 2 * i }
^
这是确定的:
$ scala -optimise
Welcome to Scala version 2.11.4 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_11).
Type in expressions to have them evaluated.
Type :help for more information.
scala> :pa
// Entering paste mode (ctrl-D to finish)
class X private (val i: Int) extends AnyVal { def doubled = 2 * i }
object X { @inline def apply(i: Int) = new X(i) }
// Exiting paste mode, now interpreting.
defined class X
defined object X
scala> X(42).doubled
warning: there was one inliner warning; re-run with -Yinline-warnings for details
res0: Int = 84
您可以使用:javap -prv -
来验证是否存在分配。
但这是一个更好的技巧:
scala> case class X private (val i: Int) extends AnyVal { def doubled = 2 * i }
defined class X
scala> X(42).doubled
res1: Int = 84
scala> :javap -prv -
[snip]
public $line7.$read$$iw$$iw$();
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: invokespecial #19 // Method java/lang/Object."<init>":()V
4: aload_0
5: putstatic #21 // Field MODULE$:L$line7/$read$$iw$$iw$;
8: aload_0
9: getstatic #26 // Field $line6/$read$$iw$$iw$X$.MODULE$:L$line6/$read$$iw$$iw$X$;
12: bipush 42
14: invokevirtual #30 // Method $line6/$read$$iw$$iw$X$.doubled$extension:(I)I
17: putfield #17 // Field res1:I
20: return
脚注:
scala> case class X[A <: X[A]] private (val i: Int) extends AnyVal { def doubled = 2 * i }
defined class X
scala> X(42).doubled
res2: Int = 84
您的示例代码使您的实际问题变得模棱两可。您的示例代码将Int
包装在 Try
中。如果您不是使用 Try
,而是在 companion 对象中使用了 require
语句,那么我的理解是下面的代码会起作用(而不会失去扩展AnyVal
优惠的"原始"好处)。如果/当尝试生成负值时,这将为您提供运行时异常。该代码在扩展 AnyVal
的 case 类上使用 private
构造函数。然后,它使用 case 类的伴随对象的 apply
方法通过 require
语句强制实施运行时约束。
如果您确实需要使用 Try
包装值,则可以提供一个额外的配套对象构造函数来包装应用以捕获异常。然而,正如其他答案所指出的,当它被Try
、Option
、Either
等"包含"时,你就失去了AnyVal
"原始"的品质。
警告:下面的代码不会在 REPL/Scala 工作表中编译。扩展 AnyVal 的案例类必须是顶级类;即不能嵌套在另一个类、特征或对象的范围内。REPL 和 Scala 工作表都是通过在执行之前将所有代码推送到一个不可见的包含类来实现的。
object PositiveInt {
def apply(value: Int): PositiveInt = {
require(value >= 0, s"value [$value] must be greater than or equal to 0")
new PositiveInt(value)
}
def tryApply(value: Int): Try[PositiveInt] =
Try(apply(value))
}
case class PositiveInt private(value: Int) extends AnyVal
val positiveTestA = PositiveInt(0)
val positiveTestB = PositiveInt(1)
val positiveTestD = PositiveInt.tryApply(-1)) //returns Failure
val positiveTestD = Try(PositiveInt(-1)) //returns Failure
val positiveTestC = PositiveInt(-1) //throws required exception