样式指南

声明

语言

类、对象和特质构造函数应全部声明在一行上,除非该行变得“太长”(约 100 个字符)。在这种情况下,将每个构造函数参数放在一行上,并使用 尾随逗号

class Person(name: String, age: Int) {
  …
}

class Person(
  name: String,
  age: Int,
  birthdate: Date,
  astrologicalSign: String,
  shoeSize: Int,
  favoriteColor: java.awt.Color,
) {
  def firstMethod: Foo = …
}

如果类/对象/特质扩展任何内容,则应用相同的通用规则,将其放在一行上,除非超过约 100 个字符,然后将每个项目放在一行上,并使用 尾随逗号;闭合括号在构造函数参数和扩展之间提供可视分隔;应添加空行以进一步将扩展与类实现分隔开

class Person(
  name: String,
  age: Int,
  birthdate: Date,
  astrologicalSign: String,
  shoeSize: Int,
  favoriteColor: java.awt.Color,
) extends Entity
  with Logging
  with Identifiable
  with Serializable {

  def firstMethod: Foo = …
}

类元素的顺序

所有类/对象/特质成员都应与换行符交错声明。此规则的唯一例外是 varval。这些可以不使用中间换行符进行声明,但前提是没有任何字段具有 Scaladoc,并且所有字段都具有简单的(最多 20 个字符左右,一行)定义

class Foo {
  val bar = 42
  val baz = "Daniel"

  def doSomething(): Unit = { ... }

  def add(x: Int, y: Int): Int = x + y
}

字段应先于作用域中的方法。唯一的例外是,如果 val 具有块定义(多个表达式)并且执行可能被视为“类方法”的操作(例如,计算 List 的长度)。在这种情况下,非平凡的 val 可以在文件中的后面声明,因为逻辑成员顺序会决定。此规则适用于 vallazy val!如果 var 声明散布在整个类文件中,则很难跟踪更改的别名。

方法

应根据以下模式声明方法

def foo(bar: Baz): Bin = expr

应以类似的方式声明具有默认参数值的方法,等号两侧各有一个空格

def foo(x: Int = 6, y: Int = 7): Int = x + y

应为所有公共成员指定返回类型。将其视为编译器检查的文档。它还有助于在类型推断发生变化时保持二进制兼容性(如果推断出返回类型,对方法实现的更改可能会传播到返回类型)。

局部方法或私有方法可以省略其返回类型

private def foo(x: Int = 6, y: Int = 7) = x + y

过程语法

避免使用(现已弃用的)过程语法,因为它往往会造成混乱,而简洁性几乎没有提升。

// don't do this
def printBar(bar: Baz) {
  println(bar)
}

// write this instead
def printBar(bar: Bar): Unit = {
  println(bar)
}

修饰符

应按以下顺序给出方法修饰符(如果每个都适用)

  1. 注释,每行一个
  2. 覆盖修饰符 (override)
  3. 访问修饰符 (protectedprivate)
  4. 隐式修饰符 (implicit)
  5. final 修饰符 (final)
  6. def
@Transaction
@throws(classOf[IOException])
override protected final def foo(): Unit = {
  ...
}

主体

当方法主体包含一个少于 30(或大约)个字符的单一表达式时,它应与方法放在一行上

def add(a: Int, b: Int): Int = a + b

当方法主体是一个长于 30(或大约)个字符但仍短于 70(或大约)个字符的单一表达式时,它应放在下一行,缩进两个空格

def sum(ls: List[String]): Int =
  ls.map(_.toInt).foldLeft(0)(_ + _)

这两种情况之间的区别有点人为。一般来说,你应根据具体情况选择更易读的样式。例如,你的方法声明可能很长,而表达式主体可能很短。在这种情况下,将表达式放在下一行可能比使声明行过长更易读。

当方法主体无法简洁地用单行表示或是非功能性质(一些可变状态,局部或其他)时,必须用大括号将主体括起来

def sum(ls: List[String]): Int = {
  val ints = ls.map(_.toInt)
  ints.foldLeft(0)(_ + _)
}

包含单个 match 表达式的方法应按如下方式声明

// right!
def sum(ls: List[Int]): Int = ls match {
  case hd :: tail => hd + sum(tail)
  case Nil => 0
}

应按如下方式声明

// wrong!
def sum(ls: List[Int]): Int = {
  ls match {
    case hd :: tail => hd + sum(tail)
    case Nil => 0
  }
}

多个参数列表

