Scala 編程風格指南[Databricks ]

Databricks Scala 編程風格指南

聲明 (Disclaimer)

The Chinese version of the Databricks Scala Guide is contributed and maintained by community member Hawstein. We do not guarantee that it will always be kept up-to-date.html

本文檔翻譯自 Databricks Scala Guide,目前由 Hawstein 進行維護。因爲是利用業餘時間進行翻譯並維護,所以該中文文檔並不保證老是與原文檔同樣處於最新版本,不過我會盡量及時地去更新它。java

前言

Spark 有超過 800 位貢獻者,就咱們所知,應該是目前大數據領域裏最大的開源項目且是最活躍的 Scala 項目。這份指南是在咱們指導,或是與 Spark 貢獻者及 Databricks 工程團隊一塊兒工做時總結出來的。git

代碼由做者__一次編寫__,而後由大量工程師__屢次閱讀並修改__。事實上,大部分的 bug 來源於後人對代碼的修改,所以咱們須要長期去優化咱們的代碼,提高代碼的可讀性和可維護性。達到這個目標最好的方式就是編寫簡單易懂的代碼。程序員

Scala 是一種強大到使人難以置信的多範式編程語言。咱們總結出瞭如下指南,它能夠很好地應用在一個高速發展的項目。固然,這個指南並不是絕對,根據團隊需求的不一樣,能夠有不一樣的標準。github

目錄

  1. 文檔歷史apache

  2. 語法風格編程

  3. Scala 語言特性

  4. 併發

  5. 性能

  6. 與 Java 的互操做性

  7. 其它

文檔歷史

  • 2015-03-16: 最第一版本。

  • 2015-05-25: 增長 override 修飾符 一節。

  • 2015-08-23: 把一些規則的嚴重程度從「不要」降級到「避免」。

  • 2015-11-17: 更新 apply 方法 一節:伴生對象中的 apply 方法應該返回其伴生類。

  • 2015-11-17: 該指南被翻譯成中文,由 Hawstein 進行維護,中文文檔並不保證老是與原文檔同樣處於最新版本。

語法風格

命名約定

咱們主要遵循 Java 和 Scala 的標準命名約定。

  • 類,trait, 對象應該遵循 Java 中類的命名約定,即 PascalCase 風格。

    class ClusterManager
    
    trait Expression
  • 包名應該遵循 Java 中包名的命名約定,即便用全小寫的 ASCII 字母。

    package com.databricks.resourcemanager
  • 方法/函數應當使用駝峯式風格命名。

  • 常量命名使用全大寫字母,並將它們放在伴生對象中。

    object Configuration {
      val DEFAULT_PORT = 10000
    }
  • 枚舉命名與類命名一致,使用 PascalCase 風格。

  • 註解也應遵循 Java 中的約定,即便用 PascalCase 風格。注意,這一點與 Scala 的官方指南不一樣。

    final class MyAnnotation extends StaticAnnotation

一行長度

  • 一行長度的上限是 100 個字符。

  • 惟一的例外是 import 語句和 URL (即使如此,也儘可能將它們保持在 100 個字符如下)。

30 法則

「若是一個元素包含的子元素超過 30 個,那麼極有可能出現了嚴重的問題」 - Refactoring in Large Software Projects

通常來講:

  • 一個方法包含的代碼行數不宜超過 30 行。

  • 一個類包含的方法數量不宜超過 30 個。

空格與縮進

  • 通常狀況下,使用兩個空格的縮進。

    if (true) {
      println("Wow!")
    }
  • 對於方法聲明,若是一行沒法容納下全部的參數,那麼使用 4 個空格來縮進它們。返回類型能夠與最後一個參數在同一行,也能夠放在下一行,使用兩個空格縮進。

    def newAPIHadoopFile[K, V, F <: NewInputFormat[K, V]](
        path: String,
        fClass: Class[F],
        kClass: Class[K],
        vClass: Class[V],
        conf: Configuration = hadoopConfiguration): RDD[(K, V)] = {
      // method body
    }
    
    def newAPIHadoopFile[K, V, F <: NewInputFormat[K, V]](
        path: String,
        fClass: Class[F],
        kClass: Class[K],
        vClass: Class[V],
        conf: Configuration = hadoopConfiguration)
      : RDD[(K, V)] = {
      // method body
    }
  • 若是一行沒法容納下類頭(即 extends 後面那部分),則把它們放到新的一行,用兩個空格縮進,而後在類內空一行再開始函數或字段的定義(或是包的導入)。

    class Foo(
        val param1: String,  // 4 space indent for parameters
        val param2: String,
        val param3: Array[Byte])
      extends FooInterface  // 2 space here
      with Logging {
    
      def firstMethod(): Unit = { ... }  // blank line above
    }
  • 不要使用垂直對齊。它使你的注意力放在代碼的錯誤部分並增大了後人修改代碼的難度。

    // Don't align vertically
    val plus     = "+"
    val minus    = "-"
    val multiply = "*"
    
    // Do the following
    val plus = "+"
    val minus = "-"
    val multiply = "*"

