- 在两个方法、类、对象定义之间使用一个空白行
- 优先考虑使用val,而非var。
- 当引入多个包时,使用花括号,当引入的包超过6个时,应使用通配符_:
- 在条件或循环语句表达式中,即使表达式只有一行也建议使用花括号
//避免
def square(x: Int) = x * x
//推荐
def square(x: Int) = {
x * x
}
- 常量应该用全大写字母表示,并且放在伴生对象里
class Configuration {}
object Configuration {
val DEFAULT_VALUE = 1
}
- 代码行长度
- 单行代码长度不要超过100个字符
- 作为例外,引入包名时(import)或单个URL超长时可能超过100个字符,但要尽量控制在此之内
- '30’原则
- 如果一个元素包含了超过30个子元素,可能在设计上有严重问题
- 一般情况:一个方法/函数应该少于30行代码
- 一般情况:一个类应该少于30个方法/函数
- 空格及代码缩进
- 通常使用两个空格作为代码缩进
- 函数定义:全部参数如果不能一行上展示则下一行使用四个空格作为参数缩进;返回类型与最后一行参数同一代码行或别起一行使用两个空格缩进:
def method1(
f1: String,
f2: String,
f3: String): RDD[K,V] = {
//function body
}
def method2(
f1: String,
f2: String,
f3: String)
: RDD[K,V] = {
//function body
}
- 对于一行放不下的类头部定义,关键字extends要新起一行使用两个空格的代码缩进;头部定义完后要添加一个空行以便与body明显区分开
class Foo (
val p1: String,
val p2: String,
val p3: String)
extends FooInterface
with Logging{
def method1():Unit = {}
}
- 圆括号的使用
- 函数定义应使用圆括号,除非是作为不会产生副作用(side-effect)的存取器(通常认为状态改变、IO操作为side-effect);因此建议函数定义统一使用圆括号
- 函数调用时应于声明时一致,保持圆括号有无一致性不仅是语法上的要求,对于不一致的调用,在对象返回时(隐式调用apply方法)会引起可能的错误
- 归置一个类中的代码块
- 如果一个类定义很长且包含多个方法/函数,就应对所有方法/函数按逻辑分不同的章节,使用不同的注释头来组织;但通常不建议定义很长的类,一旦有,要尝试拆分;除非是用于API接口
- 尽可能直接在函数定义的地方使用模式匹配。例如,在下面的写法中,match应该被折叠起来(collapse):
list map { item =>
item match {
case Some(x) => x
case None => default
}
}
//用下面的写法替代:
list map {
case Some(x) => x
case None => default
}
//对于函数体就是整个模式匹配表达式时,可能的情况下将match关键字放置于函数定义同一行上,这样可减少一级缩进:
def test(): Unit = msg match {
}
- 避免使用null,而应该使用Option的None。方法的返回值也要避免返回Null。应考虑返回Option,Either,或者Try
- 尽量避免使用多个参数列表;这样会复杂化操作符重载并会是Scala初学者感到困惑
- implicit也要少用
- 除非是在进行自然算术操作运算(如+、-、*、/),否则在任何情况下不要使用或自定义符号函数名称。因为除了自然算术运算,定义这些符号函数极其难懂。
- 类型推断
- 公共成员函数应该显式使用类型声明;主要避免编译器在某些情况下对类型进行错误推断
- 隐式的方法应该使用显式的类型声明;主要为了避免增量编译时造成的编译器Crash
- 没有明显类型的变量或闭包应该使用显式的类型声明 – 3秒法则:如果Code Review时无法在3秒内知悉类型信息,就应该用显式的类型声明
- return的使用
- 不要在闭包中返回。编译器会将return转换成scala.runtime.NonLocalReturnControl中的try/catch,闭包中返回有时会造成意想不到的行为
def test() : Option[Response] = {
tableFut.onComplete { table =>
if (table.isFail) {
return None //Do not do that!
} else {...}
}
}
- 使用return作为保护控制时,建议使用
def doSome(obj Any): Any {
if (obj == null) {
return null
}
//do something
}
- 使用return来尽快跳出循环,避免构建状态标识
while (true) {
...
return
}
- 递归及尾递归
- 尽可能不要使用递归,除非问题本身可以自然的用递归构建(如图、树等的遍历操作),大多数代码使用简单的循环和显式状态机更容易推理;用尾递归(和累加器)表示会更冗长、更难理解
- 鼓励使用尾递归,便通常你不好判断一个递归是否是尾递归。应用 @tailrec 标记让编译器帮助检查(很多时候你认为的尾递归不是你认为的样子)
- 隐式(implicit)
避免使用implicits,当使用implicit时,我们要确保另一个未参与编码的工程师可以理解用法的语义,而无需阅读implicit定义本身。 implicit具有非常复杂的解析规则,并使代码库极难理解。除非以下几种情况:
- 你在构建一种DSL(Domain Specific Language)
- 你在使用隐式类型参数(如:ClassTag、TypeTag)
- 为了减少冗余,在自己的私有类中使用类型转换(如:Scala或Java中的闭包)
- 异常处理
Try vs. try
- 不要捕获Throwable和Exception,使用:scala.util.control.NonFatal:
try {
...
} catch {
case NonFatal(e) =>
//scala.util.control.NonFatal 来匹配所有的非致命性异常(像内存泄漏之类的就是致命异常)
case e: InterruptedException =>
}
- 不要在API中使用Try,不要在方法中返回Try。对于异常情况执行使用显式的抛出异常,使用java风格的try/catch来做异常处理。
- Option
- 当值可能为空时,使用Option;作为与null的对比,一个Option可以显式的陈述一个API的调用结果可能为None。
- 当构建一个Option时,使用Option而非Some来防止空值
- 不要使用None来代表异常,该用异常时要显式的抛出异常
- 不要在Option上直接调用get方法,除非你绝对确信Option有值
- 一元链式调用
Scala一元链式调用异常强大,几乎一切(collection、Option、Future、Try……)都可以组合起来链式调用;但建议真实使用时还是稀疏松散的,建议:
- 链式或嵌套不要超过3个操作符
- 如果你花5秒钟以上才能弄懂逻辑,建议重新审视一元链式调用的使用,最好是找替代的表达方式;特别注意flatMap、fold等操作
- 在flatMap后,一个链通常会被打断,因为flatMap会改变输出类型
- 通过给中间结果一个变量名、明确键入变量并将其分解为更程序化的样式,通常可以使链式过程更易于理解
1. concurrent.Map
优先使用java.util.concurrent.ConcurrentHashMap而非scala.collection.concurrent.Map,除非使用的版本是scala 2.11.6以上的版本,因为之前的版本scala.collection.concurrent.Map不是原子操作。
2. Explicit Synchronization vs. Conconcurrent Collection
并发访问共享状态通常有三种推荐做法,不要将期混用,混用会造成难以推敲且容易形成死锁:
- java.util.concurrent.ConcurrentHashMap:当所有状态是在map中捕获,预计会高度竞争
- java.util.Collections.synchronizedMap:当所有状态是在map中捕获,竞争可能存在但你期望代码更安全。若真实情况下竞争不存在,JVM JIT编译器通过偏向锁定(biased locking)消除同步的开销:
- 显式的同步是对所有的关键代码片段进行同步(synchronized),可以用来保护多个变量,同样的,若真实情况下竞争不存在,JVM JIT编译器通过偏向锁定消除同步的开销
- 对于上述1和2,不要使用视图或iterator等脱离受保护区域,这种情形的发生可能不明显,比如:返回Map.keySet或Map.values。如果需要传递视图或值,需要复制一份数据
3. Explicit Synchronization vs. Atomic Variables vs. @volatile
java.util.concurrent.atomic提供了对原始类型不加锁的操作,比如:AtomicBoolean,AtomicInteger和 AtomicReference。
总是优先使用原子变量而非@volatile标记,前者具有严格的功能超集并且使用中更加明显。实际上原子变量底层仍然使用@volatile实现
在以下情况下优先使用原子变量而非显式的同步操作:
- 对于一个对象的所有关键的更新操作发生在同一个变量上,并且存在竞争;这种情况下原子变量不加锁形式能提供更高效的竞争
- 同步中很清晰的要表达getAndSet的操作
4. 私有成员变量
私有成员变量仍然可以被同一个类的其它实例所访问,这样同过this.synchronized在技术上不能充分满足同步要求;通过在成员变量前加上private[this]限定来达到要求:
// the following is still unsafe
class Foo {
private var count: Int = 0
def inc(): Unit = synchronized { count + 1 }
}
// the following is safe
class Foo {
private[this] var count: Int = 0
def inc(): Unit = synchronized { count + 1 }
}
5.隔离
一般来说,并发和同步逻辑应该尽可能的隔离和包含,这实际意味着:
- 避免在API、面向用户的函数及回调中暴露同步原语的内部结构
- 对于复杂模块,构建一个内部的小模块来捕获并发原语
对于实际绝大多数的编码,性能通常不是个极端要考虑的点。但对于性能敏感的代码来说,这里有些建议。
1. 遍历和zipWithIndex
使用while而非for循环或函数式的转换(map、foreach……)。for和函数式转换通常很慢(因为虚函数virtual function和装箱过程boxing):
2.Option和null
对性能敏感的代码,使用null而非Option,同样是因为虚函数和装箱过程;显式的将成员变量标识为null
3.Scala Collection库
对于性能敏感的代码,优先使用Java的Collection,因为Scala的通常比Java的慢。



