scala类型类扩展/泛化初始化



我遇到了一个奇怪而令人费解的NPE。考虑以下用例:
编写一个通用算法(在我的例子中是二进制搜索),在这里你想泛化类型,但需要一些额外的功能。

例如:也许你想把一个范围减半,你需要一个通用的halftwo"consts"。

Integral类型类是不够的,因为它只提供onezero,所以我想出了:

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)
// ...
}

在运行时,这会在以下行抛出一个令人费解的NullPointerExceptionval two = tc.plus(tc.one,tc.one)
作为一种变通方法,我刚刚将lazy添加到类型类的vals中,结果如下:

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]
}

最新更新