控制結(jié)構

2018-02-24 15:48 更新

函數(shù)式風格的程序傾向于需要更少的傳統(tǒng)的控制結(jié)構,并且使用聲明式風格寫的程序讀起來更好。這通常意味著打破你的邏輯,拆分到若干個小的方法或函數(shù),用匹配表達式(match expression)把他們粘在一起。函數(shù)式程序也傾向于更多面向表達式(expression-oriented):條件分支是同一類型的值計算,for(..) yield 表達式,以及遞歸都是司空見慣的。

遞歸

用遞歸術語來表達你的問題常常會使問題簡化,如果應用了尾遞歸優(yōu)化(可以通過@tailrec注釋檢測),編譯器甚至會將你的代碼轉(zhuǎn)換為正常的循環(huán)。對比一個標準的命令式版本的堆排序(fix-down):

 def fixDown(heap: Array[T], m: Int, n: Int): Unit = {
   var k: Int = m
   while (n >= 2*k) {
     var j = 2*k
     if (j < n && heap(j) < heap(j + 1))
       j += 1
     if (heap(k) >= heap(j))
       return
     else {
       swap(heap, k, j)
       k = j
     }
   }
 }

每次進入while循環(huán),我們工作在前一次迭代時污染過的狀態(tài)。每個變量的值是那一分支所進入函數(shù),當找到正確的位置時會在循環(huán)中返回。 (敏銳的讀者會在Dijkstra的“Go To聲明是有害的”一文找到相似的觀點)

考慮尾遞歸的實現(xiàn)[2]:

 @tailrec
 final def fixDown(heap: Array[T], i: Int, j: Int) {
   if (j < i*2) return

   val m = if (j == i*2 || heap(2*i) < heap(2*i+1)) 2*i else 2*i + 1
   if (heap(m) < heap(i)) {
     swap(heap, i, m)
     fixDown(heap, m, j)
   }
 }

每次迭代都是一個明確定義的歷史清白的變量,并且沒有引用單元:到處都是不變的(invariants)。更容易實現(xiàn),也容易閱讀。也沒有性能方面的懲罰:因為方法是尾遞歸的,編譯器會轉(zhuǎn)換為標準的命令式的循環(huán)。

返回(Return)

并不是說命令式結(jié)構沒有價值。在很多例子中它們很適合于提前終止計算而非對每個可能終止的點存在一個條件分支:的確在上面的fixDown函數(shù),如果我們已經(jīng)在堆的結(jié)尾,一個return用于提前終止。

Returns可以用于切斷分支和建立不變量(establish invariants)。這減少了嵌套,并且容易推斷后續(xù)的代碼的正確性,從而幫助了讀者。這尤其適用于衛(wèi)語句(guard clauses):

 def compare(a: AnyRef, b: AnyRef): Int = {
   if (a eq b)
     return 0

   val d = System.identityHashCode(a) compare System.identityHashCode(b)
   if (d != 0)
     return d

   // slow path..
 }

使用return增加了可讀性

 def suffix(i: Int) = {
   if      (i == 1) return "st"
   else if (i == 2) return "nd"
   else if (i == 3) return "rd"
   else             return "th"
 }

上面是針對命令式語言的,在Scala中鼓勵省略return

 def suffix(i: Int) =
   if      (i == 1) "st"
   else if (i == 2) "nd"
   else if (i == 3) "rd"
   else             "th"

但使用模式匹配更好:

 def suffix(i: Int) = i match {
   case 1 => "st"
   case 2 => "nd"
   case 3 => "rd"
   case _ => "th"
 }

注意,return會有隱性開銷:當在閉包內(nèi)部使用時。

 seq foreach { elem =>
   if (elem.isLast)
     return

   // process...
 }

在字節(jié)碼層實現(xiàn)為一個異常的捕獲/聲明(catching/throwing)對,用在頻繁的執(zhí)行的代碼中,會有性能影響。

for循環(huán)和for推導

for對循環(huán)和聚集提供了簡潔和自然的表達。 它在扁平化(flattening)很多序列時特別有用。for語法通過分配和派發(fā)閉包隱藏了底層的機制。這會導致意外的開銷和語義;例如:

 for (item <- container) {
   if (item != 2) return
 }

如果容器延遲計算(delays computation)會引起運行時錯誤,使返回不在本地上下文 (making the return nonlocal)

因為這些原因,常常更可取的是直接調(diào)用foreach, flatMap, map和filter —— 但在其意義清楚的時候使用for。

要求require和斷言(assert)

要求(require)和斷言(assert)都起到可執(zhí)行文檔的作用。兩者都在類型系統(tǒng)不能表達所要求的不變量(invariants)的場景里有用。 assert用于代碼假設的不變量(invariants) (內(nèi)部或外部的) 例如:(譯注,不變量 invariant 是指類型不可變,即不支持協(xié)變或逆變的類型變量)

 val stream = getClass.getResourceAsStream("someclassdata")
 assert(stream != null)

相反,require用于表達API契約:

 def fib(n: Int) = {
   require(n > 0)
   ...
 }
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號