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

时间:2017-10-29 05:54:10

标签: scala nullpointerexception typeclass

我遇到了一个奇怪且令人费解的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添加到类型类' val,这一切都成功了:

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 ...应该已经实例化了

1 个答案:

答案 0 :(得分:2)

  

初始化顺序应该是已知的,tc应该已经存在   到达val two

时实例化

不符合规范。真正发生的是,在构建匿名类时,首先IntegralConsts[T]将被初始化,只有才会在派生的anon类中撤消tc的覆盖,这是为什么你会遇到NullPointerException

specification section §5.1 (Templates)说:

  

模板评估

     

考虑一个模板sc with mt1 with mtn { stats }

     

如果这是特质的模板,那么它的mixin评估包括对语句序列统计的评估。

     

如果这不是特质的模板,那么它的评估包括以下步骤:

     
      
  • 首先,评估超类构造函数sc
  •   
  • 然后,模板的线性化直到由sc表示的模板超类的所有基类进行混合评估。混合评估在线性化中以相反的顺序发生。
  •   
  • 最后评估语句序列stats
  •   

我们可以通过使用-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被初始化。

除此之外,我们来看看&#34; Why is my abstract or overridden val null?&#34;:

  

'严格'或'渴望'的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]
}