13

在 Scala 中, aval可以覆盖 a def,但 adef不能覆盖 a val

那么,声明一个特征是否有好处,例如:

trait Resource {
  val id: String
}

而不是这个?

trait Resource {
  def id: String
}

接下来的问题是:编译器在实践中如何处理调用 vals 和defs 以及它实际上对vals 做了什么样的优化?编译器坚持vals 是稳定的这一事实——在实践中对编译器意味着什么?假设子类实际上是idval. 在特征中将其指定为 a 是否会受到惩罚def

如果我的代码本身不需要id成员的稳定性,是否可以认为def在这些情况下始终使用 s 并仅在此处确定性能瓶颈时才切换到vals 是一种好习惯——尽管这不太可能?

4

4 回答 4

16

简短的回答:

据我所知,这些值总是通过访问器方法访问的。Usingdef定义了一个简单的方法,它返回值。Usingval定义了一个私有的 [*] final 字段,带有一个访问器方法。所以在访问方面,两者的区别非常小。差异是概念性的,def每次都会重新评估,并且val只评估一次。这显然会对性能产生影响。

[*] Java 私有

长答案:

让我们看下面的例子:

trait ResourceDef {
  def id: String = "5"
}

trait ResourceVal {
  val id: String = "5"
}

ResourceDef&产生相同的ResourceVal代码,忽略初始化器:

public interface ResourceVal extends ScalaObject {
    volatile void foo$ResourceVal$_setter_$id_$eq(String s);
    String id();
}

public interface ResourceDef extends ScalaObject {
    String id();
}

对于产生的子类(包含方法的实现),ResourceDef产生的结果与您期望的一样,注意该方法是静态的:

public abstract class ResourceDef$class {
    public static String id(ResourceDef $this) {
        return "5";
    }

    public static void $init$(ResourceDef resourcedef) {}
}

对于 val,我们只需调用包含类中的初始化程序

public abstract class ResourceVal$class {
    public static void $init$(ResourceVal $this) {
        $this.foo$ResourceVal$_setter_$id_$eq("5");
    }
}

当我们开始扩展时:

class ResourceDefClass extends ResourceDef {
  override def id: String = "6"
}

class ResourceValClass extends ResourceVal {
  override val id: String = "6"
  def foobar() = id
}

class ResourceNoneClass extends ResourceDef

在我们覆盖的地方,我们在类中获得了一个方法,它可以满足您的期望。def 是简单的方法:

public class ResourceDefClass implements ResourceDef, ScalaObject {
    public String id() {
        return "6";
    }
}

并且 val 定义了一个私有字段和访问器方法:

public class ResourceValClass implements ResourceVal, ScalaObject {
    public String id() {
        return id;
    }

    private final String id = "6";

    public String foobar() {
        return id();
    }
}

请注意,甚至foobar()不使用 field id,而是使用访问器方法。

最后,如果我们不重写,那么我们会得到一个调用 trait 辅助类中的静态方法的方法:

public class ResourceNoneClass implements ResourceDef, ScalaObject {
    public volatile String id() {
        return ResourceDef$class.id(this);
    }
}

我已经删除了这些示例中的构造函数。

因此,总是使用访问器方法。我认为这是为了避免在扩展可以实现相同方法的多个特征时出现并发症。它很快变得复杂起来。

更长的答案:

Josh Suereth在 Scala Days 2012上做了一个非常有趣的关于二元弹性的演讲,涵盖了这个问题的背景。对此的摘要是:

本次演讲的重点是 JVM 上的二进制兼容性以及二进制兼容的含义。深入描述了 Scala 中二进制不兼容机制的概要 ,然后是一组规则和指南,这些规则和指南将帮助开发人员确保他们自己的库版本既兼容二进制又具有二进制弹性

特别是,本次演讲着眼于:

  • 特征和二进制兼容性
  • Java 序列化和匿名类
  • 懒惰的 vals 的隐藏创作
  • 开发具有二进制弹性的代码
于 2012-10-30T09:11:19.667 回答
2

不同之处主要在于您可以使用 val 实现/覆盖 def 但不能相反。此外,val 只评估一次,而 def 每次使用时都会评估,在抽象定义中使用 def 将使混合 trait 的代码在如何处理和/或优化实现方面有更多自由。所以我的观点是在没有明确的理由强制使用 val 时使用 defs。

于 2012-10-29T17:04:05.133 回答
0

val表达式在变量声明时被评估一次,它是严格且不可变的。

def每次调用时都会重新评估A

于 2012-10-29T17:05:13.103 回答
-2

def按名称和val值评估。这意味着或多或少val必须始终返回一个实际值,而def更像是一个承诺,您可以在评估它时获得一个值。例如,如果你有一个函数

def trace(s: => String ) { if (level == "trace") println s } // note the => in parameter definition

仅当日志级别设置为跟踪并且您想要记录对象时才记录事件toString。如果您已经toString用一个值覆盖,那么您需要将该值传递给trace函数。但是,如果toString是 a def,则只有在确定日志级别为跟踪时才会对其进行评估,这可以为您节省一些开销。 def为您提供更大的灵活性,同时val可能更快

编译器方面,traits被编译为 java 接口,因此在 a 上定义成员时trait,如果它是 avar或. 则没有区别def。性能上的差异取决于您选择如何实现它。

于 2012-10-29T17:18:56.607 回答