3

请注意,这个问题和类似的问题之前已经被问过,例如在前向引用中 - 为什么这段代码会编译?,但我找到了仍然留下一些问题的答案,所以我在这个问题上再试一次。

在方法和函数中,val关键字的作用似乎是词法的,即

def foo {
  println(bar)
  val bar = 42
}

屈服

error: forward reference extends over definition of value bar

但是,在类中,范围规则val似乎发生了变化:

object Foo {
  def foo = bar
  println(bar)
  val bar = 42
}

这不仅会编译,而且构造println函数中的0foo42

因此,方法似乎可以前向引用实例值,最终将在调用方法之前初始化(当然,除非您从构造函数调用它),以及构造函数中的语句以相同的方式转发引用值,在它们被初始化之前访问它们,从而产生一个愚蠢的任意值。

由此,出现了几个问题:

  • 为什么val在构造函数中使用它的词法编译时效果?

鉴于构造函数实际上只是一个方法,这似乎与完全 dropval的编译时效果不一致,只给它通常的运行时效果。

  • 为什么val有效地失去了声明不可变值的效果?

在不同时间访问该值可能会导致不同的结果。对我来说,这很像是编译器实现细节的泄露。

  • 合法的用例可能是什么样的?

我很难想出一个绝对需要val内部构造函数的当前语义的示例,并且不容易用适当的词法实现val,可能与lazy.

  • 一个人将如何解决这种行为val,从其他方法中使用它来取回一个习惯于使用它的所有保证?

可以推测,可以将所有 instance 声明val为是lazy为了回到val不可变的状态并产生相同的结果,无论它们如何访问,并降低在常规方法中观察到的编译时效果的相关性,但这似乎对于这种事情,对我来说就像一个非常糟糕的黑客攻击。

鉴于这种行为在实际语言中不太可能发生变化,编译器插件是否是解决此问题的正确位置,或者是否可以实现val-alike 关键字,对于刚刚花了一个小时调试由这种奇怪的,语言中更明智的语义?

4

2 回答 2

3

只是部分答案:

鉴于构造函数实际上只是一种方法......

它不是。

  • 它不返回结果,也不声明返回类型(或没有名称)
  • 不能为所述类的对象再次调用它,例如"foo".new ("bar")
  • 你不能从派生类中隐藏它
  • 你必须用“新”来称呼他们
  • 他们的名字由班级的名字固定

从语法上看,Ctor 有点像方法,它们接受参数并有一个主体,但仅此而已。

  • 为什么 val 实际上失去了声明不可变值的效果?

它没有。您必须采用不能为空的基本类型才能获得这种错觉 - 使用对象,它看起来不同:

object Foo {
  def foo = bar
  println (bar.mkString)
  val bar = List(42)
}
// Exiting paste mode, now interpreting.

defined module Foo

scala> val foo=Foo 
java.lang.NullPointerException

你不能改变一个 val 2 次,你不能给它一个不同于 null 或 0 的值,你不能把它改回来,不同的值只能用于基本类型。所以这远不是一个变量——它是一个——可能是未初始化的——最终值。

  • 合法的用例可能是什么样的?

我猜想在 REPL 中使用交互式反馈工作。您在没有显式包装对象或类的情况下执行代码。要获得这种即时反馈,不能等到(隐式)对象关闭}。因此,类/对象不会以两遍方式读取,其中首先执行所有声明和初始化。

  • 如何解决 val 的这种行为,从在其他方法中使用它来恢复所有习惯?

不要读取 Ctor 中的属性,就像您不读取 Java 中的属性一样,这些属性可能会在子类中被覆盖。

更新

Java 中也可能出现类似的问题。编译器会阻止直接访问未初始化的最终属性,但如果您通过其他方法调用它:

public class FinalCheck
{
    final int foo;
    
    public FinalCheck ()
    {
        // does not compile: 
        // variable foo might not have been initialized
        // System.out.println (foo);
        
        // Does compile - 
        bar ();

        foo = 42;       
        System.out.println (foo);
    }

    public void bar () {
        System.out.println (foo);   
    }
    public static void main (String args[])
    {
        new FinalCheck ();
    }
}

...您会看到 . 的两个值foo

0
42

我不想为这种行为辩解,而且我同意,如果编译器可以因此发出警告——在 Java 和 Scala 中,那就太好了。

于 2012-04-21T08:46:28.180 回答
2

因此,方法似乎可以前向引用实例值,最终将在调用方法之前初始化(当然,除非您从构造函数调用它),以及构造函数中的语句以相同的方式转发引用值,在它们被初始化之前访问它们,从而产生一个愚蠢的任意值。

构造函数构造函数。您正在构建对象。它的所有字段都由 JVM 初始化(基本上是归零),然后构造函数填写需要填写的任何字段。

  • 为什么 val 在构造函数中使用其词法编译时效果?

    鉴于构造函数实际上只是一种方法,完全放弃 val 的编译时效果似乎相当不一致,只给它通常的运行时效果。

我不知道你在这里说什么或问什么,但构造函数不是方法。

  • 为什么 val 实际上失去了声明不可变值的效果?

    在不同时间访问该值可能会导致不同的结果。对我来说,这很像是编译器实现细节的泄露。

它没有。如果您尝试bar从构造函数进行修改,您会发现这是不可能的。当然,在构造函数中的不同时间访问值可能会导致不同的结果。

您正在构造对象:它开始未构造,并结束构造。为了不改变它,它必须从它的最终值开始,但是如果没有人分配那个值,它怎么能做到呢?

猜猜这是谁做的?构造函数。

  • 合法的用例可能是什么样的?

我很难想出一个示例,该示例绝对需要构造函数中 val 的当前语义,并且不容易用适当的词法 val 实现,可能与惰性结合使用。

在填充值之前访问 val 是没有用例的。只是不可能找出它是否已经初始化。例如:

class Foo {
  println(bar)
  val bar = 10
}

你认为编译器能保证它没有被初始化吗?好吧,然后打开REPL,放入上面的类,然后这个:

class Bar extends { override val bar = 42 } with Foo
new Bar

并且看到它bar在打印时被初始化。

  • 如何解决 val 的这种行为,从在其他方法中使用它来恢复所有习惯?

在使用它们之前声明你的 val。但请注意,构造函数不是方法。当你这样做时:

println(bar)

在构造函数中,您正在编写:

println(this.bar)

并且this,您正在为其编写构造函数的类的对象有一个bargetter,因此它被调用。

当你对一个bar有定义的方法做同样的事情时,就没有getterthis了。bar

于 2012-04-21T18:34:15.103 回答