2015-09-10 2 views
0

Я ищу некоторое представление о внутренних компонентах scala. Мы только что вышли на другую сторону болезненного сеанса отладки и выяснили, что наша проблема была вызвана неожиданным нулевым значением, которое, как мы думали, было бы предварительно инициализировано. Мы не можем понять, почему это так.Инициализированное значение класса оценивается как null при переопределенном методе

Ниже приведен пример примера, который иллюстрирует проблему (если он выглядит запутанным, потому что он намного сложнее в реальном коде, но я оставил основную структуру в том случае, если она значительна).

trait A { 
    println("in A") 

    def usefulMethod 

    def overrideThisMethod = { 
    //defaultImplementation 
    } 

    val stubbableFunction = { 
    //do some stuff 

    val stubbableMethod = overrideThisMethod 

    //do some other stuff with stubbableMethod 
    } 
} 

class B extends A { 
    println("in B") 

    def usefulMethod = { 
    //do something with stubbableFunction 
    } 
} 

class StubB extends B { 
    println("in StubB") 

    var usefulVar = "super useful" //<<---this is the val that ends up being null 

    override def overrideThisMethod { 
    println("usefulVar = " + usefulVar) 
    } 
} 

Если мы пнуть цепочки инициализации, это то, что выводится на консоль:

scala> val stub = new StubB 
in A 
usefulVar = null 
in B 
in StubB 

Мои предположения

Я полагаю, что для того, чтобы создать экземпляр Стубб, первый мы создаем характеристику A, а затем B и, наконец, StubB: следовательно, порядок печати («в A», «в B», «в StubB»). Я предполагаю, что stubbableFunction в признаке A оценивается при инициализации, потому что это значение val, то же самое для stubbableMethod.

Отсюда я смущен.

Мой вопрос

Когда val overrideThisMethod оценивается в признака А, я бы ожидать, что загрузчик классов следовать цепи вниз к Стубб (что он делает, вы можете сказать из-за печатанием «usefulVal = нуль»), но ... почему здесь значение null? Как можно оценить overrideThisMethod в StubB без предварительной инициализации класса StubB и, следовательно, установить usefulVal? Я не знал, что вы могли бы оценивать методы «осиротевших» таким образом - конечно, методы должны принадлежать классу, который должен быть инициализирован, прежде чем вы сможете вызвать метод?

Мы на самом деле решили проблему, изменив val stubbableFunction = на def stubbableFunction = в черте A, но нам все равно хотелось бы понять, что здесь происходит. Я с нетерпением жду возможности узнать что-то интересное о том, как Scala (или, может быть, Java) работает под капотом :)

Редактирование: Я изменил значение null на var и произошло то же самое - вопрос обновлен для ясности в ответ на Ответ mz

ответ

1

Я убрал оригинальный код еще больше, оставив оригинальное поведение неповрежденным. Я также переименован некоторые методы и Vals, чтобы выразить семантику лучше (в основном функций против значения):

trait A { 
    println("in A") 

    def overridableComputation = { 
    println("A::overridableComputation") 
    1 
    } 

    val stubbableValue = overridableComputation 

    def stubbableMethod = overridableComputation 
} 

class StubB extends A { 
    println("in StubB") 

    val usefulVal = "super useful" //<<---this is the val that ends up being null 

    override def overridableComputation = { 
    println("StubB::overridableComputation") 
    println("usefulVal = " + usefulVal) 
    2 
    } 
} 

При запуске он дает следующий результат:

in A 
StubB::overridableComputation 
usefulVal = null 
in StubB 
super useful 

Вот некоторые реализации Scala детали, чтобы помочь нам понять, что происходит:

  1. главный конструктор переплетается с классом определение, т.е.большая часть кода (кроме определения метода) между фигурными фигурными скобками помещается в конструктор;
  2. каждый класс val реализован как частное поле и метод получения, и поле и метод имеют имя после val (соглашение JavaBean не соблюдается);
  3. значение для val вычисляется внутри конструктора и используется для инициализации поля.

Как уже отмечалось, инициализация выполняется сверху вниз, то есть сначала создается класс или конструктор родительского элемента, конструктор потомка вызывается последним. Итак, вот что происходит, когда вы звоните new StubB():

  1. StubB объекта выделяется в куче, все его поля установлены значения по умолчанию в зависимости от их типа (0, 0.0, null и т.д.);
  2. A::A вызывается сначала как самый верхний конструктор;
    1. "in A" печатается;
    2. , чтобы вычислить значение для stubbableValueoverridableComputation, улов фактически является тем, что вызывается переопределенный метод, то есть StubB::overridableComputation см. What's wrong with overridable method calls in constructors? для более подробной информации;
      1. «StubB :: overridableComput» печатается;
      2. начиная с usefulVal еще не инициализировано StubB::StubB используется значение по умолчанию, поэтому печатается «полезноVal = null»;
      3. 2 возвращается;
    3. stubbableValue Инициализировано с вычисленной стоимостью 2;
  3. StubB::StubB вызывается как следующий конструктор в цепи;
    1. "in StubB" печатается;
    2. рассчитано значение для usefulVar, в данном случае используется буква "super useful";
    3. usefulVar инициализируется значением "super useful".