空行

  • 一個空行能夠出如今:

    • 連續的類成員或初始化器(initializers)之間:字段,構造函數,方法,嵌套類,靜態初始化器及實例初始化器。

      • 例外:連續的兩個字段之間的空行是可選的(前提是它們之間沒有其它代碼),這一類空行主要爲這些字段作邏輯上的分組。

    • 在方法體內,根據須要,使用空行來爲語句建立邏輯上的分組。

    • 在類的第一個成員以前或最後一個成員以後,空行都是可選的(既不鼓勵也不阻止)。

  • 使用一個或兩個空行來分隔不一樣類的定義。

  • 不鼓勵使用過多的空行。

括號

  • 方法聲明應該加括號(即便沒有參數列表),除非它們是沒有反作用(狀態改變,IO 操做都認爲是有反作用的)的訪問器(accessor)。

    class Job {
      // Wrong: killJob changes state. Should have ().
      def killJob: Unit
    
      // Correct:
      def killJob(): Unit
    }
  • 函數調用應該與函數聲明在形式上保持一致,也就是說,若是一個方法聲明時帶了括號,那調用時也要把括號帶上。注意這不只僅是語法層面的人爲約定,當返回對象中定義了 apply 方法時,這一點還會影響正確性。

    class Foo {
      def apply(args: String*): Int
    }
    
    class Bar {
      def foo: Foo
    }
    
    new Bar().foo  // This returns a Foo
    new Bar().foo()  // This returns an Int!

大括號

即便條件語句或循環語句只有一行時,也請使用大括號。惟一的例外是,當你把 if/else 做爲一個單行的三元操做符來使用而且沒有反作用時,這時你能夠不加大括號。

// Correct:
if (true) {
  println("Wow!")
}

// Correct:
if (true) statement1 else statement2

// Correct:
try {
  foo()
} catch {
  ...
}

// Wrong:
if (true)
  println("Wow!")

// Wrong:
try foo() catch {
  ...
}

長整型字面量

長整型字面量使用大寫的 L 做爲後綴,不要使用小寫,由於它和數字 1 長得很像,經常難以區分。

val longValue = 5432L  // Do this

val longValue = 5432l  // Do NOT do this

文檔風格

使用 Java Doc 風格,而非 Scala Doc 風格。

/** This is a correct one-liner, short description. */

/**
 * This is correct multi-line JavaDoc comment. And
 * this is my second line, and if I keep typing, this would be
 * my third line.
 */

/** In Spark, we don't use the ScalaDoc style so this
  * is not correct.
  */

類內秩序

若是一個類很長,包含許多的方法,那麼在邏輯上把它們分紅不一樣的部分並加上註釋頭,以此組織它們。

class DataFrame {

  ///////////////////////////////////////////////////////////////////////////
  // DataFrame operations
  ///////////////////////////////////////////////////////////////////////////

  ...

  ///////////////////////////////////////////////////////////////////////////
  // RDD operations
  ///////////////////////////////////////////////////////////////////////////

  ...
}

固然,強烈不建議把一個類寫得這麼長,通常只有在構建某些公共 API 時才容許這麼作。

Imports

  • __導入時避免使用通配符__, 除非你須要導入超過 6 個實體或者隱式方法。通配符導入會使代碼在面對外部變化時不夠健壯。

  • 始終使用絕對路徑來導入包 (如:scala.util.Random) ,而不是相對路徑 (如:util.Random)。

  • 此外,導入語句按照如下順序排序:

    • java.*javax.*

    • scala.*

    • 第三方庫 (org.*, com.*, 等)

    • 項目中的類 (對於 Spark 項目,即 com.databricks.*org.apache.spark)

  • 在每一組導入語句內,按照字母序進行排序。

  • 你可使用 IntelliJ 的「import organizer」來自動處理,請使用如下配置:

    java
    javax
    _______ blank line _______
    scala
    _______ blank line _______
    all other imports
    _______ blank line _______
    com.databricks  // or org.apache.spark if you are working on spark

