Scala编程规范(中文翻译)

原文链接:SCALA STYLE GUIDE

1 缩进(Indentation)

每一级缩进使用2个空格。不使用制表符(Tab)。因此,不要像这样缩进:

// 错误!
class Foo {
    def fourspaces = {
        val x = 4
        ..
    }
}

你应该像这样缩进:

// 正确!
class Foo {
  def twospaces = {
    val x = 2
    ..
  }
}

Scala语言鼓励使用数量惊人的嵌套域和逻辑块(函数值等等)。帮自己一个忙,不要因为打开了一个新的区块而在语法上惩罚自己。来自Java的这种风格确实需要一点时间来适应,但是非常值得。

1.1 换行(Line wrapping)

有时候,一个表达式的长度达到了难以理解的程度,就是为了将其限制在一行以内(通常该行的长度超过了80个字符)。在这种情况下,首选的方法是通过将中间结果赋值,从而将表达式分割成多个表达式。然而,这并不总是一个实际可行的解决方案。

当一定需要将表达式分为多行时,每个后续行应该以第一行为基准,缩进两个空格。还请记住,Scala要求每个“换行”要么有一个非封闭的圆括号,要么以一个无右边参数的中缀方法结束,比如:

val result = 1 + 2 + 3 + 4 + 5 + 6 +
  7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 +
  15 + 16 + 17 + 18 + 19 + 20

如果没有使用这种换行方法,Scala将推断在换行行尾存在一个分号,有时甚至会在没有警告的情况下取消编译。

1.2 具有多个参数的方法(Methods with Numerous Arguments)

当调用一个包含许多参数(5个或更多)的方法时,通常需要将方法调用封装到多行中。在这种情况下,将每个参数单独放在一行上,从当前缩进级别缩进两个空格:

foo(
  someVeryLongFieldName,
  andAnotherVeryLongFieldName,
  "this is a string",
  3.1415)

这样所有参数都对齐了,即便稍后更改方法的名称,也不需要重新对齐它们。

应该非常小心地避免一类多参数方法的调用缩进写法。更具体地说,当必须将每个参数缩进超过50个空格才能实现对齐时,应该避免这种调用缩进写法。在这种情况下,调用本身应该移到下一行并缩进两个空格:

// 正确!
val myLongFieldNameWithNoRealPoint =
  foo(
    someVeryLongFieldName,
    andAnotherVeryLongFieldName,
    "this is a string",
    3.1415)

// 错误!
val myLongFieldNameWithNoRealPoint = foo(someVeryLongFieldName,
                                         andAnotherVeryLongFieldName,
                                         "this is a string",
                                         3.1415)

更好的做法是,尽量避免任何需要超过两个或三个参数的方法!


2 命名约定(Naming Conventions)

一般来说,Scala使用“驼峰大小写”命名。也就是说,除了第一个单词外,其他每个单词首字母都大写:

UpperCamelCase
lowerCamelCase

首字母缩略词应视为正常单词:

Xhtml
maxId

而不是:

XHTML
maxID

命名中的下划线实际上并不是编译器所禁止的,但是由于它们在Scala语法中具有特殊的含义,因此强烈不鼓励使用下划线(例外情况请参见下文)。

2.1 类/特质(Classes/Traits)

类的名称应该用大写驼峰:

class MyFairLady

这模拟了类的Java命名约定。

2.2 对象(Objects)

对象名称类似于类名称(大写驼峰)。例外情况是模仿包或函数。这不是很常见。示例:

object ast {
  sealed trait Expr

  case class Plus(e1: Expr, e2: Expr) extends Expr
  ...
}

object inc {
  def apply(x: Int): Int = x + 1
}

2.3 包(Packages)

Scala包应该遵循Java包命名约定:

// 错误!
package coolness

// 正确! 仅将coolness._ 放在域内
package com.novell.coolness

// 正确! 将novell._ 和coolness._都放在域内
package com.novell
package coolness

// 正确, 用于com.novell.coolness包对象
package com.novell
/**
 * 提供与“coolness”相关的类
 */
package object coolness {
}

2.3.1 根(root)

有时需要使用_root_对导入进行完全限定。例如,如果另一个net在作用域内,则访问net.liftweb,我们必须这样写:

import _root_.net.liftweb._

不要过度使用_root_。通常,嵌套包解析是一件好事,对于减少导入混乱非常有帮助。使用_root_不仅否定了它们的优点,而且本身也引入了额外的混乱。

2.4 方法

方法的文本(字母)名称应使用小写驼峰:

def myFairMethod = ...

本节并不全面介绍Scala中的惯用方法命名。更多信息可以在方法调用部分找到。

2.4.1 访问器/存储器(Accessors/Mutators)

Scala不遵循Java约定的预挂接set / get 存储器和访问器方法。相反使用以下约定:

  • 对于属性的访问器,方法的名称应该是属性的名称。
  • 在某些情况下,在布尔访问符前加上“is”是可以接受的(例如isEmpty)。只有在没有提供相应的存储器时才会出现这种情况。请注意,Lift公约附加“_?”布尔访问器是不标准的,不能在Lift框架之外使用。
  • 对于存储器,方法的名称应该是附加“_=”的属性的名称。只要在封闭类型上定义了具有该特定属性名称的对应访问器,该约定就会启用与之对应的存储器。注意,这不仅是一种约定,而且是语言的一种要求。
class Foo {

  def bar = ...

  def bar_=(bar: Bar) {
    ...
  }

  def isBaz = ...
}

val foo = new Foo
foo.bar                 // 访问器
foo.bar = bar2      // 存储器
foo.isBaz             // 布尔属性

不幸的是,这些约定与Java约定相冲突,Java约定根据访问器和存储器所表示的属性来命名由访问器和存储器封装的私有字段。例如:

public class Company {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

在Scala中,字段和方法之间没有区别。实际上,字段完全由编译器命名和控制。如果我们想采用Scala中bean getter /setter的Java约定,这是一个相当简单的编码:

class Company {
  private var _name: String = _

  def name = _name