Поскольку значение stubbableValue вычисляется во время запуска конструктора

Чтобы доказать эти предположения fernflower Java декомпилятор можно использовать. Вот как выше Scala выглядит код, когда декомпилирован на Java (я удалил ненужные @ScalaSignature аннотаций):

import scala.collection.mutable.StringBuilder; 

public class A { 
    private final int stubbableValue; 

    public int overridableComputation() { 
     .MODULE$.println("A::overridableComputation"); 
     return 1; 
    } 

    public int stubbableValue() { 
     return this.stubbableValue; 
    } 

    public int stubbableMethod() { 
     return this.overridableComputation(); 
    } 

    public A() { 
     .MODULE$.println("in A"); 
     // Note, that overridden method is called below! 
     this.stubbableValue = this.overridableComputation(); 
    } 
} 

public class StubB extends A { 
    private final String usefulVal; 

    public String usefulVal() { 
     return this.usefulVal; 
    } 

    public int overridableComputation() { 
     .MODULE$.println("StubB::overridableComputation"); 
     .MODULE$.println(
     (new StringBuilder()).append("usefulVal = ") 
          .append(this.usefulVal()) 
          .toString() 
    ); 
     return 2; 
    } 

    public StubB() { 
     .MODULE$.println("in StubB"); 
     this.usefulVal = "super useful"; 
    } 
} 

В случае A является trait вместо class кода является немного более многословным, но поведение согласуется с class A вариант.Поскольку JVM не поддерживает множественное наследование Scala компилятор разбивает trait в абстрактном вспомогательный класс, который содержит только статические элементы и интерфейс:

import scala.collection.mutable.StringBuilder; 

public abstract class A$class { 
    public static int overridableComputation(A $this) { 
     .MODULE$.println("A::overridableComputation"); 
     return 1; 
    } 

    public static int stubbableMethod(A $this) { 
     return $this.overridableComputation(); 
    } 

    public static void $init$(A $this) { 
     .MODULE$.println("in A"); 
     $this.so32501595$A$_setter_$stubbableValue_$eq($this.overridableComputation()); 
    } 
} 

public interface A { 
    void so32501595$A$_setter_$stubbableValue_$eq(int var1); 

    int overridableComputation(); 

    int stubbableValue(); 

    int stubbableMethod(); 
} 

public class StubB implements A { 
    private final String usefulVal; 
    private final int stubbableValue; 

    public int stubbableValue() { 
     return this.stubbableValue; 
    } 

    public void so32501595$A$_setter_$stubbableValue_$eq(int x$1) { 
     this.stubbableValue = x$1; 
    } 

    public String usefulVal() { 
     return this.usefulVal; 
    } 

    public int overridableComputation() { 
     .MODULE$.println("StubB::overridableComputation"); 
     .MODULE$.println(
     (new StringBuilder()).append("usefulVal = ") 
          .append(this.usefulVal()) 
          .toString() 
    ); 
     return 2; 
    } 

    public StubB() { 
     A$class.$init$(this); 
     .MODULE$.println("in StubB"); 
     this.usefulVal = "super useful"; 
    } 
} 

Помните, что val оказывается в поле и метод? Поскольку несколько признаков могут быть смешаны с одним классом, черта не может быть реализована как класс. Поэтому часть метода val помещается в интерфейс, а часть поля помещается в класс, в который попадает признак.

Абсолютный класс содержит код всех методов признака, доступ к полям участников предоставляется путем передачи $this явно.

+0

wow Ihor, мне нужно взять litle, чтобы переварить это. Просто комментарий, чтобы сказать, что useVal является val или var - я вижу такое же поведение. Я не знаю, повлияет ли это на любой из ваших ответов - я упоминаю об этом, потому что вы добавили этот ответ во время редактирования. Спасибо, я прочитаю ваш полный ответ, как только я получу чашку чая. – moncheery

+0

'var' vs' val' ничего не меняет. Единственное, что меняется, это то, что для 'var x' генерируется сеттер с именем' x_ $ eq', но это не влияет ни на что в вашем случае. –

+0

Awesome. Отличный ответ. Я знал, что это будет что-то интересное. Спасибо, Игорь – moncheery

Смежные вопросы