【问题标题】:Field in a trait is not initialized in time特征中的字段未及时初始化
【发布时间】:2013-05-20 19:18:35
【问题描述】:

我不明白为什么在调用类构造函数时,以下代码中的字段 encryptKey 没有初始化为 3:

trait Logger{
  println("Construction of Logger")
  def log(msg: String) { println(msg) }
}

trait EncryptingLogger extends Logger {
  println("Construction of EncryptingLogger")
  val encryptKey = 3

  override def log(msg: String){
    super.log(msg.map(encrypt(_, encryptKey)))
  }

  def encrypt(c: Char, key: Int) =
    if (c isLower) (((c - 'a') + key) % 26 + 'a').toChar
    else if (c isUpper) (((c.toInt - 'A') + key) % 26 + 'A').toChar
    else c
}

class SecretAgent (val id: String, val name: String) extends Logger {
  println("Construction of SecretAgent")
  log("Agent " + name + " with id " + id + " was created.")
}

val bond = new SecretAgent("007", "James Bond") with EncryptingLogger

在我的理解中,创建对象的线性化应该是:

SecretAgent -> EncryptingLogger -> Logger -> ScalaObject

并且构造顺序是从右到左,这意味着变量应该在SecretAgent的构造函数开始之前已经初始化。但是prinln告诉我不同​​:

scala> val bond = new SecretAgent("007", "James Bond") with EncryptingLogger
Construction of Logger
Construction of SecretAgent
Agent James Bond with id 007 was created.
Construction of EncryptingLogger
bond: SecretAgent with EncryptingLogger = $anon$1@49df83b5

我试图以不同的方式混合相同的特征:

class SecretAgent (val id: String, val name: String) extends Logger with EncryptingLogger

并及时初始化变量:

scala> val bond = new SecretAgent("007", "James Bond")
Construction of Logger
Construction of EncryptingLogger
Construction of SecretAgent
Djhqw Mdphv Erqg zlwk lg 007 zdv fuhdwhg.
bond: SecretAgent = SecretAgent@1aa484ca

那么在类定义中混合和在对象中混合有什么区别,有人可以解释一下吗?

【问题讨论】:

    标签: scala


    【解决方案1】:

    这里的问题是,在调用超级构造函数之前,您在类的构造函数中调用了一个方法,该方法访问了超类/特征的字段。解决此问题的最简单方法是使该字段变得惰性,因为它将在首次访问时对其进行评估:

    trait Logger{
      def log(msg: String) { println(msg) }
    }
    
    trait EncryptingLogger extends Logger {
      lazy val encryptKey = 3
    
      override def log(msg: String){
        super.log(msg.map(encrypt(_, encryptKey)))
      }
    
      def encrypt(c: Char, key: Int) =
        if (c isLower) (((c - 'a') + key) % 26 + 'a').toChar
        else if (c isUpper) (((c.toInt - 'A') + key) % 26 + 'A').toChar
        else c
    }
    
    class SecretAgent (val id: String, val name: String) extends Logger {
      log("Agent " + name + " with id " + id + " was created.")
    }
    
    
    scala> val bond = new SecretAgent("007", "James Bond") with EncryptingLogger
    Djhqw Mdphv Erqg zlwk lg 007 zdv fuhdwhg.
    bond: SecretAgent with EncryptingLogger = $anon$1@4f4ffd2f
    

    编辑

    当我们查看反编译的java代码时,这里发生了什么就很清楚了

    class Foo extends SecretAgent("007", "James Bond") with EncryptingLogger
    

    构造函数如下所示

    public Foo() { 
      super("007", "James Bond");
      EncryptingLogger.class.$init$(this);
    }
    

    如您所见,它首先调用调用 log 方法的超级构造函数(在本例中为 SecretAgent),然后调用EncryptionLoggerinit 方法。因此encryptKey 仍然有它的默认值,即0 表示整数。

    如果您现在使 encryptKey 字段变得惰性,getter 将如下所示:

    public int encryptKey() {
      return this.bitmap$0 ? this.encryptKey : encryptKey$lzycompute();
    }
    

    在第一次访问时,它会调用以下方法将字段设置为其值:

    private int encryptKey$lzycompute() {
      synchronized (this) {
        if (!this.bitmap$0) {
          this.encryptKey = EncryptingLogger.class.encryptKey(this);
          this.bitmap$0 = true;
        }
        return this.encryptKey;
      }
    }
    

    edit2

    为了回答您关于构造函数顺序的问题,它实际上以正确的顺序调用构造函数。当你创建一个匿名实例时,它实际上就像

    class Anonymous extends SecretAgent with EncryptingLogger
    

    在这种情况下,SecretAgent 的构造函数将首先被调用。如果您使用 trait 扩展类 SecretAgent,它将首先调用超级构造函数。它的行为完全符合预期。因此,如果您想将特征用作匿名实例的 mixin,则必须注意初始化顺序,这有助于使可能在构造函数中访问的字段变得惰性。

    【讨论】:

    • 嗨,你能解释一下为什么超类不在子类之前构造吗?根据线性化规则,应先初始化EncryptingLogger。
    • 是的,很明显字节码在两种情况下是不同的。主要问题是为什么线性化规则在这种情况下不起作用。
    • 感谢您的解释。我需要一些时间来消化它:)
    【解决方案2】:

    使用 extends

    class SecretAgent (val id: String, val name: String) extends EncryptingLogger
    val bond = new SecretAgent("007", "James Bond");
    

    在上述场景中,来自特征的属性通过继承添加到SecretAgent 类中。平原老直接继承。所以当你尝试实例化一个对象时,“钩子”已经创建好了。

    使用 with

    虽然它在这种情况下产生类似的输出,但withextends 是不同的运算符。 with 用于混入,这意味着您的特征的属性以与使用继承不同的方式添加到对象中。例如,如果您不为每个实例添加with EncryptionLogger,则并非所有SecretAgent 对象都将具有encryptKey

    要隐式指定所有SpecialAgent 对象都继承自EncryptingLogger,请使用extends

    时机

    即使在仔细阅读 Martin Odersky 的书 (p 217 - 221) 之后,也没有提到 mixin 被惰性求值或某种形式的延迟解析被应用于 mixin。我还运行了您的两个示例,它们都产生了相同的输出:

    println(bond.encryptKey); // 3
    

    【讨论】:

    • 当你运行第一个例子(混入对象)时,你会得到不同的构造顺序:Logger -> SecretAgent -> EncryptinAgent。我更改了代码并添加了 printlns 来显示这个
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-07-19
    • 2013-10-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-07-27
    • 1970-01-01
    相关资源
    最近更新 更多