  def name_=(name: String) {
    _name = name
  }
}

虽然匈牙利表示法非常难看,但它确实具有消除_name变量歧义而不混淆标识符的优点。下划线位于前缀而不是后缀的位置,以避免误输入name _而不是name_的危险。由于大量使用Scala的类型推断,这样的错误可能会导致非常混乱的错误。

注意,Java getter/setter范式通常用于解决缺乏对属性和绑定的第一类支持的问题。在Scala中,有一些库支持属性和绑定。约定是对包含自己的getter和setter的属性类使用不可变引用。例如:

class Company {
  val string: Property[String] = Property("Initial Value")

2.4.2 括号(Parentheses)

与Ruby不同,Scala重视方法是否使用括号声明(仅适用于无参数方法)。例如:

def foo1() = ...

def foo2 = ...

这些是编译时的不同方法。虽然可以使用或不使用圆括号调用foo1,但是不能使用圆括号调用foo2

因此,对于何时适合声明没有括号的方法以及何时不适合声明没有括号的方法,必须遵守适当的准则,这实际上非常重要。

作为任何类型的访问器(封装字段或逻辑属性)的方法都应该不使用括号声明,除非它们有副作用。而Ruby和Lift使用的是!为了说明这一点,最好使用圆括号(请注意,出于语法的考虑,流式API和DSL往往会违反下面给出的准则。这种例外不应被认为是一种违反,甚至当这些规则不适用的时候。在DSL中,语法应该高于约定)。

此外,调用点应遵循声明;如果使用圆括号声明,则使用圆括号调用。虽然有节省一些字符的诱惑,但是如果您遵循这条原则,您的代码将更具可读性和可维护性。

// 不会改变状态,以birthdate方式调用
def birthdate = firstName

// 更新了内部状态,以age()方式调用
def age() = {
  _age = updateAge(birthdate)
  _age
}

2.4.3 符号化方法名(Symbolic Method Names)

避免!尽管Scala在很大程度上促进了API设计的这一领域,但是不应该轻率地定义具有符号化名称的方法,特别是当符号本身是非标准的时候(例如,>>#>>)。一般来说,符号方法名有两个有效的使用场景:

  • DSL(比如:actor1 ! Msg
  • 逻辑数学运算(比如:a + bc :: d

在前一种情况下,符号方法名可以不受惩罚地使用,只要语法是有益的。但是,在标准API设计过程中,符号方法名应该严格保留给纯函数操作。因此,定义用于连接两个单元(monad)的>>=方法是可以接受的,但是定义用于写入输出流的<<方法是不可接受的。前者在数学上定义明确,没有副作用,而后者两者都不是。

作为一般规则,符号化方法名在本质上应该被很好地理解和自注释。经验法则如下:如果您需要解释方法的功能,那么它应该有一个真实的、描述性的名称,而不是符号。在一些非常罕见的情况下,可以接受创建新的符号方法名称。很可能,您的API不是这种情况!

具有符号化名称的方法定义应该被认为是Scala的一个高级特性,只有那些最熟悉其缺陷的人才能使用。如果不小心,过度使用符号方法名很容易将最简单的代码转换成符号汤。

2.5 常量,值,变量和方法(Constants,Values,Variable and Methods)

常量名称应该使用大写驼峰。与Java的static final成员类似,如果成员是final的、不可变的,并且属于包对象或对象,则可以将其视为常量:

object Container {
  val MyConstant = ...
}

scala.math包中的Pi是常量的例子。

方法名、值名和变量名应小写:

val myValue = ...
def myMethod = ...
var myVariable

2.6 类型参数(泛型)(Type Parameter(generics))

对于简单的类型参数,应该使用单个大写字母(来自英语字母表),以A开头(这与Java约定的以T开头不同)。

class List[A] {
  def map[B](f: A => B): List[B] = ...
}

如果类型参数具有更具体的含义,则应使用描述性名称,并遵循类命名约定(与全大写风格相反):

// 正确
class Map[Key, Value] {
  def get(key: Key): Value
  def put(key: Key, value: Value): Unit
}

// 错误:不要使用全大写
class Map[KEY, VALUE] {
  def get(key: KEY): VALUE
  def put(key: KEY, value: VALUE): Unit
}

如果类型参数的作用域足够小,可以使用助记符来代替较长的描述性名称:

class Map[K, V] {
  def get(key: K): V
  def put(key: K, value: V): Unit
}

2.6.1 高级类型和参数化类型参数(Higher-Kinds and Parameterized Type parameters)

从理论上讲,高级类型与常规类型参数没有什么不同(除了它们的类型至少是*=>*而不是简单的*)。命名约定大致相似,但为了清晰起见,最好使用描述性名称,而不是单个字母:

class HigherOrderMap[Key[_], Value[_]] { ... }

用单字母形式来表示一个代码库中使用的基本概念,如F[_]表示函子(Functor),M[_]表示单字(Monad),是可以接受的。

在这种情况下,基本概念应该是团队所熟知和理解的东西,或者有第三方说明,如以下

def doSomething[M[_]: Monad](m: M[Int]) = ...

这里,类型绑定:Monad提供了必要的证据来通知读者M[_]是Monad的类型。

2.7 注解(Annotation)

注解,比如@volatile,应该用小写驼峰:

class cloneable extends StaticAnnotation

整个Scala库都使用这种约定,尽管它与Java注释命名不一致。

注意:即使在注解上使用类型别名,这种约定也适用。例如,当使用JDBC时:

type id = javax.persistence.Id @annotation.target.field
@id
var id: Int = 0

2.8 关于简洁的注意事项(Special Note on Brevity)

由于Scala源于函数语言,所以本地名称很短是很正常的:

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

这在Java这样的语言中是不好的实践,但在Scala中是很好的实践。这种约定之所以有效,是因为适当编写的Scala方法非常短,只跨越一个表达式,很少超过几行。很少使用本地名称(包括参数),因此没有必要设计很长,但具有描述性的名称。这种约定极大地提高了大多数Scala源代码的简洁性。这反过来又提高了可读性,因为大多数表达式适合在一行中,方法的参数具有描述性类型名称。

这个约定只适用于非常简单方法的参数(以及非常简单类的局部字段);公共接口中的所有内容都应该是描述性的。还要注意,参数的名称现在是类的公共API的一部分,因为用户可以在方法调用中使用命名参数。


3 类型(Types)

3.1 推断(Inference)

在可能的情况下使用类型推断,但要把清晰放在首位,并在公共API中支持明确性。

您几乎不应该注释私有字段或局部变量的类型,因为它们的类型通常会立即在它们的值中显示出来:

private val name = "Daniel"

但是,您可能希望仍然显示具有复杂或不明显形式的已赋值值(assigned value)得类型。

所有公共方法都应该有显式的类型注释。在这些情况下,类型推断可能会破坏封装,因为它依赖于内部方法和类细节。在没有显式类型的情况下,对方法或val的内部进行更改可以在没有警告的情况下更改类的公共API,这可能会破坏客户机代码。显式类型注释还可以帮助改进编译时间。

3.1.1 函数值(Function Values)

函数值支持类型推断的特殊情况,值得单独调用:

val ls: List[String] = ...
ls map (str => str.toInt)

在Scala已经知道我们声明的函数值类型的情况下,不需要注释参数(在本例中是str)。这是一个非常有用的推论,应该尽可能首选。注意,对函数值操作的隐式转换将使此推断无效,从而强制显式注释参数类型。

3.2 注解(Annotation)

类型注解应该根据下面的模板进行模式化:

value: Type

这是Scala标准库和Martin Odersky所有示例所采用的样式。值和类型之间的空格有助于眼睛准确地解析语法。之所以将冒号放在值的末尾而不是类型的开头,是为了避免在这种情况下混淆:

value :::

这实际上是有效的Scala代码,表示声明一个类型为::的值。显然,前缀样式的注释冒号会把事情搞得一团糟。

3.3 归因(Ascription)

类型归因常常与类型注解混淆,因为Scala中的语法是相同的。下面是归因的例子:

  • Nil: List[String]
  • Set(values: _*)
  • "Daniel": AnyRef

因为类型检查器的缘故,归因基本上只是在编译时执行的向上转换。它的使用并不常见,但有时也会发生。归因最常见的情况是使用单个Seq参数调用varargs方法。这是通过赋予_*类型来实现的(如上面的第二个例子所示)。

类型归因遵循类型注解约定;冒号后面有空格。

3.4 函数(Functions)

函数类型应该在参数类型、箭头和返回类型之间声明一个空格:

def foo(f: Int => String) = ...

def bar(f: (Boolean, Double) => List[String]) = ...

应该尽可能省略括号(例如一个参数的方法,Int => String)。

3.4.1 参数数量-1(Arity-1)

Scala有一种特殊的语法来声明Arity-1函数的类型。例如:

def map[B](f: A => B) = ...

具体来说,圆括号可以从参数类型中省略。因此,我们没有将f声明为类型(A) => B,因为这样会不必要地冗长。考虑一个更极端的例子:

// 错误!
def foo(f: (Int) => (String) => (Boolean) => Double) = ...

// 正确!
def foo(f: Int => String => Boolean => Double) = ...

通过省略括号,我们节省了6个字符,并显著提高了类型表达式的可读性。

3.5 结构类型(Structural Types)

如果结构类型长度小于50个字符,则应在一行中声明它们。否则,它们应该被分割成多行,并(通常)分配给它们自己的类型别名:

// 错误!
def foo(a: { def bar(a: Int, b: Int): String; val baz: List[String => String] }) = ...

// 正确!
private type FooParam = {
  val baz: List[String => String]
  def bar(a: Int, b: Int): String
}

def foo(a: FooParam) = ...

更简单的结构类型(少于50个字符)可以内联声明和使用:

def foo(a: { val bar: String }) = ...

在内联声明结构类型时,每个成员之间应该用分号和空格分隔,左大括号后面应该跟着空格,而右大括号前面应该跟着空格(如上面的两个例子所示)。

结构类型在运行时使用反射实现,其性能天生低于名义类型。开发人员应该更喜欢使用名义类型,除非结构类型提供了明显的好处。


4 嵌套块(Nested Blocks)

4.1 大括号(Curly Braces)

开大括号({)必须与它们所表示的声明位于同一行:

def foo = {
  ...
}

从技术上讲,Scala的解析器确实支持GNU风格的表示法,在声明之后的行上使用大括号。但是,由于分号推断的实现方式,在处理这种样式时解析器的可预测性不是很好。只要遵循上面演示的花括号约定,就可以省去许多麻烦。

4.2 括号(Parentheses)

在少数情况下,当括号块换行时,开括号和闭括号应该是不间隔的,通常保持在同一行的内容(Lisp风格):

(this + is a very ++ long *
  expression)

圆括号还可以禁用分号推断,因此允许开发人员使用一些更喜欢的操作符开始行:

(  someCondition
|| someOtherCondition
|| thirdCondition
)

出于美观的原因,在本例中,可以接受下面一行的尾随圆括号。


5 申明(Declaration)

5.1 类(Classes)

Class/Object/Trait构造函数应该全部在一行中声明,除非行变得“太长”(大约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 = …
}

如果一个class/object/trait扩展了任何东西,应用同样的一般规则,把它放在一行上,除非它超过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 = …
}

5.1.1 类元素的顺序(Ordering of Class Elements)

所有class/object/trait成员都应该用换行声明。这条规则的唯一例外是varval,它们可以在没有插入换行符的情况下声明,但是只有当没有一个字段具有Scaladoc,并且所有字段都具有简单的定义(最多20个字符一行)时才可以声明:

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

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

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

域(scope)中字段应该放在的方法之前。唯一的例外是如果val有一个块定义(多于一个表达式),并且执行可能被认为是“类似方法”的操作(例如计算List的长度)。在这种情况下,非平凡的val可以在稍后的文件中声明,作为逻辑成员排序的指示。这条规则只适用于vallazy val!如果var声明散布在整个类文件中,跟踪不断变化的别名就变得非常困难。

5.1.2 方法

方法应该按照下面的模式声明:

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

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

局部方法或私有方法可能会省略它们的返回类型:

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

5.1.2.1 过程语法(Procedure Syntax)

避免过程语法,因为它容易使人混淆,从而在简短性方面收效甚微。

// 不要这样做
def printBar(bar: Baz) {
  println(bar)
}

// 建议这样写
def printBar(bar: Bar): Unit = {
  println(bar)
}

5.1.2.2 修饰符(Modifiers)

方法修饰符应该按照以下顺序给出(当每种方法都适用时):

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

5.1.2.3 方法体(Body)

当一个方法体包含一个小于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表达式的方法应按以下方式声明:

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

别这样:

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

5.1.2.4 多参数列表(Multiple Parameter Lists)

通常,如果有充分的理由,您应该只使用多个参数列表。这些方法(或类似声明的函数)具有更详细的声明和调用语法,经验较少的Scala开发人员更难理解。

您应该这样做的主要原因有三个:

  1. 为了API的流畅性

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

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

当使用隐式参数时,您使用implicit关键字,它适用于整个参数列表。因此,如果只希望某些参数是隐式的,则必须使用多个参数列表。

  1. 用于类型推断

当仅使用一些参数列表调用方法时,类型推断器可以在调用其余参数列表时允许使用更简单的语法。考虑折叠:

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) => a + b.length)
List("").foldLeft[Int](0, _ + _.length)

对于复杂的DSL,或者具有较长的类型名,很难将整个签名放在一行中。在这些情况下,对齐参数列表的开括号,每行一个列表(也就是说,如果不能将所有列表都放在一行中,那么每行一个):

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

5.1.2.5 高阶函数(Higher-Order Functions)

在声明高阶函数时,值得注意的是,当函数参数作为最后一个参数时,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)完全断开。这种款式因简洁干净而受到青睐。

5.1.3字段(Fields)

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

惰性求值val应该在val之前直接使用Lazy关键字:

private lazy val foo = bar()

5.2 函数值(Function Values)

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

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

在这些样式中,(1)和(4)总是首选的。(2)在本例中较短,但是当函数值跨越多行时(通常是这种情况),这种语法就变得非常笨拙。同样,(3)简洁,但迟钝。对于未经训练的眼睛来说,很难理解这甚至产生了一个函数值。

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

5.2.1 空格(Spacing)

括号和它们所包含的代码之间不应该有空格。
花括号应该与其中的代码间隔一个空格,以给视觉繁忙的花括号“喘息的空间”。

5.2.2 多表达函数(Multi-Expression Functions)

大多数函数值都没有上面给出的例子那么简单。许多包含多个表达式。在这种情况下,将函数值分割成多行通常更具可读性。当发生这种情况时,应该只使用风格(1),用大括号替换圆括号。当包含大量代码时,风格(4)变得非常难以遵循。声明本身应该松散地遵循方法的声明样式,左大括号与赋值或调用位于同一行,而右大括号位于函数的最后一行之后的自己的行上。参数应该与左大括号位于同一行,“箭头”也应该位于同一行(=>)

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

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


6 控制结构(Control Structures)

所有控制结构都应该在定义关键字后面加上空格:

// 正确!
if (foo) bar else baz
for (i <- 0 to 10) { ... }
while (true) { println("Hello, World!") }

// 错误!
if(foo) bar else baz
for(i <- 0 to 10) { ... }
while(true) { println("Hello, World!") }

6.1 花括号(Curly-Braces)

如果控制结构表示纯功能操作,并且控制结构的所有分支(与if/else相关)都是单行表达式,则应该省略花括号。请记住以下准则

