風格指南

宣告

語言

類別

類別、物件和特質建構函式應全部宣告在同一行,除非該行「太長」(約 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. 存取修飾詞 (protected, private)
  4. 隱含修飾詞 (implicit)
  5. 最終修飾詞 (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 允許在呼叫位置針對此類函式使用較好的語法,前提是函式參數作為最後一個引數進行 currying。例如,這是 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)完全無關。此樣式因其簡潔和乾淨而受到青睞。

欄位

欄位應遵循方法的宣告規則,特別注意存取修飾符的順序和註解慣例。

Lazy vals 應在 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
}

如前所述,函式值應盡可能利用類型推論。

此頁面的貢獻者