Scala 访问mixin特征的成员

Scala 访问mixin特征的成员,scala,Scala,我在scala中有以下代码: trait A { val foo: String => Int = { in => in.toInt } } trait B extends A { def bar(a: String) = { foo(a) } } class C(a: String) { self: B => val b = bar(a) } val x = new C("34") with B 在实例化x的过程中,我得到了N

我在scala中有以下代码:

trait A {
  val foo: String => Int = { in =>
    in.toInt
  }
}

trait B extends A {
  def bar(a: String) = {
    foo(a)
  }
}

class C(a: String) {
  self: B =>

  val b = bar(a)
}

val x = new C("34") with B
在实例化
x
的过程中,我得到了NPE。不知道为什么

编辑


注意:无法理解为什么
A
特征的
foo
没有被初始化

声明
A.foo
lazy val

关于为什么会出现
NullPointerException
的简短答案是,
C
的初始化需要初始化
b
,调用存储在
val foo
中的方法,此时该方法未初始化

问题是,
foo
为什么此时没有初始化?不幸的是,我无法完全回答这个问题,但我想向您展示一些实验:

如果将
C
的签名更改为
extensed B
,则
B
,因为
C
的超类之前已实例化,因此不会引发异常

其实

trait A {
  val initA = {
    println("initializing A")
  }
}

trait B extends A {
  val initB = {
    println("initializing B")
  }
}

class C(a: String) {
  self: B => // I imagine this as C has-a B
  val initC = {
    println("initializing C")
  }
}

object Main {
  def main(args: Array[String]): Unit ={
    val x = new C("34") with B
  }
}
印刷品

initializing C
initializing A
initializing B
initializing A
initializing B
initializing C

trait A {
  val initA = {
    println("initializing A")
  }
}

trait B extends A {
  val initB = {
    println("initializing B")
  }
}

class C(a: String) extends B { // C is-a B: The constructor of B is invoked before
  val initC = {
    println("initializing C")
  }
}

object Main {
  def main(args: Array[String]): Unit ={
    val x = new C("34") with B
  }
}
印刷品

initializing C
initializing A
initializing B
initializing A
initializing B
initializing C
如您所见,初始化顺序不同。我设想依赖注入
self:B=>
类似于动态导入(即,将
B
实例的字段放入
C
的范围),其组合为
B
(即
C
具有-a
B
)。我无法证明它是这样解决的,但是当使用IntelliJ的调试器进行调试时,
B
的字段在
this
下未列出,但仍在范围内

这应该回答关于为什么您会得到NPE的问题,但是关于为什么没有首先实例化mixin的问题仍然没有答案。我想不出其他情况下可能出现的问题(因为扩展特性基本上就是这样),所以这很可能是一个设计选择,或者没有人考虑过这个用例。幸运的是,这只会在实例化期间产生问题,因此最好的“解决方案”可能是在实例化期间不使用混合值(即构造函数和
val
/
var
成员)

Edit:使用
lazy val
也可以,因此您还可以定义
lazy val initC={initB}
,因为直到需要时才执行
lazy val
。但是,如果您不关心副作用或性能,我更喜欢
def
而不是
lazy val
,因为它背后的“魔力”更少

请参阅

唯一的补充是self类型使类C变得抽象。所以实际上你是这样做的:

abstract class C(a: String) {
  def bar(a: String): Int
  val b = bar(a)
} 

val x = new C("34") with B
您可以尝试在代码中替换它,并看到相同的结果。更多信息

简而言之:新C与B的线性化将为(B)。请参阅第节:

在执行超类构造函数之后,将执行每个mixin特性的构造函数。由于它们在线性化中是按从右到左的顺序执行的,但是线性化是通过反转特征的顺序创建的,这意味着mixin特征的构造函数是按照它们在类声明中出现的顺序执行的。但是,请记住,当mixin共享层次结构时,执行顺序可能与mixin在声明中的显示方式不同

在你的例子中,
新的C(“34”)和B
等于
类K扩展了C(“34”)和B;新K
。请注意,类C的self类型不影响初始化顺序

简化示例:

scala> trait C {def aa: String; println(s"C:$aa")}
defined trait C

scala> trait D {val aa = "aa"; println(s"D:$aa")}
defined trait D

scala> new C with D
C:null
D:aa
res19: C with D = $anon$1@2b740b6
解决方案:如果您的foo被放在第三方库中(因此您不能让它变懒),您可以只使用mix-in而不是self-type,或者至少将A混合到C类中:

trait A {
  val foo: String => Int = { in =>
    in.toInt
  }
}

trait B extends A {       
  def bar(a: String) = {
    foo(a)
  }
}

class C(a: String) extends A {
  self: B =>

  val b = bar(a)
}

val x = new C("34") with B

这是一个简化的例子。实际上,
trait A
在第三方库中。但我想了解力学。为什么会这样?关于类抽象性:它真的是真的吗?你有证明这一点的规范链接吗?即使这是真的,我仍然不明白为什么它不起作用:当我们用B说
newc(“34”)时,它就像我们在抽象C中自动重写
bar
方法一样。但是我们用从trait扩展的类重写它,所以trait的成员必须首先初始化。1)你可以开始阅读,但很明显,如果不实现bar方法,就无法实例化C。这个实现对于C本身来说是未知的,因为它可以在C的实例化过程中被重写2)它不是相同的-特征B只重写A,线性化仍然是B}(请参阅)。因此,在C初始化期间,成员栏是未知的。换句话说,C和B我在我的答案中添加了更多的信息;简而言之:不管C类是什么-它的自身类型不影响初始化顺序
bar
是已知的,但是foo是未知的