2011年5月5日 星期四

4-6. 控制結構 --try,例外處理

例外處理幾乎是現代語言所必備,很重要,你一定會使用。
既然大家都熟悉,所以這裡不準備解釋例外處理的緣由,以及發生例外時的處理流程。

Scala 的例外處理與 Java 的例外處理大致類似,都是 try -- catch -- finally 所組成。
Scala 例外處理的語法如下
try {
  val socket = new Socket("www.a.com")
}
catch {
  case ex: IOException => ... //處理 IOException 的程式碼
  case ex: Exception => ... //處理 Exception 的程式碼
}

在例外處理這個議題上,Scala 與 Java 不同的地方有以下幾點:
1.catch 子句把所有的 exception 放在同一個 catch 子句處理。不再像 Java 一樣,一種 exception 需要一個 catch 子句,看起來比較簡潔。
2. catch 子句使用 pattern match 的語法。exception 發生時,會依序比對 case 子句,看哪一個符合,若符合,執行對應的程式碼,表示該 exception 被處理完成。pattern match 將在下篇中提及。
3. Scala 不再支援 checked exception:這是 Scala 與 Java 最大不同的地方。

所謂「checked exception」是 Java 的特殊設計,基本概念是要求 programmer 一定要處理掉程式中可能遇到的所有 Exception,若沒有處理,compiler 會發出 error。

在「checked exception」的原則下,method 要使用 throws 宣告自己可能產生的 exception。呼叫其他 method 時,被呼叫 method 的可能 exception 都需使用 catch 處理。若有某些 exception 不處理,表示本 method 執行時可能會產生這些沒處理到 exception,因此需要在自己的 method 使用 throws 宣告這些沒處理到的 exception,告知若有人要呼叫到本 method 者記得繼續處理這些 exception。

「checked exception」的處理對象是 Exception 以及其 subclass 的例外。若程式的例外情況不是 Exception 產生,那不會運用「checked exception」。比如例外情況是 Error 產生,compiler 並不會強制要求程式碼處理這種例外。

在「checked exception」原則下,你撰寫 Java 程式,一定常遇到 compiler 要求你加上某些 exception 的處理。這時你是感到 Java 很窩心,還時覺得很煩?若你感到很煩,很正常。

「checked exception」設計的出發點是好的,但實際使用時,常形成以下狀況
1. 造成沒處理的 exception,傳染到所有的可能用到的 method,導致這些 method 都需要使用 throws 宣告該 exception。這個現象以 IOException 最普遍與著名。
2. 為避免麻煩,programmer 乾脆在每個 method 內直接攔截所有的 exception,造成 checked exception 的原意盡失。
3. Programmer 花太多時間處理這些例外的狀況。

關於 Java 的 checked exception,論戰已久,Java 設計者認為這是一個輔助 programmer 的好工具,但實際應用卻不盡理想,因此 Scala 決定將這個概念移除。Scala 中,沒有所謂的 checked exception。

Scala 中,若有一些 exception 沒有處理,該 exception 就會往上層 method 傳遞。Compiler 不會檢查有哪些 exception 沒被處理。所以你只需要針對你想處理的 exception 處理就好,其他的 exception 可以完全不管。

若你的 Scala 程式碼想要給 Java 程式呼叫,也想要製造 checked exception 的效果,這時可以使用 @throws annotation 來達到。
下例的 print method() 會有 IOException 的 throws 宣告
throw exception
class S {
  @throws(classOf[IOException]) //這個 annotation 宣告 throws IOException
  def print() = ...
}
以下是實際的 compile 的結果
由上例,使用 javap 可以看到 p1 method 使 throws 子句,宣告該 method 產生 java.io.IOException。

我們使用 try ... catch ... finally 來處理 exception,那我們如果想在程式中產生一個 Exception 該如何?

在程式中拋出一個 Exception,作法與 Java 一樣,使用 throw 這一個 keyword。
以下為一簡單例子
throw new IOException("file not found") // 這個是 Scala 程式碼,雖然與 Java 程式碼相同

「Scala 所有 statement 都有 result value,那 throw statement 的 result value 什麼?」