一般来说,只有在有充分理由的情况下才应使用多个参数列表。这些方法(或类似声明的函数)具有更详细的声明和调用语法,并且对于经验较少的 Scala 开发人员来说更难理解。

有三个主要原因需要这样做

  1. 对于流畅的 API

    多个参数列表允许你创建自己的“控制结构”

    def unless(exp: Boolean)(code: => Unit): Unit =
      if (!exp) code
    unless(x < 5) {
      println("x was not less than five")
    }
    
  2. 隐式参数

    使用隐式参数时,如果使用 implicit 关键字,则它适用于整个参数列表。因此,如果你只想让某些参数为隐式,则必须使用多个参数列表。

  3. 对于类型推断

    仅使用部分参数列表调用方法时,类型推断器可以在调用剩余参数列表时允许更简单的语法。考虑 fold

    def foldLeft[B](z: B)(op: (B, A) => B): B
    List("").foldLeft(0)(_ + _.length)
    
    // If, instead:
    def foldLeft[B](z: B, op: (B, A) => B): B
    // above won't work, you must specify types
    List("").foldLeft(0, (b: Int, a: String) => b + a.length)
    List("").foldLeft[Int](0, _ + _.length)
    

对于复杂的 DSL,或者对于名称很长的类型,将整个签名放在一行上可能很困难。对于这些情况,有几种不同的样式可供使用

  1. 拆分参数列表,每行一个参数,带有 尾随逗号,并且括号位于单独的行上,从而在列表之间增加视觉分隔

     protected def forResource(
       resourceInfo: Any,
     )(
       f: (JsonNode) => Any,
     )(implicit
       urlCreator: URLCreator,
       configurer: OAuthConfiguration,
     ): Any = {
       ...
     }
    
  2. 或者对齐参数列表的左括号,每行一个列表

     protected def forResource(resourceInfo: Any)
                              (f: (JsonNode) => Any)
                              (implicit urlCreator: URLCreator, configurer: OAuthConfiguration): Any = {
       ...
     }
    

高阶函数

在声明高阶函数时,值得记住 Scala 允许在调用站点为此类函数提供更简洁的语法,前提是函数参数作为最后一个参数进行柯里化。例如,这是 SML 中的 foldl 函数

fun foldl (f: ('b * 'a) -> 'b) (init: 'b) (ls: 'a list) = ...

在 Scala 中,首选样式恰好相反

def foldLeft[A, B](ls: List[A])(init: B)(f: (B, A) => B): B = ...

通过将函数参数放在最后,我们启用了如下调用语法

foldLeft(List(1, 2, 3, 4))(0)(_ + _)

此调用中的函数值没有用括号括起来;它在语法上与函数本身(foldLeft)完全脱节。这种风格因其简洁和干净而受到青睐。

字段

字段应遵循方法的声明规则,特别注意访问修饰符的顺序和注释约定。

惰性 val 应在 val 之前直接使用 lazy 关键字

private lazy val foo = bar()

函数值

Scala 为声明函数值提供了许多不同的语法选项。例如,以下声明完全等效

  1. val f1 = ((a: Int, b: Int) => a + b)
  2. val f2 = (a: Int, b: Int) => a + b
  3. val f3 = (_: Int) + (_: Int)
  4. val f4: (Int, Int) => Int = (_ + _)

在这些样式中,始终首选 (1) 和 (4)。(2) 在此示例中看起来较短,但每当函数值跨越多行(通常如此)时,此语法就会变得极其笨拙。同样,(3) 简洁,但晦涩。对于未经训练的眼睛来说,很难辨别出这甚至会产生函数值。

当专门使用样式 (1) 和 (4) 时,就很容易区分源代码中使用函数值的位置。两种样式都使用括号,因为它们在一行上看起来很干净。

间距

括号与其包含的代码之间不应有空格。花括号应与其内部的代码用一个空格间隔开,以给视觉上繁忙的花括号“喘息空间”。

多表达式函数

大多数函数值都不如上面给出的示例那么简单。许多函数值包含多个表达式。在这种情况下,将函数值拆分到多行通常更具可读性。当这种情况发生时,只应使用样式 (1),用花括号替换括号。当包含在大量代码中时,样式 (4) 变得极其难以遵循。声明本身应大致遵循方法的声明样式,其中左花括号与赋值或调用位于同一行,而右花括号位于函数最后一行紧随其后的单独一行上。参数应与左花括号位于同一行,而“箭头”(=>)也应如此

val f1 = { (a: Int, b: Int) =>
  val sum = a + b
  sum
}

如前所述,函数值应尽可能利用类型推断。

此页面的贡献者