控制結(jié)構(gòu)

2018-02-24 15:48 更新

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

遞歸

用遞歸術(shù)語(yǔ)來(lái)表達(dá)你的問(wèn)題常常會(huì)使問(wèn)題簡(jiǎn)化,如果應(yīng)用了尾遞歸優(yōu)化(可以通過(guò)@tailrec注釋檢測(cè)),編譯器甚至?xí)⒛愕拇a轉(zhuǎn)換為正常的循環(huán)。對(duì)比一個(gè)標(biāo)準(zhǔ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
     }
   }
 }

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

考慮尾遞歸的實(shí)現(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)
   }
 }

每次迭代都是一個(gè)明確定義的歷史清白的變量,并且沒(méi)有引用單元:到處都是不變的(invariants)。更容易實(shí)現(xiàn),也容易閱讀。也沒(méi)有性能方面的懲罰:因?yàn)榉椒ㄊ俏策f歸的,編譯器會(huì)轉(zhuǎn)換為標(biāo)準(zhǔn)的命令式的循環(huán)。

返回(Return)

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

Returns可以用于切斷分支和建立不變量(establish invariants)。這減少了嵌套,并且容易推斷后續(xù)的代碼的正確性,從而幫助了讀者。這尤其適用于衛(wèi)語(yǔ)句(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"
 }

上面是針對(duì)命令式語(yǔ)言的,在Scala中鼓勵(lì)省略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會(huì)有隱性開(kāi)銷:當(dāng)在閉包內(nèi)部使用時(shí)。

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

   // process...
 }

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

for循環(huán)和for推導(dǎo)

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

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

如果容器延遲計(jì)算(delays computation)會(huì)引起運(yùn)行時(shí)錯(cuò)誤,使返回不在本地上下文 (making the return nonlocal)

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

要求require和斷言(assert)

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

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

相反,require用于表達(dá)API契約:

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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)