【问题标题】:Initialised class value evaluates to null in overridden method初始化的类值在被覆盖的方法中计算为 null
【发布时间】:2015-09-10 12:14:43
【问题描述】:

我正在寻找有关 scala 内部结构的一些见解。我们刚刚结束了痛苦的调试会话,发现我们的问题是由我们认为会预先初始化的意外空值引起的。我们无法理解为什么会这样。

这是一个非常精简的代码示例,它说明了问题(如果它看起来很复杂,那是因为它在实际代码中要复杂得多,但我已经单独留下了基本结构,以防它很重要)。

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

我的假设

我假设为了实例化 StubB,首先我们实例化 trait A,然后是 B,最后是 StubB:因此(“in A”,“in B”,“in StubB”)的打印顺序。我假设特征 A 中的 stubbableFunction 是在初始化时评估的,因为它是一个 val,与 stubbableMethod 相同。

从这里开始我会感到困惑。

我的问题

val overrideThisMethod 在特征 A 中被评估时,我希望类加载器会沿着链向下到 StubB(它确实如此,因为打印了“usefulVal = null”,你可以知道)但是......为什么是这里的值为空?如何在不首先初始化 StubB 类并因此设置 usefulVal 的情况下评估 StubB 中的 overrideThisMethod?我不知道您可以以这种方式评估“孤立”方法 - 当然方法必须属于一个必须在调用该方法之前初始化的类?

我们实际上通过将特征 A 中的 val stubbableFunction = 更改为 def stubbableFunction = 解决了这个问题,但我们仍然很想了解这里发生了什么。我期待着学习一些有趣的关于 Scala(或者可能是 Java)如何在幕后工作的东西 :)

编辑:我将 null 值更改为 var 并且发生了同样的事情 - 为响应 m-z 的回答而更新了问题

【问题讨论】:

    标签: scala


    【解决方案1】:

    我进一步剥离了原始代码,使原始行为保持不变。我还重命名了一些方法和 val 以更好地表达语义(主要是 function vs value):

    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都实现为私有字段和getter方法,字段和方法均以val命名(不遵守JavaBean约定);
    3. val 的值在构造函数中计算并用于初始化字段。

    正如 m-z 已经指出的那样,初始化自上而下运行,即首先调用父类或特征构造函数,最后调用子构造函数。所以当你打电话给new StubB()时会发生这种情况:

    1. StubB 对象分配在堆中,其所有字段根据其类型(00.0null 等)设置为默认值;
    2. A::A 作为最顶层的构造函数首先被调用;
      1. "in A" 已打印;
      2. 为了计算stubbableValue 的值overridableComputation 被调用,关键在于调用了被覆盖的方法,即StubB::overridableComputation 更多细节见What's wrong with overridable method calls in constructors?
        1. “StubB::overridableComputation”被打印;
        2. 由于usefulVal 还没有被StubB::StubB 初始化,所以使用了默认值,所以打印了“usefulVal = 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";
       }
    }
    

    如果Atrait 而不是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 的方法部分被放入接口,而字段部分被放入 trait 混入的类中。

    抽象类包含所有 trait 方法的代码,通过显式传递 $this 提供对成员字段的访问。

    【讨论】:

    • 哇,Ihor,我需要一点时间来消化这个。只是评论说 useVal 是 val 还是 var - 我看到了相同的行为。我不知道这是否会改变您的任何答案 - 我提到它是因为您在我进行编辑时添加了此答案。谢谢,我会在喝杯茶后阅读您的完整回复:)
    • var vs val 并没有真正改变任何东西。唯一改变的是 var x 会生成一个名为 x_$eq 的设置器,但这不会影响您的情况。
    • 太棒了。完美的答案。我知道这会很有趣。谢谢Ihor
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-08-23
    • 2021-06-03
    • 2020-03-18
    • 1970-01-01
    • 1970-01-01
    • 2011-02-28
    相关资源
    最近更新 更多