模式匹配

  • 若是整個方法就是一個模式匹配表達式,可能的話,能夠把 match 關鍵詞與方法聲明放在同一行,以此減小一級縮進。

    def test(msg: Message): Unit = msg match {
      case ...
    }
  • 當以閉包形式調用一個函數時,若是隻有一個 case 語句,那麼把 case 語句與函數調用放在同一行。

    list.zipWithIndex.map { case (elem, i) =>
      // ...
    }

若是有多個 case 語句,把它們縮進而且包起來。

list.map {
  case a: Foo =>  ...
  case b: Bar =>  ...
}

中綴方法

__避免中綴表示法__,除非是符號方法(即運算符重載)。

// Correct
list.map(func)
string.contains("foo")

// Wrong
list map (func)
string contains "foo"

// 重載的運算符應該以中綴形式調用
arrayBuffer += elem

Scala 語言特性

apply 方法

避免在類裏定義 apply 方法。這些方法每每會使代碼的可讀性變差,尤爲是對於不熟悉 Scala 的人。它也難以被 IDE(或 grep)所跟蹤。在最壞的狀況下,它還可能影響代碼的正確性,正如你在括號一節中看到的。

然而,將 apply 方法做爲工廠方法定義在伴生對象中是能夠接受的。在這種狀況下,apply 方法應該返回其伴生類的類型。

object TreeNode {
  // 下面這種定義是 OK 的
  def apply(name: String): TreeNode = ...

  // 不要像下面那樣定義,由於它沒有返回其伴生類的類型:TreeNode
  def apply(name: String): String = ...
}

override 修飾符

不管是覆蓋具體的方法仍是實現抽象的方法,始終都爲方法加上 override 修飾符。實現抽象方法時,不加 override 修飾符,Scala 編譯器也不會報錯。即使如此,咱們也應該始終把 override 修飾符加上,以此顯式地表示覆蓋行爲。以此避免因爲方法簽名不一樣(而你也難以發現)而致使沒有覆蓋到本應覆蓋的方法。

trait Parent {
  def hello(data: Map[String, String]): Unit = {
    print(data)
  }
}

class Child extends Parent {
  import scala.collection.Map

  // 下面的方法沒有覆蓋 Parent.hello,
  // 由於兩個 Map 的類型是不一樣的。
  // 若是咱們加上 override 修飾符,編譯器就會幫你找出問題並報錯。
  def hello(data: Map[String, String]): Unit = {
    print("This is supposed to override the parent method, but it is actually not!")
  }
}

解構綁定

解構綁定(有時也叫元組提取)是一種在一個表達式中爲兩個變量賦值的便捷方式。

val (a, b) = (1, 2)

然而,請不要在構造函數中使用它們,尤爲是當 ab 須要被標記爲 transient 的時候。Scala 編譯器會產生一個額外的 Tuple2 字段,而它並非暫態的(transient)。

class MyClass {
  // 如下代碼沒法 work,由於編譯器會產生一個非暫態的 Tuple2 指向 a 和 b
  @transient private val (a, b) = someFuncThatReturnsTuple2()
}

按名稱傳參

__避免使用按名傳參__. 顯式地使用 () => T

背景:Scala 容許按名稱來定義方法參數,例如:如下例子是能夠成功執行的:

def print(value: => Int): Unit = {
  println(value)
  println(value + 1)
}

var a = 0
def inc(): Int = {
  a + 1
  a
}

print(inc())

在上面的代碼中,inc() 以閉包的形式傳遞給 print 函數,而且在 print 函數中被執行了兩次,而不是以數值 1 傳入。按名傳參的一個主要問題是在方法調用處,咱們沒法區分是按名傳參仍是按值傳參。所以沒法確切地知道這個表達式是否會被執行(更糟糕的是它可能會被執行屢次)。對於帶有反作用的表達式來講,這一點是很是危險的。

多參數列表

__避免使用多參數列表__。它們使運算符重載變得複雜,而且會使不熟悉 Scala 的程序員感到困惑。例如:

// Avoid this!
case class Person(name: String, age: Int)(secret: String)

