Короткий ответ на то, почему вы получите NullPointerException
в том, что инициализация C
требует инициализации b
, который вызывает метод, хранящийся в val foo
, который не инициализирован в этой точке.
Вопрос, почему foo
не инициализирован в этой точке? К сожалению, я не могу полностью ответить на этот вопрос, но я хотел бы показать вам некоторые эксперименты:
Если изменить подпись C
к extends 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
в то время как
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 A
initializing B
initializing C
Как вы можете видеть, порядок инициализации отличается. Я полагаю, что инъекция зависимостей self: B =>
будет чем-то вроде динамического импорта (то есть помещение полей экземпляра B
в объем C
) с составом B
(т. Е. C
имеет -B
). Я не могу доказать, что он решается именно так, но когда вы переходите к отладчику IntelliJ, поля B
не перечислены под this
, все еще находясь в области видимости.
Это должно ответить на вопрос о том, почему вы получаете NPE, но оставляет открытым вопрос о почему Mixin не первый экземпляр. Я не могу думать о проблемах, которые могут возникнуть в противном случае (так как расширение черты делает это в основном), поэтому это может быть либо выбор дизайна, либо никто не думал об этом случае использования. К счастью, это приведет только к проблемам во время создания экземпляра, поэтому лучшим «решением» является, вероятно, не использовать смешанные значения во время создания экземпляра (т. Е. Конструкторы и val
/var
членов).
Edit: Использование lazy val
также хорошо, так что вы можете также определить lazy val initC = {initB}
, потому что lazy val
не выполняются, пока они не нужны. Однако, если вы не заботитесь о побочных эффектах или производительности, я бы предпочел def
- lazy val
, потому что за ним меньше «магии».
о классе абстрактности: это действительно так? есть ли у вас ссылка на спецификацию, которая доказывает это? Даже если это правда, я до сих пор не понимаю, почему это не работает: когда мы говорим «новый C (« 34 ») с B', это то же самое, что мы автоматически переопределяем метод« bar »в абстрактном C. Но мы переопределяем это с классом, который простирается от черты A, поэтому сначала необходимо инициализировать элементы признака A. – maks
1) Вы можете начать чтение [здесь] (http://stackoverflow.com/questions/11274106/does-a-class-with-a-self-type-of-another-class-make-sense), но это в значительной степени очевидным - вы не можете создать экземпляр C без реализации метода bar. И эта реализация неизвестна для самого C, потому что она может быть переопределена во время создания экземпляра C – dk14
2) Это не то же самое - признак B переопределяет только A, линеаризация по-прежнему B <- A <- C. Но инициализация - {C -> A - > B} (см. Раздел [Секция инициализации класса] (http://jim-mcbeath.blogspot.ro/2009/08/scala-class- linearization.html)). Таким образом, панель элементов не известна во время инициализации C. Другими словами, C и B <- A инициализируются независимо. – dk14