类
类、对象和特质构造函数应全部声明在一行上,除非该行变得“太长”(约 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 = …
}
类元素的顺序
所有类/对象/特质成员都应与换行符交错声明。此规则的唯一例外是 var
和 val
。这些可以不使用中间换行符进行声明,但前提是没有任何字段具有 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
可以在文件中的后面声明,因为逻辑成员顺序会决定。此规则仅适用于 val
和 lazy 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)
}
修饰符
应按以下顺序给出方法修饰符(如果每个都适用)
- 注释,每行一个
- 覆盖修饰符 (
override
) - 访问修饰符 (
protected
、private
) - 隐式修饰符 (
implicit
) - final 修饰符 (
final
) 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 开发人员来说更难理解。
有三个主要原因需要这样做
-
对于流畅的 API
多个参数列表允许你创建自己的“控制结构”
def unless(exp: Boolean)(code: => Unit): Unit = if (!exp) code unless(x < 5) { println("x was not less than five") }
-
隐式参数
使用隐式参数时,如果使用
implicit
关键字,则它适用于整个参数列表。因此,如果你只想让某些参数为隐式,则必须使用多个参数列表。 -
对于类型推断
仅使用部分参数列表调用方法时,类型推断器可以在调用剩余参数列表时允许更简单的语法。考虑 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,或者对于名称很长的类型,将整个签名放在一行上可能很困难。对于这些情况,有几种不同的样式可供使用
-
拆分参数列表,每行一个参数,带有 尾随逗号,并且括号位于单独的行上,从而在列表之间增加视觉分隔
protected def forResource( resourceInfo: Any, )( f: (JsonNode) => Any, )(implicit urlCreator: URLCreator, configurer: OAuthConfiguration, ): Any = { ... }
-
或者对齐参数列表的左括号,每行一个列表
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 为声明函数值提供了许多不同的语法选项。例如,以下声明完全等效
val f1 = ((a: Int, b: Int) => a + b)
val f2 = (a: Int, b: Int) => a + b
val f3 = (_: Int) + (_: Int)
val f4: (Int, Int) => Int = (_ + _)
在这些样式中,始终首选 (1) 和 (4)。(2) 在此示例中看起来较短,但每当函数值跨越多行(通常如此)时,此语法就会变得极其笨拙。同样,(3) 简洁,但晦涩。对于未经训练的眼睛来说,很难辨别出这甚至会产生函数值。
当专门使用样式 (1) 和 (4) 时,就很容易区分源代码中使用函数值的位置。两种样式都使用括号,因为它们在一行上看起来很干净。
间距
括号与其包含的代码之间不应有空格。花括号应与其内部的代码用一个空格间隔开,以给视觉上繁忙的花括号“喘息空间”。
多表达式函数
大多数函数值都不如上面给出的示例那么简单。许多函数值包含多个表达式。在这种情况下,将函数值拆分到多行通常更具可读性。当这种情况发生时,只应使用样式 (1),用花括号替换括号。当包含在大量代码中时,样式 (4) 变得极其难以遵循。声明本身应大致遵循方法的声明样式,其中左花括号与赋值或调用位于同一行,而右花括号位于函数最后一行紧随其后的单独一行上。参数应与左花括号位于同一行,而“箭头”(=>
)也应如此
val f1 = { (a: Int, b: Int) =>
val sum = a + b
sum
}
如前所述,函数值应尽可能利用类型推断。