一個值得注意的例外是,當在定義底層庫時,可使用第二個參數列表來存放隱式(implicit)參數。儘管如此,咱們應該避免使用 implicits

符號方法(運算符重載)

__不要使用符號做爲方法名__,除非你是在定義算術運算的方法(如:+, -, *, /),不然在任何其它狀況下,都不要使用。符號化的方法名讓人難以理解方法的意圖是什麼,來看下面兩個例子:

// 符號化的方法名難以理解
channel ! msg
stream1 >>= stream2

// 下面的方法意圖則不言而喻
channel.send(msg)
stream1.join(stream2)

類型推導

Scala 的類型推導,尤爲是左側類型推導以及閉包推導,可使代碼變得更加簡潔。儘管如此,也有一些狀況咱們是須要顯式地聲明類型的:

  • __公有方法應該顯式地聲明類型__,編譯器推導出來的類型每每會使你大吃一驚。

  • __隱式方法應該顯式地聲明類型__,不然在增量編譯時,它會使 Scala 編譯器崩潰。

  • __若是變量或閉包的類型並不是顯而易見,請顯式聲明類型__。一個不錯的判斷準則是,若是評審代碼的人沒法在 3 秒內肯定相應實體的類型,那麼你就應該顯式地聲明類型。

Return 語句

__閉包中避免使用 return__。return 會被編譯器轉成 scala.runtime.NonLocalReturnControl 異常的 try/catch 語句,這可能會致使意外行爲。請看下面的例子:

def receive(rpc: WebSocketRPC): Option[Response] = {
  tableFut.onComplete { table =>
    if (table.isFailure) {
      return None // Do not do that!
    } else { ... }
  }
}

.onComplete 方法接收一個匿名閉包並把它傳遞到一個不一樣的線程中。這個閉包最終會拋出一個 NonLocalReturnControl 異常,並在 __一個不一樣的線程中__被捕獲,而這裏執行的方法卻沒有任何影響。

然而,也有少數狀況咱們是推薦使用 return 的。

  • 使用 return 來簡化控制流,避免增長一級縮進。

    def doSomething(obj: Any): Any = {
      if (obj eq null) {
        return null
      }
      // do something ...
    }
  • 使用 return 來提早終止循環,這樣就不用額外構造狀態標誌。

    while (true) {
      if (cond) {
        return
      }
    }

遞歸及尾遞歸

__避免使用遞歸__,除非問題能夠很是天然地用遞歸來描述(好比,圖和樹的遍歷)。

對於那些你意欲使之成爲尾遞歸的方法,請加上 @tailrec 註解以確保編譯器去檢查它是否真的是尾遞歸(你會很是驚訝地看到,因爲使用了閉包和函數變換,許多看似尾遞歸的代碼事實並不是尾遞歸)。

大多數的代碼使用簡單的循環和狀態機會更容易推理,使用尾遞歸反而可能會使它更加繁瑣且難以理解。例如,下面的例子中,命令式的代碼比尾遞歸版本的代碼要更加易讀:

// Tail recursive version.
def max(data: Array[Int]): Int = {
  @tailrec
  def max0(data: Array[Int], pos: Int, max: Int): Int = {
    if (pos == data.length) {
      max
    } else {
      max0(data, pos + 1, if (data(pos) > max) data(pos) else max)
    }
  }
  max0(data, 0, Int.MinValue)
}

// Explicit loop version
def max(data: Array[Int]): Int = {
  var max = Int.MinValue
  for (v <- data) {
    if (v > max) {
      max = v
    }
  }
  max
}

Implicits