  • if - 如果有else子句,则省略大括号。否则,即使内容只有一行,也要用花括号包围内容。
  • while - 永远不要省略括号(while不能以纯函数的方式使用)。
  • for - 如果有yield子句,则省略大括号。否则,即使内容只有一行,也要用大括号将内容包围起来。
  • case - 总是在case子句中省略大括号。
val news = if (foo)
  goodNews()
else
  badNews()

if (foo) {
  println("foo was true")
}

news match {
  case "good" => println("Good news!")
  case "bad" => println("Bad news!")
}

6.2 推导式(Comprehensions)

Scala能够使用多个生成器(通常是多个<-符号)表示for推导式。在这种情况下,有两种可供选择的语法:

// wrong!
for (x <- board.rows; y <- board.files)
  yield (x, y)

// right!
for {
  x <- board.rows
  y <- board.files
} yield (x, y)

虽然后一种样式更冗长,但通常认为它更容易阅读,而且更“可伸缩”(这意味着它不会随着理解的复杂性增加而变得模糊)。对于多个生成器的所有for推导式,您应该更喜欢这种形式。只有一个生成器的理解(例如for (i <- 0 to10) yield i)应该使用第一种形式(圆括号而不是大括号)。

这个规则的例外是缺少yield子句时的for推导式。在这种情况下,构造实际上是一个循环,而不是函数推导式,通常将生成器串在括号之间比使用语法混乱的} {构造更具可读性:

// 错误!
for {
  x <- board.rows
  y <- board.files
} {
  printf("(%d, %d)", x, y)
}

// 正确!
for (x <- board.rows; y <- board.files) {
  printf("(%d, %d)", x, y)
}

最后,for推导式更优于对mapflatMap,和filter的链式调用,因为这很难读(这是增强for推导式的目的之一)。

6.3 平凡条件(Trivial Conditionals)

在某些情况下,创建一个简短的if/else表达式用于在较大的表达式中嵌套使用是很有用的。在Java中,这类情况通常由三元运算符(?/:)处理,这是Scala缺少的一种语法设备。在这些情况下(实际上在任何时候你有一个非常简短的if/else表达式),允许把“then”和“else”分支放在ifelse关键字相同的行上:

val res = if (foo) bar else baz

这里的关键是可读性不会因为将两个分支都内联到if/else中而受到阻碍。注意这种风格不应该与命令式if表达式一起使用,也不应该使用花括号。


7 方法调用(Method Invocation)

一般来说,Scala中的方法调用遵循Java约定。换句话说,调用目标和点(.)之间不应该有空格,点和方法名之间也不应该有空格,方法名和参数分隔符(圆括号)之间也不应该有空格。每个参数应该用逗号()后面的空格隔开:

foo(42, bar)
target.foo(42, bar)
target.foo()

从2.8版开始,Scala现在支持命名参数。方法调用中的命名参数应被视为规则参数(在逗号后面相应地间隔),等号两边各有一个空格:

foo(x = 6, y = 7)

虽然这种样式确实在命名参数和变量赋值方面造成了视觉上的模糊,但是另一种方法(等号周围没有空格)会导致代码非常难以阅读,特别是对于实值的非平凡表达式。

7.1 无参数(Arity-0)

Scala允许在无参数方法上省略括号(没有参数):

reply()

// 等同于

reply

但是,只有当所讨论的方法没有副作用(纯函数)时,才应该使用这种语法。换句话说,在调用queue.size时省略括号是可以接受的。但在调用println()时不是。这个约定反映了上面给出的方法声明约定。

虔诚地遵守这一约定将极大地提高代码的可读性,并使其更容易理解任何给定方法的最基本操作。不要为了节省两个字符而省略圆括号!

7.1.1 中缀记号(Infix Notation)

Scala有一个特殊的无标点语法,用于调用带有一个参数的方法。许多Scala程序员使用这种符号命名方法:

// 推荐
a + b

// 合法,但可读性弱
a+b

// 合法,但很奇怪
a.+(b)

但几乎所有字母命名的方法都要避免使用它:

// 推荐
names.mkString(",")

// 有时也会看到;有争议的
names mkString ","

灰色区域是短的、类似操作符式的方法,如max,特别有可交换性:

// 很常见
a max b

接受多个参数的符号方法(它们确实存在!)仍然可以使用中缀表示法调用,用空格分隔:

foo ** (bar, baz)

然而,这种方法相当少见,通常在API设计期间应该避免使用。例如,应该避免使用/::\方法,而应该使用它们更知名的名称foldLeftfoldRight

7.1.2 后缀记号(Postfix Notation)

Scala允许使用后缀表示法调用不带参数的方法:

// 推荐
names.toList

// 不鼓励
names toList

这种样式不安全,不应该使用。由于分号是可选的,因此编译器将尝试将其视为中缀方法(如果可能的话),可能从下一行中提取一个术语。

names toList
val answer = 42        // 将不会编译!

8 文件

通常,文件应该包含一个逻辑编译单元。我所说的“逻辑”指的是类、特质或对象。这个准则的一个例外是,类或特质具有伴生对象。应该在同一个文件中使用对应的类或特质对伴生对象进行分组。这些文件应该根据它们所包含的类、特质或对象来命名:

package com.novell.coolness

class Inbox { ... }

// 半生对象
object Inbox { ... }

这些编译单元应该放在com/novell/coolness目录下,名为Inbox.scala的文件中。简而言之,应该首选Java文件命名和定位约定,尽管Scala在这方面允许更大的灵活性。

8.1 多单元文件(Multi-Unit Files)

不管上面说了什么,但是在一些重要的情况下,需要在一个文件中包含多个编译单元。一个常见的例子是一个封闭的特质和几个子类(通常模拟功能语言中可用的ADT语言特性):

sealed trait Option[+A]

case class Some[A](a: A) extends Option[A]

case object None extends Option[Nothing]

由于密封超类(和traits)的性质,所有子类型必须包含在同一个文件中。因此,这种情况绝对可以作为应该忽略对单个单元文件的首选项的实例。

另一种情况是,多个类逻辑上形成一个具有内聚性的组,共享概念,将它们包含在一个文件中,从而极大地促进了维护。这些情况比前面提到的密封超类型异常更难预测。一般来说,如果在一个文件中对多个单元执行长期维护和开发比在多个单元中执行更容易,那么这些类应该首选这种组织策略。但是,请记住,当一个文件中包含多个单元时,当需要进行更改时,通常很难找到特定的单元。

所有多单元文件都应该使用驼峰命名,并以小写字母开头。
这是一个非常重要的约定。它将多文件与单个单元文件区别开来,极大地简化了查找声明的过程。这些文件名可能基于它们包含的重要类型(例如option.scala对于上面的例子),或者描述所有单元共享的逻辑属性(例如,ast.scala)。


9 Scaladoc

为所有包、类、特质、方法和其他成员提供文档是很重要的。Scaladoc通常遵循Javadoc的约定,但是提供了许多额外的特性,可以简化Scala代码的文档编写。

一般来说,比起格式,你更关心内容和写作风格。Scaladocs需要对代码的新用户和经验丰富的用户有用。实现这一点非常简单:在编写过程中增加细节和解释的层次,从一个简短的摘要开始(对于有经验的用户很有用,可以作为参考),同时在详细部分中提供更深入的示例(经验丰富的用户可以忽略这些示例,但是对于新手来说是非常宝贵的)。

Scaladoc工具不要求文档注释样式。

下面的示例演示了三种常见的缩进样式中的单行摘要和详细文档。

Javadoc风格:

/**
 * Provides a service as described.
 *
 * This is further documentation of what we're documenting.
 * Here are more details about how it works and what it does.
 */
def member: Unit = ()

Scaladoc风格,排水沟星号对齐在第二列:

/** Provides a service as described.
 *
 *  This is further documentation of what we're documenting.
 *  Here are more details about how it works and what it does.
 */
def member: Unit = ()

Scaladoc风格,排水沟星号对齐在第三列:

/** Provides a service as described.
  *
  * This is further documentation of what we're documenting.
  * Here are more details about how it works and what it does.
  */
def member: Unit = ()

由于注释标记对空格敏感,工具必须能够推断出左边的空白。

当只需要简单、简短的描述时,可以使用一行格式:

/** Does something very simple */
def simple: Unit = ()

注意,与Javadoc约定相反,Scaladoc样式中的文本从注释的第一行开始。这种格式节省了源文件中的垂直空间。

在任何一种Scaladoc样式中,所有的文本行都对齐在第5列上。由于Scala源代码通常由两个空格缩进,所以文本以一种视觉上令人愉悦的方式与源代码缩进对齐。

有关格式化Scaladoc的更多技术信息,请参见Scaladoc获取库作者

9.1 一般性规范(General Style)

与Scaladoc保持一致的样式非常重要。同样重要的是,要将Scaladoc瞄准那些不熟悉您的代码的用户和只需要快速参考的经验丰富的用户。以下是一些一般的指导方针:

  • 尽可能快地说到点子上。例如,说“returns true if some condition”而不是“if some condition return true”。
  • 将方法的第一句话格式化为“Returns XXX”,如在“Returns the first element of the List”中,与“this method Returns”或“get the first”等相反。方法通常返回直接需要的东西。
  • 这同样适用于类;省略“This class do XXX”;就说" do XXX "
  • 使用方括号语法创建指向引用的Scala库类的链接,例如[[Scala . option]]
  • @return注释中总结方法的返回值,为主Scaladoc留下更长的描述。
  • 如果方法的文档是该方法返回内容的一行描述,则不要使用@return注释重复它。
  • 记录方法做了什么,而不是方法应该做什么。换句话说,“returns the result of applying f to x”return the result of applying f to x”。细微的,但是很重要。
  • 当引用类的实例时,使用“this XXX”或“this”,而不是“the XXX”。对于对象,说“this object”。
  • 使代码示例与本指南一致。
  • 尽可能使用wiki样式的语法而不是HTML。
  • 根据需要,示例应该使用完整的代码清单或REPL(包含REPL代码的最简单方法是在REPL中开发示例并将其粘贴到Scaladoc中)。
  • 自由使用@macro来引用需要特殊格式化的常见重复值。

9.2 包(Packages)

为每个包提供Scaladoc。它放在包的目录下,一个名为package.scala的文件中。它看起来像这样(parent.package.name.mypackage):

package parent.package.name

/** This is the Scaladoc for the package. */
package object mypackage {
}

包的文档应该首先记录哪些类是包的一部分。其次,记录包对象本身提供的一般内容。

虽然包文档不需要是关于使用包中的类的完整教程,但它应该提供主要类的概述,以及一些如何使用包中的类的基本示例。确保使用方括号符号引用类:

package my.package
/** Provides classes for dealing with complex numbers.  Also provides
 *  implicits for converting to and from `Int`.
 *
 *  ==Overview==
 *  The main class to use is [[my.package.complex.Complex]], as so
 *  {{{
 *  scala> val complex = Complex(4,3)
 *  complex: my.package.complex.Complex = 4 + 3i
 *  }}}
 *
 *  If you include [[my.package.complex.ComplexConversions]], you can
 *  convert numbers more directly
 *  {{{
 *  scala> import my.package.complex.ComplexConversions._
 *  scala> val complex = 4 + 3.i
 *  complex: my.package.complex.Complex = 4 + 3i
 *  }}}
 */
package complex {}

9.3 类,对象,以及特质(Classes,Objects,and Traits)

记录所有类、对象和特质。Scaladoc的第一句话应该提供类或trait的功能概述。用@tparam记录所有类型参数。

9.3.1 类(Classes)

如果一个类应该使用它的同伴对象来创建,那么在类的描述之后就这样指出(尽管构造的细节留给同伴对象)。不幸的是,目前没有办法内联地创建指向伴生对象的链接,但是生成的Scaladoc将在类文档输出中为您创建一个链接。

如果应该使用构造函数创建类,则使用@constructor语法记录它:

/** A person who uses our application.
 *
 *  @constructor create a new person with a name and age.
 *  @param name the person's name
 *  @param age the person's age in years
 */
class Person(name: String, age: Int) {
}

根据类的复杂性,提供一个常见用法的示例。

9.3.2 对象(Objects)

由于对象可以用于各种目的,所以记录如何使用对象(例如作为工厂,用于隐式方法)是很重要的。如果这个对象是其他对象的工厂,请在这里这样表示,将apply方法的细节推迟到Scaladoc。如果你的对象没有使用apply作为工厂方法,一定要注明实际的方法名:

/** Factory for [[mypackage.Person]] instances. */
object Person {
  /** Creates a person with a given name and age.
   *
   *  @param name their name
   *  @param age the age of the person to create
   */
  def apply(name: String, age: Int) = {}

  /** Creates a person with a given name and birthdate
   *
   *  @param name their name
   *  @param birthDate the person's birthdate
   *  @return a new Person instance with the age determined by the
   *          birthdate and current date.
   */
  def apply(name: String, birthDate: java.util.Date) = {}
}

如果您的对象包含隐式转换,请在Scaladoc中提供一个例子:

/** Implicit conversions and helpers for [[mypackage.Complex]] instances.
 *
 *  {{{
 *  import ComplexImplicits._
 *  val c: Complex = 4 + 3.i
 *  }}}
 */
object ComplexImplicits {}

9.3.3 特质(Traits)

在概述特质的功能之后,提供必须在混合使用特质的类中指定的方法和类型的概述。如果有已知的类使用这个特质,那么引用它们。

9.4 方法和其他成员(Methods and Other Members)

文档记录所有方法。与其他可文档化实体一样,第一句话应该是方法功能的总结。后面的句子解释得更详细。记录每个参数以及每个类型参数(使用@tparam)。对于柯里化函数,请考虑提供有关预期用法或惯用用法的更详细示例。对于隐式参数,要特别注意解释这些参数来自何处,以及用户是否需要做额外的工作来确保这些参数可用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容