這是我去年在一個Scala項目中結合一些參考資料和項目實踐整理的一份編碼規範,基於的Scala版本爲2.10,但同時也適用於2.11版本。參考資料見文後。整個編碼規範分爲以下六個部分:javascript
1. 格式與命名php
2. 語法特性css
3. 編碼風格html
4. 高效編碼java
5. 編碼模式python
6. 測試git
1) 代碼格式用兩個空格縮進。避免每行長度超過100列。在兩個方法、類、對象定義之間使用一個空白行。github
2) 優先考慮使用val,而非var。sql
3) 當引入多個包時,使用花括號:數據庫
import jxl.write.{WritableCell, Number, Label}
當引入的包超過6個時,應使用通配符_:
import org.scalatest.events._
4)若方法暴露爲接口,則返回類型應該顯式聲明。例如:
def execute(conn: Connection): Boolean = { executeCommand(conn, sqlStatement) match { case Right(result) => result case Left(_) => false } }
5) 集合的命名規範
xs, ys, as, bs等做爲某種Sequence對象的名稱;
x, y, z, a, b做爲sequence元素的名稱。
h做爲head的名稱,t做爲tail的名稱。
6)避免對簡單的表達式採用花括號;
//suggestion def square(x: Int) = x * x //avoid def square(x: Int) = { x * x }
7) 泛型類型參數的命名雖然沒有限制,但建議遵循以下規則:
A 表明一個簡單的類型,例如List[A]
B, C, D 用於第二、第三、第4等類型。例如:
class List[A] {
def mapB: List[B] = ...
}
N 表明數值類型
注意:在Java中,一般以K、V表明Map的key與value,可是在Scala中,更傾向於使用A、B表明Map的key與value。
1) 定義隱式類時,應該將構造函數的參數聲明爲val。
2)使用for表達式;若是須要條件表達式,應將條件表達式寫到for comprehension中:
//not good
for (file <- files) { if (hasSoundFileExtension(file) && !soundFileIsLong(file)) { soundFiles += file } } //better for { file <- files if hasSoundFileExtension(file) if !soundFileIsLong(file) } yield file
一般狀況下,咱們應優先考慮filter, map, flatMap等操做,而非for comprehension:
//best files.filter(hasSourceFileExtension).filterNot(soundFileIsLong)
3) 避免使用isInstanceOf,而是使用模式匹配,尤爲是在處理比較複雜的類型判斷時,使用模式匹配的可讀性更好。
//avoid
if (x.isInstanceOf[Foo]) { do something … //suggest def isPerson(x: Any): Boolean = x match { case p: Person => true case _ => false }
4)如下狀況使用abstract class,而不是trait:
5) 若是但願trait只能被某個類(及其子類)extend,應該使用self type:
trait MyTrait { this: BaseType => }
若是但願對擴展trait的類作更多限制,能夠在self type後增長更多對trait的混入:
trait WarpCore { this: Starship with WarpCoreEjector with FireExtinguisher => } // this works class Enterprise extends Starship with WarpCore with WarpCoreEjector with FireExtinguisher // won't compile class Enterprise extends Starship with WarpCore with WarpCoreEjector
若是要限制擴展trait的類必須定義相關的方法,能夠在self type中定義方法,這稱之爲structural type(相似動態語言的鴨子類型):
trait WarpCore { this: { def ejectWarpCore(password: String): Boolean def startWarpCore: Unit } => } class Starship class Enterprise extends Starship with WarpCore { def ejectWarpCore(password: String): Boolean = { if (password == "password") { println("core ejected"); true } else false } def startWarpCore { println("core started") } }
6) 對於較長的類型名稱,在特定上下文中,以不影響閱讀性和表達設計意圖爲前提,建議使用類型別名,它能夠幫助程序變得更簡短。例如:
class ConcurrentPool[K, V] { type Queue = ConcurrentLinkedQueue[V] type Map = ConcurrentHashMap[K, Queue] }
7) 若是要使用隱式參數,應儘可能使用自定義類型做爲隱式參數的類型,而避免過於寬泛的類型,如String,Int,Boolean等。
//suggestion
def maxOfList[T](elements: List[T]) (implicit orderer: T => Ordered[T]): T = elements match { case List() => throw new IllegalArgumentException("empty list!") case List(x) => x case x :: rest => val maxRest = maxListImpParm(rest)(orderer) if (orderer(x) > maxRest) x else maxRest } //avoid def maxOfListPoorStyle[T](elements: List[T]) (implicit orderer: (T, T) => Boolean): T
8) 對於異常的處理,Scala除了提供Java風格的try...catch...finally以外,還提供了allCatch.opt、Try…Success…Failure以及Either…Right…Left等風格的處理方式。其中,Try是2.10提供的語法。根據不一樣的場景選擇不一樣風格:
優先選擇Try風格。Try很好地支持模式匹配,它兼具Option與Either的特色,於是既提供了集合的語義,又支持模式匹配,又提供了getOrElse()方法。同時,它還能夠組合多個Try,並支持運用for combination。
val z = for { a <- Try(x.toInt) b <- Try(y.toInt) } yield a * b val answer = z.getOrElse(0) * 2
若是但願清楚的表現非此即彼的特性,應考慮使用Either。注意,約定成俗下,咱們習慣將正確的結果放在Either的右邊(Right既表示右邊,又表示正確)
若是但願將異常狀況處理爲None,則應考慮使用allCatch.opt。
import scala.util.control.Exception._ def readTextFile(f: String): Option[List[String]] = allCatch.opt(Source.fromFile(f).getLines.toList)
若是但願在執行後釋放資源,從而須要使用finally時,考慮try…catch...finally,或者結合try...catch...finally與Either。
private def executeQuery(conn: Connection, sql: String): Either[SQLException, ResultSet] = { var stmt: Statement = null var rs: ResultSet = null try { stmt = conn.createStatement() rs = stmt.executeQuery(sql) Right(rs) } catch { case e: SQLException => { e.printStackTrace() Left(e) } } finally { try { if (rs != null) rs.close() if (stmt != null) stmt.close() } catch { case e: SQLException => e.printStackTrace() } } }
爲避免重複,還應考慮引入Load Pattern。
1) 儘量直接在函數定義的地方使用模式匹配。例如,在下面的寫法中,match應該被摺疊起來(collapse):
list map { item => item match { case Some(x) => x case None => default } }
用下面的寫法替代:
list map { case Some(x) => x case None => default }
它很清晰的表達了 list中的元素都被映射,間接的方式讓人不容易明白。此時,傳入map的函數實則爲partial function。
2)避免使用null,而應該使用Option的None。
import java.io._ object CopyBytes extends App { var in = None: Option[FileInputStream] var out = None: Option[FileOutputStream] try { in = Some(new FileInputStream("/tmp/Test.class")) out = Some(new FileOutputStream("/tmp/Test.class.copy")) var c = 0 while ({c = in.get.read; c != −1}) { out.get.write(c) } } catch { case e: IOException => e.printStackTrace } finally { println("entered finally ...") if (in.isDefined) in.get.close if (out.isDefined) out.get.close } }
方法的返回值也要避免返回Null。應考慮返回Option,Either,或者Try。例如:
import scala.util.{Try, Success, Failure} def readTextFile(filename: String): Try[List[String]] = { Try(io.Source.fromFile(filename).getLines.toList ) val filename = "/etc/passwd" readTextFile(filename) match { case Success(lines) => lines.foreach(println) case Failure(f) => println(f) }
3)若在Class中須要定義常量,應將其定義爲val,並將其放在該類的伴生對象中:
class Pizza (var crustSize: Int, var crustType: String) { def this(crustSize: Int) { this(crustSize, Pizza.DEFAULT_CRUST_TYPE) } def this(crustType: String) { this(Pizza.DEFAULT_CRUST_SIZE, crustType) } def this() { this(Pizza.DEFAULT_CRUST_SIZE, Pizza.DEFAULT_CRUST_TYPE) } override def toString = s"A $crustSize inch pizza with a $crustType crust" } object Pizza { val DEFAULT_CRUST_SIZE = 12 val DEFAULT_CRUST_TYPE = "THIN" }
4)合理爲構造函數或方法提供默認值。例如:
class Socket (val timeout: Int = 10000)
5)若是須要返回多個值時,應返回tuple。
def getStockInfo = { // ("NFLX", 100.00, 101.00) }
6) 做爲訪問器的方法,若是沒有反作用,在聲明時建議定義爲沒有括號。
例如,Scala集合庫提供的scala.collection.immutable.Queue中,dequeue方法沒有反作用,聲明時就沒有括號:
import scala.collection.immutable.Queue val q = Queue(1, 2, 3, 4) val value = q.dequeue
7) 將包的公有代碼(常量、枚舉、類型定義、隱式轉換等)放到package object中。
package com.agiledon.myapp package object model { // field val MAGIC_NUM = 42 182 | Chapter 6: Objects  // method def echo(a: Any) { println(a) } // enumeration object Margin extends Enumeration { type Margin = Value val TOP, BOTTOM, LEFT, RIGHT = Value } // type definition type MutableMap[K, V] = scala.collection.mutable.Map[K, V] val MutableMap = scala.collection.mutable.Map }
8) 建議將package object放到與包對象命名空間一致的目錄下,並命名爲package.scala。以model爲例,package.scala文件應放在:
+-- com
+-- agiledon
+-- myapp
+-- model
+-- package.scala
9) 如有多個樣例類屬於同一類型,應共同繼承自一個sealed trait。
sealed trait Message case class GetCustomers extends Message case class GetOrders extends Message
注:這裏的sealed,表示trait的全部實現都必須聲明在定義trait的文件中。
10) 考慮使用renaming clause來簡化代碼。例如,替換被頻繁使用的長名稱方法:
import System.out.{println => p} p("hallo scala") p("input")
11) 在遍歷Map對象或者Tuple的List時,且須要訪問map的key和value值時,優先考慮採用Partial Function,而非使用_1和_2的形式。例如:
val dollar = Map("China" -> "CNY", "US" -> "DOL") //perfer dollar.foreach { case (country, currency) => println(s"$country -> $currency") } //avoid dollar.foreach ( x => println(s"$x._1 -> $x._2") )
或者,考慮使用for comprehension:
for ((country, currency) <- dollar) println(s"$country -> $currency")
12) 遍歷集合對象時,若是須要得到並操做集合對象的下標,不要使用以下方式:
val l = List("zero", "one", "two", "three") for (i <- 0 until l.length) yield (i, l(i))
而應該使用zipWithIndex方法:
for ((number, index) <- l.zipWithIndex) yield (index, number)
或者:
l.zipWithIndex.map(x => (x._2, x._1))
固然,若是須要將索引值放在Tuple的第二個元素,就更方便了。直接使用zipWithIndex便可。
zipWithIndex的索引初始值爲0,若是想指定索引的初始值,可使用zip:
l.zip(Stream from 1)
13) 應儘可能定義小粒度的trait,而後再以混入的方式繼承多個trait。例如ScalaTest中的FlatSpec:
class FlatSpec extends FlatSpecLike ... trait FlatSpecLike extends Suite with ShouldVerb with MustVerb with CanVerb with Informing …
小粒度的trait既有利於重用,同時還有利於對業務邏輯進行單元測試,尤爲是當一部分邏輯須要依賴外部環境時,能夠運用「關注點分離」的原則,將不依賴於外部環境的邏輯分離到單獨的trait中。
14) 優先使用不可變集合。若是肯定要使用可變集合,應明確的引用可變集合的命名空間。不要用使用import scala.collection.mutable._;而後引用 Set,應該用下面的方式替代:
import scala.collections.mutable val set = mutable.Set()
這樣更明確在使用一個可變集合。
15) 在本身定義的方法和構造函數裏,應適當的接受最寬泛的集合類型。一般能夠歸結爲一個: Iterable, Seq, Set, 或 Map。若是你的方法須要一個 sequence,使用 Seq[T],而不是List[T]。這樣能夠分離集合與它的實現,從而達成更好的可擴展性。
16) 應謹慎使用流水線轉換的形式。當流水線轉換的邏輯比較複雜時,應充分考慮代碼的可讀性,準確地表達開發者的意圖,而不過度追求函數式編程的流水線轉換風格。例如,咱們想要從一組投票結果(語言,票數)中統計不一樣程序語言的票數並按照得票的順序顯示:
val votes = Seq(("scala", 1), ("java", 4), ("scala", 10), ("scala", 1), ("python", 10)) val orderedVotes = votes .groupBy(_._1) .map { case (which, counts) => (which, counts.foldLeft(0)(_ + _._2)) }.toSeq .sortBy(_._2) .reverse
上面的代碼簡潔而且正確,但幾乎每一個讀者都很差理解做者的本來意圖。一個策略是聲明中間結果和參數:
val votesByLang = votes groupBy { case (lang, _) => lang } val sumByLang = votesByLang map { case (lang, counts) => val countsOnly = counts map { case (_, count) => count } (lang, countsOnly.sum) } val orderedVotes = sumByLang.toSeq .sortBy { case (_, count) => count } .reverse
代碼也一樣簡潔,但更清晰的表達了轉換的發生(經過命名中間值),和正在操做的數據的結構(經過命名參數)。
17) 對於Options對象,若是getOrElse可以表達業務邏輯,就應避免對其使用模式匹配。許多集合的操做都提供了返回Options的方法。例如headOption等。
val x = list.headOption getOrElse 0
這要比模式匹配更清楚:
val x = list match case head::_ => head case Nil: => 0
18) 當須要對兩個或兩個以上的集合進行操做時,應優先考慮使用for表達式,而非map,flatMap等操做。此時,for comprehension會更簡潔易讀。例如,獲取兩個字符的全部排列,相同的字符不能出現兩次。使用flatMap的代碼爲:
val chars = 'a' to 'z' val perms = chars flatMap { a => chars flatMap { b => if (a != b) Seq("%c%c".format(a, b)) else Seq() } }
使用for comprehension會更易懂:
val perms = for { a <- chars b <- chars if a != b } yield "%c%c".format(a, b)
1) 應儘可能避免讓trait去extend一個class。由於這種作法可能會致使間接的繼承多個類,從而產生編譯錯誤。同時,還會致使繼承體系的複雜度。
class StarfleetComponent trait StarfleetWarpCore extends StarfleetComponent class Starship extends StarfleetComponent with StarfleetWarpCore class RomulanStuff // won't compile class Warbird extends RomulanStuff with StarfleetWarpCore
2) 選擇使用Seq時,若須要索引下標功能,優先考慮選擇Vector,若須要Mutable的集合,則選擇ArrayBuffer;若要選擇Linear集合,優先選擇List,若須要Mutable的集合,則選擇ListBuffer。
3) 若是須要快速、通用、不變、帶順序的集合,應優先考慮使用Vector。Vector很好地平衡了快速的隨機選擇和快速的隨機更新(函數式)操做。Vector是Scala集合庫中最靈活的高效集合。一個原則是:當你對選擇集合類型猶疑不定時,就應選擇使用Vector。
須要注意的是:當咱們建立了一個IndexSeq時,Scala實際上會建立Vector對象:
scala> val x = IndexedSeq(1,2,3) x: IndexedSeq[Int] = Vector(1, 2, 3)
4) 若是須要選擇通用的可變集合,應優先考慮使用ArrayBuffer。尤爲面對一個大的集合,且新元素老是要添加到集合末尾時,就能夠選擇ArrayBuffer。若是使用的可變集合特性更近似於List這樣的線性集合,則考慮使用ListBuffer。
5) 若是須要將大量數據添加到集合中,建議選擇使用List的prepend操做,將這些數據添加到List頭部,最後作一次reverse操做。例如:
var l = List[Int]() (1 to max).foreach { i => i +: l } l.reverse
6) 當一個類的某個字段在獲取值時須要耗費資源,而且,該字段的值並不是一開始就須要使用。則應將該字段聲明爲lazy。
lazy val field = computation()
7) 在使用Future進行併發處理時,應使用回調的方式,而非阻塞:
//avoid val f = Future { //executing long time } val result = Await.result(f, 5 second) //suggesion val f = Future { //executing long time } f.onComplete { case Success(result) => //handle result case Failure(e) => e.printStackTrace }
8) 如有多個操做須要並行進行同步操做,能夠選擇使用par集合。例如:
val urls = List("http://scala-lang.org", "http://agiledon.github.com") def fromURL(url: String) = scala.io.Source.fromURL(url) .getLines().mkString("\n") val t = System.currentTimeMillis() urls.par.map(fromURL(_)) println("time: " + (System.currentTimeMillis - t) + "ms")
9) 如有多個操做須要並行進行異步操做,則採用for comprehension對future進行join方式的執行。例如,假設Cloud.runAlgorithm()方法返回一個Futrue[Int],能夠同時執行多個runAlgorithm方法:
val result1 = Cloud.runAlgorithm(10) val result2 = Cloud.runAlgorithm(20) val result3 = Cloud.runAlgorithm(30) val result = for { r1 <- result1 r2 <- result2 r3 <- result3 } yield (r1 + r2 + r3) result onSuccess { case result => println(s"total = $result") }
1) Loan Pattern: 確保打開的資源(如文件、數據庫鏈接)可以在操做完畢後被安全的釋放。
Loan Pattern的通用格式以下:
def using[A](r : Resource)(f : Resource => A) : A = try { f(r) } finally { r.dispose() }
這個格式針對Resource類型進行操做。還有一種作法是:只要實現了close方法,均可以運用Loan Pattern:
def using[A <: def close():Unit, B][resource: A](f: A => B): B = try { f(resource) } finally { resource.close() }
以FileSource爲例:
using(io.Source.fromFile("example.txt")) { source => { for (line <- source.getLines) { println(line) } } }
2) Cake Pattern: 利用self type實現依賴注入
例如,對於DbAccessor而言,須要提供不一樣的DbConnectionFactory來建立鏈接,從而訪問不一樣的Data Source。
trait DbConnectionFactory { def createDbConnection: Connection } trait SybaseDbConnectionFactory extends DbConnectionFactory… trait MySQLDbConnectionFactory extends DbConnectionFactory…
運用Cake Pattern,DbAccessor的定義應該爲:
trait DbAccessor { this: DbConnectionFactory => //… }
因爲DbAccessor使用了self type,所以能夠在DbAccessor中調用DbConnectionFactory的方法createDbConnection()。客戶端在建立DbAccessor時,能夠根據須要選擇混入的DbConnectionFactory:
val sybaseDbAccessor = new DbAccessor with SybaseDbConnectionFactory
固然,也能夠定義object:
object SybaseDbAccessor extends DbAccessor with SybaseDbConnectionFactory object MySQLDbAccessor extends DbAccessor with MySQLDbConnectionFactory
1) 測試類應該與被測試類處於同一包下。若是使用Spec2或ScalaTest的FlatSpec等,則測試類的命名應該爲:被測類名 + Spec;若使用JUnit等框架,則測試類的命名爲:被測試類名 + Test
2) 測試含有具體實現的trait時,可讓被測試類直接繼承Trait。例如:
trait RecordsGenerator { def generateRecords(table: List[List[String]]): List[Record] { //... } } class RecordsGeneratorSpec extends FlatSpec with ShouldMatcher with RecordGenerator { val table = List(List("abc", "def"), List("aaa", "bbb")) it should "generate records" in { val records = generateRecords(table) records.size should be(2) } }
3) 若要對文件進行測試,能夠用字符串僞裝文件:
type CsvLine = String def formatCsv(source: Source): List[CsvLine] = { source.getLines(_.replace(", ", "|")) }
formatCsv須要接受一個文件源,例如Source.fromFile("testdata.txt")。但在測試時,能夠經過Source.fromString方法來生成formatCsv須要接收的Source對象:
it should "format csv lines" in { val lines = Source.fromString("abc, def, hgi\n1, 2, 3\none, two, three") val result = formatCsv(lines) result.mkString("\n") should be("abc|def|hgi\n1|2|3\none|two|three") }