throw statement 執行時,程式將會由該 statement 跳離,所以理論上,throw statement 的 result value 無法被取得,所以無意義。Scala 定義 throw statement 的 result value 是「沒有東西」。為特別表示這個「沒有東西」,特別定義一個 「Nothing」 這個  class 來表示,Nothing 這個 class 並沒有 instance,不像 Unit class 有一個 instance,為 ()。

「為何不使用 Unit 來作為 throw statement 的 retsult?」
其實,Unit 是有特殊用法,它用來與 Java 溝通,Java 的 void 在 Scala 的表示就是 Unit,所以 Unit 不能拿去給其他地方使用。我們可以想像「Unit 就是 Java void 的 class」,Unit 有相當重要的功用在。

Nothing 表示不可能有值,所以叫做「沒有東西」。不是代表「沒有值」,沒有值指的是 void,在 Scala 中使用 Unit 來表示。

最後,我們談一下 finally 的用法。

finally在 Scala 與在 Java 的用法一模一樣,使用 finally keyword 然後接上大括號所括住的程式碼。
try {
 ...
}
finally {
 //你的 finally 程式碼,通常用在 cleanup 工作上
}
請注意,finally 本意是設計來做 cleanup 處理,請不要將其他有意義的程式碼放在 finally 子句中。

try ... catch ... finally 整體是一個 statement,會有整體的 result value。
請不要把 try ... catch ... finally 分割成「try 一個 statement,catch 一個 statement,finally 一個 statement」三個 statement。但 try、catch、finally 這三個子句確實有自己的 result value。

「那整體的 result value 又是什麼?」
通常 result value 都是把最後一個執行的 statement 當成 result value。try ... catch ... finally 整體的 result value 也是同樣概念,但做了些微的調整。

在 try ... catch ... finally statement 中,不管有無產生 exception,最後一個執行的 statement 一定是 finally 的最後一個 statement。若按照「最後一個執行 statement 當 result value」的原則,finally 子句的 result value 將會是整體的 result value。

但因為 finally 主要用途是做 cleanup,不應該是正常工作的程式碼,所以使用 finally 子句的 result value 做為整體的 result value,會變成使用 cleanup 的結果,做為整體的結果。這是比較奇怪的作法。
由於上述原因,正常情況下,Scala 並不採用 finally 子句的 result value,而使用 try 子句或 catch 子句的 result value,做為整體 statement 的 result value,端視是否有 exception 產生。

我們把 try...catch...finally 執行,分成以下情況
1. 當沒有產生 exception 時,整體的 result value 就是 try 子句的 result value。
2. 若有 exception,且被 catch 所攔截下來,整體的 result value 就是該 exception handler 的 result value。
3. 若有 exception,且沒被 catch 所攔截下來:雖然會先執行 finally 再跳離,但 exception 沒被處理。這種 exception 沒被處理的情況,整體的 result value 是「沒有」,但不是 Nothing,Scala 對於這種情況完全沒定義。

不採用 finally 子句的 result value,是 Scala 的處理原則,但有一種狀況要特別小心,若 finally 子句使用 return 來結束 method,這時就有點不同了。
由於 return statement 要求將 return 的參數當成 method 的 return 值並結束,若 finally 包含 return,當程式執行到 finally 的 return 時,method 馬上會被結束並傳回。
這種現象將很容易造成誤解。請看下例
def m1() = {
  try {
    return 1 //這裡有個 return 值,但會被 finally 的 return 蓋掉
  }
  finally {
    return 2 //finally 的 return 值,決定真正 method 的 return 值
  }
}
m1() // 這裡會得到 2
如果不使用 return 的情況
def m1() = {
  try {
    1 // 這裡是 try 子句最後執行的 statement,會被當成 try...finally 的 result,最後變成 method 的 return 值
  }
  finally {
    2 //finally 的 statement,被當成 cleanup,不會成為 try...finally 的 result
  }
}
m1() // 這裡會得到 1
以上的例子,很清楚看到 return statement 的強制力,容易造成與預期不同的狀況,為避免這種問題,不使用 return 是一個好方法。Scala 建議不要使用 return statement,這裡是一個很好例證。

沒有留言:

張貼留言