我遇到了一个奇怪而令人费解的NPE。考虑以下用例:
编写一个通用算法(在我的例子中是二进制搜索),在这里你想泛化类型,但需要一些额外的功能。
例如:也许你想把一个范围减半,你需要一个通用的half
或two
"consts"。
Integral
类型类是不够的,因为它只提供one
和zero
,所以我想出了:
trait IntegralConsts[N] {
val tc: Integral[N]
val two = tc.plus(tc.one,tc.one)
val four = tc.plus(two,two)
}
object IntegralConsts {
implicit def consts[N : Integral] = new IntegralConsts[N] {
override val tc = implicitly[Integral[N]]
}
}
并使用如下:
def binRangeSearch[N : IntegralConsts]( /* irrelevant args */ ) = {
val consts = implicitly[IntegralConsts[N]]
val math = consts.tc
// some irrelevant logic, which contain expressions like:
val halfRange = math.quot(range, consts.two)
// ...
}
在运行时,这会在以下行抛出一个令人费解的NullPointerException
:val two = tc.plus(tc.one,tc.one)
作为一种变通方法,我刚刚将lazy
添加到类型类的val
s中,结果如下:
trait IntegralConsts[N] {
val tc: Integral[N]
lazy val two = tc.plus(tc.one,tc.one)
lazy val four = tc.plus(two,two)
}
但我想知道为什么我会得到这个奇怪的NPE。初始化顺序应该是已知的,并且tc
在到达val two ...
时应该已经实例化
初始化顺序应该是已知的,并且
tc
应该已经到达val two
时实例化
不符合规范。实际情况是,在构造匿名类时,首先初始化IntegralConsts[T]
,然后才会在派生的anon类中清空tc
的覆盖,这就是为什么您会遇到NullPointerException
。
规范第5.1节(模板)规定:
模板评估
考虑一个模板
sc with mt1 with mtn { stats }
。如果这是一个特性的模板,那么它的mixin评估由语句序列统计数据的评估组成。
如果这不是一个特征的模板,那么它的评估包括以下步骤:
- 首先,对超类构造函数
sc
进行求值- 然后,混合评估模板线性化中的所有基类,直到由
sc
表示的模板超类。Mixin评估以线性化中出现的相反顺序发生- 最后对语句序列CCD_ 20进行了评价
我们可以通过查看使用-Xprint:typer
:编译的代码来验证这一点
final class $anon extends AnyRef with IntegralConsts[N] {
def <init>(): <$anon: IntegralConsts[N]> = {
$anon.super.<init>();
()
};
private[this] val tc: Integral[N] = scala.Predef.implicitly[Integral[N]](evidence$1);
override <stable> <accessor> def tc: Integral[N] = $anon.this.tc
};
我们看到,首先调用super.<init>
,然后才初始化val tc
。
除此之外,让我们看看"为什么我的抽象或重写val为null?":
一个"严格"或"热切"的val是一个不标记为懒惰的val。
在缺乏"早期定义"(见下文)的情况下严格的vals按以下顺序进行:
- 超类在子类之前完全初始化
- 否则,按申报顺序
当
val
被重写时,它不会被初始化多次。。。事实并非如此:在构造超类的过程中,重写的val将显示为null,抽象的val也是如此。
我们还可以通过将-Xcheckinit
标志传递给scalac
:来验证这一点
> set scalacOptions := Seq("-Xcheckinit")
[info] Defining *:scalacOptions
[info] The new value will be used by compile:scalacOptions
[info] Reapplying settings...
[info] Set current project to root (in build file:/C:/)
> console
> :pa // paste code here
defined trait IntegralConsts
defined module IntegralConsts
binRangeSearch: [N](range: N)(implicit evidence$2: IntegralConsts[N])Unit
scala> binRangeSearch(100)
scala.UninitializedFieldError: Uninitialized field: <console>: 16
at IntegralConsts$$anon$1.tc(<console>:16)
at IntegralConsts$class.$init$(<console>:9)
at IntegralConsts$$anon$1.<init>(<console>:15)
at IntegralConsts$.consts(<console>:15)
at .<init>(<console>:10)
at .<clinit>(<console>)
at .<init>(<console>:7)
at .<clinit>(<console>)
正如您所注意到的,由于这是一个匿名类,将lazy
添加到定义中可以完全避免初始化的怪癖。另一种选择是使用早期定义:
object IntegralConsts {
implicit def consts[N : Integral] = new {
override val tc = implicitly[Integral[N]]
} with IntegralConsts[N]
}