__避免使用 implicit__,除非:

  • 你在構建領域特定的語言(DSL)

  • 你在隱式類型參數中使用它(如:ClassTagTypeTag

  • 你在你本身的類中使用它(意指不要污染外部空間),以此減小類型轉換的冗餘度(如:Scala 閉包到 Java 閉包的轉換)。

當使用 implicit 時,咱們應該確保另外一個工程師能夠直接理解使用語義,而無需去閱讀隱式定義自己。Implicit 有着很是複雜的解析規則,這會使代碼變得極其難以理解。Twitter 的 Effective Scala 指南中寫道:「若是你發現你在使用 implicit,始終停下來問一下你本身,是否能夠在不使用 implicit 的條件下達到相同的效果」。

若是你必需使用它們(好比:豐富 DSL),那麼不要重載隱式方法,即確保每一個隱式方法有着不一樣的名字,這樣使用者就能夠選擇性地導入它們。

// 別這麼作,這樣使用者沒法選擇性地只導入其中一個方法。
object ImplicitHolder {
  def toRdd(seq: Seq[Int]): RDD[Int] = ...
  def toRdd(seq: Seq[Long]): RDD[Long] = ...
}

// 應該將它們定義爲不一樣的名字:
object ImplicitHolder {
  def intSeqToRdd(seq: Seq[Int]): RDD[Int] = ...
  def longSeqToRdd(seq: Seq[Long]): RDD[Long] = ...
}

異常處理,Try 仍是 try

  • 不要捕獲 Throwable 或 Exception 類型的異常。請使用 scala.util.control.NonFatal

    try {
      ...
    } catch {
      case NonFatal(e) =>
        // 異常處理;注意 NonFatal 沒法匹配 InterruptedException 類型的異常
      case e: InterruptedException =>
        // 處理 InterruptedException
    }

這能保證咱們不會去捕獲 NonLocalReturnControl 異常(正如在Return 語句中所解釋的)。

  • 不要在 API 中使用 Try,即,不要在任何方法中返回 Try。對於異常執行,請顯式地拋出異常,並使用 Java 風格的 try/catch 作異常處理。

背景資料:Scala 提供了單子(monadic)錯誤處理(經過 TrySuccessFailure),這樣便於作鏈式處理。然而,根據咱們的經驗,發現使用它一般會帶來更多的嵌套層級,使得代碼難以閱讀。此外,對於預期錯誤仍是異常,在語義上經常是不明晰的。所以,咱們不鼓勵使用 Try 來作錯誤處理,尤爲是如下狀況:

一我的爲的例子:

class UserService {
  /** Look up a user's profile in the user database. */
  def get(userId: Int): Try[User]
}

如下的寫法會更好:

class UserService {
  /**
   * Look up a user's profile in the user database.
   * @return None if the user is not found.
   * @throws DatabaseConnectionException when we have trouble connecting to the database/
   */
  @throws(DatabaseConnectionException)
  def get(userId: Int): Option[User]
}

第二種寫法很是明顯地能讓調用者知道須要處理哪些錯誤狀況。

Options

  • 若是一個值可能爲空,那麼請使用 Option。相對於 nullOption 顯式地代表了一個 API 的返回值可能爲空。

  • 構造 Option 值時,請使用 Option 而非 Some,以防那個值爲 null

    def myMethod1(input: String): Option[String] = Option(transform(input))
    
    // This is not as robust because transform can return null, and then
    // myMethod2 will return Some(null).
    def myMethod2(input: String): Option[String] = Some(transform(input))
  • 不要使用 None 來表示異常,有異常時請顯式拋出。

  • 不要在一個 Option 值上直接調用 get 方法,除非你百分百肯定那個 Option 值不是 None

單子連接

單子連接是 Scala 的一個強大特性。Scala 中幾乎一切都是單子(如:集合,Option,Future,Try 等),對它們的操做能夠連接在一塊兒。這是一個很是強大的概念,但你應該謹慎使用,尤爲是:

  • 避免連接(或嵌套)超過 3 個操做。

  • 若是須要花超過 5 秒鐘來理解其中的邏輯,那麼你應該儘可能去想一想有沒什麼辦法在不使用單子連接的條件下來達到相同的效果。通常來講,你須要注意的是:不要濫用 flatMapfold

  • 連接應該在 flatMap 以後斷開(由於類型發生了變化)。

經過給中間結果顯式地賦予一個變量名,將連接斷開變成一種更加過程化的風格,能讓單子連接更加易於理解。來看下面的例子:

class Person(val data: Map[String, String])
val database = Map[String, Person]
// Sometimes the client can store "null" value in the  store "address"

// A monadic chaining approach
def getAddress(name: String): Option[String] = {
  database.get(name).flatMap { elem =>
    elem.data.get("address")
      .flatMap(Option.apply)  // handle null value
  }
}

// 儘管代碼會長一些,但如下方法可讀性更高
def getAddress(name: String): Option[String] = {
  if (!database.contains(name)) {
    return None
  }

  database(name).data.get("address") match {
    case Some(null) => None  // handle null value
    case Some(addr) => Option(addr)
    case None => None
  }
}

併發

Scala concurrent.Map

__優先考慮使用 java.util.concurrent.ConcurrentHashMap 而非 scala.collection.concurrent.Map__。尤爲是 scala.collection.concurrent.Map 中的 getOrElseUpdate 方法要慎用,它並不是原子操做(這個問題在 Scala 2.11.16 中 fix 了:SI-7943)。因爲咱們作的全部項目都須要在 Scala 2.10 和 Scala 2.11 上使用,所以要避免使用 scala.collection.concurrent.Map

顯式同步 vs 併發集合

有 3 種推薦的方法來安全地併發訪問共享狀態。__不要混用它們__,由於這會使程序變得難以推理,而且可能致使死鎖。

  • java.util.concurrent.ConcurrentHashMap:當全部的狀態都存儲在一個 map 中,而且有高程度的競爭時使用。

    private[this] val map = new java.util.concurrent.ConcurrentHashMap[String, String]
  • java.util.Collections.synchronizedMap:使用情景:當全部狀態都存儲在一個 map 中,而且預期不存在競爭狀況,但你仍想確保代碼在併發下是安全的。若是沒有競爭出現,JVM 的 JIT 編譯器可以經過偏置鎖(biased locking)移除同步開銷。

    private[this] val map = java.util.Collections.synchronizedMap(new java.util.HashMap[String, String])
  • 經過同步全部臨界區進行顯式同步,可用於監視多個變量。與 2 類似,JVM 的 JIT 編譯器可以經過偏置鎖(biased locking)移除同步開銷。

    class Manager {
      private[this] var count = 0
      private[this] val map = new java.util.HashMap[String, String]
      def update(key: String, value: String): Unit = synchronized {
        map.put(key, value)
        count += 1
      }
      def getCount: Int = synchronized { count }
    }

注意,對於 case 1 和 case 2,不要讓集合的視圖或迭代器從保護區域逃逸。這可能會以一種不明顯的方式發生,好比:返回了 Map.keySetMap.values。若是須要傳遞集合的視圖或值,生成一份數據拷貝再傳遞。

val map = java.util.Collections.synchronizedMap(new java.util.HashMap[String, String])

// This is broken!
def values: Iterable[String] = map.values

// Instead, copy the elements
def values: Iterable[String] = map.synchronized { Seq(map.values: _*) }

顯式同步 vs 原子變量 vs @volatile

java.util.concurrent.atomic 包提供了對基本類型的無鎖訪問,好比:AtomicBoolean, AtomicIntegerAtomicReference

始終優先考慮使用原子變量而非 @volatile,它們是相關功能的嚴格超集而且從代碼上看更加明顯。原子變量的底層實現使用了 @volatile

優先考慮使用原子變量而非顯式同步的狀況:(1)一個對象的全部臨界區更新都被限制在單個變量裏而且預期會有競爭狀況出現。原子變量是無鎖的而且容許更爲有效的競爭。(2)同步被明確地表示爲 getAndSet 操做。例如:

// good: 明確又有效地表達了下面的併發代碼只執行一次
val initialized = new AtomicBoolean(false)
...
if (!initialized.getAndSet(true)) {
  ...
}

// poor: 下面的同步就沒那麼明晰,並且會出現沒必要要的同步
val initialized = false
...
var wasInitialized = false
synchronized {
  wasInitialized = initialized
  initialized = true
}
if (!wasInitialized) {
  ...
}

私有字段

注意,private 字段仍然能夠被相同類的其它實例所訪問,因此僅僅經過 this.synchronized(或 synchronized)來保護它從技術上來講是不夠的,不過你能夠經過 private[this] 修飾私有字段來達到目的。

// 如下代碼仍然是不安全的。
class Foo {
  private var count: Int = 0
  def inc(): Unit = synchronized { count + 1 }
}

// 如下代碼是安全的。
class Foo {
  private[this] var count: Int = 0
  def inc(): Unit = synchronized { count + 1 }
}

隔離

通常來講,併發和同步邏輯應該儘量地被隔離和包含起來。這實際上意味着:

  • 避免在 API 層面、面向用戶的方法以及回調中暴露同步原語。

  • 對於複雜模塊,建立一個小的內部模塊來包含併發原語。

性能

對於你寫的絕大多數代碼,性能都不該該成爲一個問題。然而,對於一些性能敏感的代碼,如下有一些小建議:

Microbenchmarks

因爲 Scala 編譯器和 JVM JIT 編譯器會對你的代碼作許多神奇的事情,所以要寫出一個好的微基準程序(microbenchmark)是極其困難的。更多的狀況每每是你的微基準程序並無測量你想要測量的東西。

若是你要寫一個微基準程序,請使用 jmh。請確保你閱讀了全部的樣例,這樣你才理解微基準程序中「死代碼」移除、常量摺疊以及循環展開的效果。

Traversal 與 zipWithIndex

使用 while 循環而非 for 循環或函數變換(如:mapforeach),for 循環和函數變換很是慢(因爲虛函數調用和裝箱的緣故)。

val arr = // array of ints
// 偶數位置的數置零
val newArr = list.zipWithIndex.map { case (elem, i) =>
  if (i % 2 == 0) 0 else elem
}

// 這是上面代碼的高性能版本
val newArr = new Array[Int](arr.length)
var i = 0
val len = newArr.length
while (i < len) {
  newArr(i) = if (i % 2 == 0) 0 else arr(i)
  i += 1
}

Option 與 null

對於性能有要求的代碼,優先考慮使用 null 而不是 Option,以此避免虛函數調用以及裝箱操做。用 Nullable 註解明確標示出可能爲 null 的值。

class Foo {
  @javax.annotation.Nullable
  private[this] var nullableField: Bar = _
}

Scala 集合庫

對於性能有要求的代碼,優先考慮使用 Java 集合庫而非 Scala 集合庫,由於通常來講,Scala 集合庫要比 Java 的集合庫慢。

private[this]

對於性能有要求的代碼,優先考慮使用 private[this] 而非 privateprivate[this] 生成一個字段而非生成一個訪問方法。根據咱們的經驗,JVM JIT 編譯器並不老是會內聯 private 字段的訪問方法,所以經過使用
private[this] 來確保沒有虛函數調用會更保險。

class MyClass {
  private val field1 = ...
  private[this] val field2 = ...

  def perfSensitiveMethod(): Unit = {
    var i = 0
    while (i < 1000000) {
      field1  // This might invoke a virtual method call
      field2  // This is just a field access
      i += 1
    }
  }
}

與 Java 的互操做性

本節內容介紹的是構建 Java 兼容 API 的準則。若是你構建的組件並不須要與 Java 有交互,那麼請無視這一節。這一節的內容主要是從咱們開發 Spark 的 Java API 的經歷中得出的。

Scala 中缺失的 Java 特性

如下的 Java 特性在 Scala 中是沒有的,若是你須要使用如下特性,請在 Java 中定義它們。然而,須要提醒一點的是,你沒法爲 Java 源文件生成 ScalaDoc。

  • 靜態字段

  • 靜態內部類

  • Java 枚舉

  • 註解

Traits 與抽象類

對於容許從外部實現的接口,請記住如下幾點:

  • 包含了默認方法實現的 trait 是沒法在 Java 中使用的,請使用抽象類來代替。

  • 通常狀況下,請避免使用 trait,除非你百分百肯定這個接口即便在將來也不會有默認的方法實現。

// 如下默認實現沒法在 Java 中使用
trait Listener {
  def onTermination(): Unit = { ... }
}

// 能夠在 Java 中使用
abstract class Listener {
  def onTermination(): Unit = { ... }
}

類型別名

不要使用類型別名,它們在字節碼和 Java 中是不可見的。

默認參數值

不要使用默認參數值,經過重載方法來代替。

// 打破了與 Java 的互操做性
def sample(ratio: Double, withReplacement: Boolean = false): RDD[T] = { ... }

// 如下方法是 work 的
def sample(ratio: Double, withReplacement: Boolean): RDD[T] = { ... }
def sample(ratio: Double): RDD[T] = sample(ratio, withReplacement = false)

多參數列表

不要使用多參數列表。

可變參數

  • 爲可變參數方法添加 @scala.annotation.varargs 註解,以確保它能在 Java 中使用。Scala 編譯器會生成兩個方法,一個給 Scala 使用(字節碼參數是一個 Seq),另外一個給 Java 使用(字節碼參數是一個數組)。

    @scala.annotation.varargs
    def select(exprs: Expression*): DataFrame = { ... }
  • 須要注意的一點是,因爲 Scala 編譯器的一個 bug(SI-1459SI-9013),抽象的變參方法是沒法在 Java 中使用的。

  • 重載變參方法時要當心,用另外一個類型去重載變參方法會破壞源碼的兼容性。

    class Database {
      @scala.annotation.varargs
      def remove(elems: String*): Unit = ...
    
      // 當調用無參的 remove 方法時會出問題。
      @scala.annotation.varargs
      def remove(elems: People*): Unit = ...
    }
    
    // remove 方法有歧義,所以編譯不過。
    new Database().remove()

一種解決方法是,在可變參數前顯式地定義第一個參數:

class Database {
  @scala.annotation.varargs
  def remove(elems: String*): Unit = ...

  // 如下重載是 OK 的。
  @scala.annotation.varargs
  def remove(elem: People, elems: People*): Unit = ...
}

Implicits

不要爲類或方法使用 implicit,包括了不要使用 ClassTagTypeTag

class JavaFriendlyAPI {
  // 如下定義對 Java 是不友好的,由於方法中包含了一個隱式參數(ClassTag)。
  def convertTo[T: ClassTag](): T
}

伴生對象,靜態方法與字段

當涉及到伴生對象和靜態方法/字段時,有幾件事情是須要注意的:

  • 伴生對象在 Java 中的使用是很是彆扭的(伴生對象 Foo 會被定義爲 Foo$ 類內的一個類型爲 Foo$ 的靜態字段 MODULE$)。

    object Foo
    
    // 等價於如下的 Java 代碼
    public class Foo$ {
      Foo$ MODULE$ = // 對象的實例化
    }

若是非要使用伴生對象,能夠在一個單獨的類中建立一個 Java 靜態字段。

  • 不幸的是,沒有辦法在 Scala 中定義一個 JVM 靜態字段。請建立一個 Java 文件來定義它。

  • 伴生對象裏的方法會被自動轉成伴生類裏的靜態方法,除非方法名有衝突。確保靜態方法正確生成的最好方式是用 Java 寫一個測試文件,而後調用生成的靜態方法。

    class Foo {
      def method2(): Unit = { ... }
    }
    
    object Foo {
      def method1(): Unit = { ... }  // 靜態方法 Foo.method1 會被建立(字節碼)
      def method2(): Unit = { ... }  // 靜態方法 Foo.method2 不會被建立
    }
    
    // FooJavaTest.java (in test/scala/com/databricks/...)
    public class FooJavaTest {
      public static compileTest() {
        Foo.method1();  // 正常編譯
        Foo.method2();  // 編譯失敗,由於 method2 並無生成
      }
    }
  • 樣例對象(case object) MyClass 的類型並非 MyClass。

    case object MyClass
    
    // Test.java
    if (MyClass$.MODULE instanceof MyClass) {
      // 上述條件始終爲 false
    }

要實現正確的類型層級結構,請定義一個伴生類,而後用一個樣例對象去繼承它:

class MyClass
case object MyClass extends MyClass

其它

優先使用 nanoTime 而非 currentTimeMillis

當要計算持續時間或者檢查超時的時候,避免使用 System.currentTimeMillis()。請使用 System.nanoTime(),即便你對亞毫秒級的精度並不感興趣。

System.currentTimeMillis() 返回的是當前的時鐘時間,而且會跟進系統時鐘的改變。所以,負的時鐘調整可能會致使超時而掛起很長一段時間(直到時鐘時間遇上先前的值)。這種狀況可能發生在網絡已經中斷一段時間,ntpd 走過了一步以後。最典型的例子是,在系統啓動的過程當中,DHCP 花費的時間要比日常的長。這可能會致使很是難以理解且難以重現的問題。而 System.nanoTime() 則能夠保證是單調遞增的,與時鐘變化無關。

注意事項:

  • 永遠不要序列化一個絕對的 nanoTime() 值或是把它傳遞給另外一個系統。絕對的 nanoTime() 值是無心義的、與系統相關的,而且在系統重啓時會重置。

  • 絕對的 nanoTime() 值並不保證老是正數(但 t2 - t1 能確保老是產生正確的值)。

  • nanoTime() 每 292 年就會從新計算起。因此,若是你的 Spark 任務須要花很是很是很是長的時間,你可能須要別的東西來處理了:)

優先使用 URI 而非 URL

當存儲服務的 URL 時,你應當使用 URI 來表示。

URL相等性檢查)實際上執行了一次網絡調用(這是阻塞的)來解析 IP 地址。URI 類在表示能力上是 URL 的超集,而且它執行的是字段的相等性檢查。

https://github.com/Hawstein/scala-style-guide/blob/master/README-ZH.md

相關文章
相關標籤/搜索