Scala 无参方法和空括号方法区别

2017-11-30

概述

在 scala 中,如果方法不接受参数,如:

def foo(): String = "foo"

这样的方法称为空括号(empty-paren)方法。对于空括号方法,我们可以在调用时省略括号:

scala> foo()
res0: String = foo

scala> foo
res1: String = foo

除此之外,我们还可以在定义时省去括号:

def bar: String = "bar"

这样的方法称为无参(parameterless)方法。对于无参方法,调用时不能加上括号,否则会报错:

scala> bar
res2: String = bar

scala> bar()
<console>:13: error: not enough arguments for method apply: (index: Int)Char in class StringOps.
Unspecified value parameter index.
       bar()
          ^

使用惯例

在 Scala 中,如果方法不接受参数并且没有副作用,那么可以定义成无参方法;反之,尽管方法不接受参数,但是有副作用就要定义成空括号方法。当然,无参方法还有更多的作用,将在下面提到。

区别

语义区别

Scala 允许调用空括号方法时不加上括号,所以 foo() 与 foo 等价。而对于无参方法,因为定义时没有括号,那么 bar 效果等价于 foo()(不考虑返回结果),但是 bar 区别于 bar()。事实上,bar() 会被编译器翻译成 (bar).apply(),那么就是 “bar”.apply(),这其实就是调用 StringOps 的 apply 函数,如下:

final class StringOps(override val repr: String) extends AnyVal with StringLike[String] {
  ...
  override def apply(index: Int): Char = repr charAt index
  ...
}

因为 apply 接受一个 Int 参数,所以编译器就报错:

error: not enough arguments for method apply: (index: Int)Char in class StringOps. Unspecified value parameter index.

类型区别

无参方法和空括号方法在类型上也不等价,可以通过结构类型验证。如下,我们定义了两个类以及一个结构类型(基于 scala 2.12.4):

class A {
  def foo(): String = "foo"
}

class B {
  def foo: String = "foo"
}

type fooType = {
  def foo: String
}
val a = new A()
val b = new B()

然后定义一个接受 fooType 类型的函数:

def fun(fooCls: fooType): Unit =
  println(fooCls.foo)

调用 fun(b) 将输出 foo:

scala> fun(b)
foo

但是调用 fun(a) 则会报错:

scala> fun(a)
<console>:14: error: type mismatch;
 found   : a.type (with underlying type A)
 required: fooType
    (which expands to)  AnyRef{def foo: String}
       fun(a)
           ^

但是如果我们将结构类型定义为:

type fooType = {
  def foo(): String
}

那么这时调用 fun(b) 成功,但是调用 fun(a) 失败。编译器将严格区分 foo() 和 foo,认为这是两个不同类型的函数。

一个有趣的问题是编译器如何区分 foo() 和 foo 呢?如果你用 javap 查看编译后的 A.class 和 B.class,会发现它们的反编译代码中 foo() 定义是一样的:

$ javap A.class
public class A {
  public java.lang.String foo();
  public A();
}

$ javap B.class
public class B {
  public java.lang.String foo();
  public B();
}

事实上,编译器会为 A 和 B 生成两个不同的 ScalaSignature,以此区分这两个类。使用 javap -verbose 选项可以看到这两个类都有一个 ScalaSignature 的 class 文件属性:

$ javap -verbose A.class

...
Constant pool:
   #1 = Utf8               A
   #2 = Class              #1             // A
   #3 = Utf8               java/lang/Object
   #4 = Class              #3             // java/lang/Object
   #5 = Utf8               test.scala
   #6 = Utf8               Lscala/reflect/ScalaSignature;
   #7 = Utf8               bytes
...


$ javap -verbose B.class
...
Constant pool:
   #1 = Utf8               B
   #2 = Class              #1             // B
   #3 = Utf8               java/lang/Object
   #4 = Class              #3             // java/lang/Object
   #5 = Utf8               test.scala
   #6 = Utf8               Lscala/reflect/ScalaSignature;
   #7 = Utf8               bytes
...

ScalaSignature 以字节的形式存储了 Scala 在运行时需要知道的额外类型信息。可以使用 scalap 看到 A 和 B 的区别:

$ scalap A
class A extends scala.AnyRef {
  def this() = { /* compiled code */ }
  def foo(): scala.Predef.String = { /* compiled code */ }
}

$ scalap B
class B extends scala.AnyRef {
  def this() = { /* compiled code */ }
  def foo: scala.Predef.String = { /* compiled code */ }
}

无参方法的意义

柯里化

通常来说,一个函数(function)或者方法(method)(在这里不区分这两者)可以有一个或者多个参数列表,每个参数列表又可以有零个或者多个参数。这意味着下面的定义都是合法的函数(方法):

def a = ???
def b() = ???
def c()() = ???
def d(x: Int)()()(y: Int) = ???
def e()(x: Int)(y: Int)()() = ???s

Scala 引入无参方法解决柯里化一致性的问题。有了无参方法,那么 def a 效果等价于 def b()def c()()def d(x: Int)()()(y: Int) 效果等价于 def e(x: Int)()()(y: Int)()()

一个例子如下:

scala> def a = ""
a: String

scala> def b() = ""
b: ()String

scala> def c()() = ""
c: ()()String

scala> def d(x: Int)()()(y: Int) = x + y
d: (x: Int)()()(y: Int)Int

scala> def e(x: Int)()()(y: Int)()() = x + y
e: (x: Int)()()(y: Int)()()Int

scala> a == b
res0: Boolean = true

scala> b == c
res1: Boolean = true

scala> d(1)()()(2) == e(1)()()(2)
res2: Boolean = true

统一访问规则

无参方法还支持统一访问规则。父类中定义的无参方法,可以被子类使用 val/var 覆盖,如:

abstract class Parent{
  def a: Int
}

class Child extends Parent{
  override val a = 1
}

这是因为 Scala 只有两个命名空间:

  1. 值(字段/方法/包/单例)
  2. 类型(类/特质)

Scala 不允许字段和无参方法同名,允许使用 val/var 覆盖无参方法(但是不能使用无参方法覆盖 val/var)。

参考

  1. 无参方法与小括号问题
  2. Structural Types: Multiple Methods and Type Aliasing
  3. How does Scala know the difference between “def foo” and “def foo()”?
  4. Why does Scala need parameterless in addition to zero-parameter methods?
  5. scala 中的无参方法与统一访问原则
  6. Learning Scala part nine – Uniform Access