第五章 : 錯誤處理

??????發表於2012-08-14

編寫在正常情況下每次都按預期執行的程式是一個好的開始。然而真正具有挑戰性的是讓你的程式在預料外的情況下作出合適的反應。

一個程式可以遇到的有問題的情形可以分為兩類:程式設計師錯誤或者是真正的問題。所謂的程式設計師錯誤指的是,比如,有人在呼叫函式時忘了傳遞一個必須的引數。而真正的問題,則是如一個程式要求使用者輸入一個名字然而使用者卻輸入空的字串的情形。這是程式設計師不可避免的。

大致上,對於程式設計上的錯誤,我們會尋找並修正它們,而對於真正的錯誤要靠程式碼檢查,並執行適當的處理(例如,再問一遍名字),最少,也要失敗得明確和乾淨。。


確定錯誤的型別是很重要的,下面請看看我們舊的power(冪)函式。

function power(base, exponent) {
  var result = 1;
  for (var count = 0; count < exponent; count++)
    result *= base;
  return result;
}

如果某極客呼叫 power("Rabbit", 4), 很明顯這是一個程式設計師型別的錯誤,但如果他呼叫的是 power(9, 0.5)? 顯然這個函式並不能處理那個分數冪, 但是,數學上來說,將一個數字的冪提高半個單位是十分合理的, (事實上我們可以通過Math.pow函式處理這個). 在第二種情況下我們非常清楚地看到power函式接受的輸入型別,所以一個較好的方法是明確地註釋函式的引數型別。


但如果一個函式遇到的是它不能解決的問題,它應該做什麼呢?第四章的時候我們寫了以下的函式:

function between(string, start, end) {
  var startAt = string.indexOf(start) + start.length;
  var endAt = string.indexOf(end, startAt);
  return string.slice(startAt, endAt);
}

如果一個字串的開始和結束並不存在的話,indexOf 函式將返回 -1 而顯然這個結果並不合理: 比如呼叫between("Your mother!", "{-", "-}") 則會返回 "our mother".

當程式執行的時候,如果一個函式被這樣數值錯誤的方式呼叫的話,它還是會返回一個字串,然而,這是否為我們所期望的結果?這個程式在呼叫後還會繼續做其它的工作。但因為它的返回值是錯誤的,所以它後面過程的結果也是錯誤。很不幸的,最後你看到錯誤的時候也許這個錯誤的返回結果已經被數十個其它的函式所呼叫了,那麼你在定位錯誤程式碼的時候將會非常的困難。

在某些情況下,你並不擔心一個函式因為錯誤的輸入而發生問題。例如,在你可以確定你的函式只會被少量的呼叫並且你可以證明這些呼叫的地方總是提供合適的輸入的話,那麼為了處理出問題情況而把這個函式弄得又長又噁心並不值得。

但在大多數情況下,函式安靜的失效將會使得這個函式變得難以使用,甚至是危險的。如果呼叫的函式想知道被呼叫的函式工作是否正常它該怎麼辦呢?注意,在這個時候,被呼叫的函式並不能返回這個資訊,那麼呼叫者只能重做被呼叫者的所有工作並對比兩個結果。這太壞了。我們的解決方法是被呼叫的函式可以返回一個特殊的值,如undefined 和 False.

function between(string, start, end) {
  var startAt = string.indexOf(start);
  if (startAt == -1)
    return undefined;
  startAt += start.length;
  var endAt = string.indexOf(end, startAt);
  if (endAt == -1)
    return undefined;

  return string.slice(startAt, endAt);
}

你可以看到這段錯誤檢查的程式碼並沒有把這個函式變得更漂亮。但是現在函式的呼叫者可以像下面的例子一樣做一些事情了:

var input = prompt("Tell me something", "");
var parenthesized = between(input, "(", ")");
if (parenthesized != undefined)
  print("You parenthesized '", parenthesized, "'.");

在許多情況下返回一個特殊值是代表錯誤的一個很好的方法,但是它還是有些弱點。首先,如果這個函式本身可能的返回值已經涵蓋了所有的可能怎麼辦?下面的例子中函式將返回陣列中最後的一個元素:

function lastElement(array) {
  if (array.length > 0)
    return array[array.length - 1];
  else
    return undefined;
}

show(lastElement([1, 2, undefined]));

所以這個陣列到底有沒有最後一個元素呢?看看lastElement的返回值吧,這可說不清楚。

第二,返回特殊的值可能把整個程式弄的很糟。如果一小段程式碼被呼叫的10次,每次呼叫者都要檢查返回值是否為undefined。同時,如果一個函式呼叫了上面的between函式,並且呼叫的函式中沒有失敗恢復的策略,那麼呼叫的函式本身也將返回一個undefined或者其它的返回值,然後等待這個值被呼叫函式的更上層的呼叫者檢查。

有的時候,一些奇怪的事情會發生,然後現在執行的東西會馬上中止並向上遞迴到可以處理問題的地方。

還好,我們很幸運,很多的程式語言提供了通常我們稱之為異常處理的方法。


異常處理的原理是:一段程式碼可以丟擲一個異常,就好象來自函式的一個強力的返回值直接跳到執行程式的最高階別呼叫者上。這被稱之為堆疊回退,你可能還記得第三章提過的函式呼叫堆。一個異常的傳遞可以一步步跳過這些中間呼叫。

如果異常總是被拋到程式的堆底,那麼它並沒有什麼用,它只是提供了一個新穎的方法來中止你的程式罷了。還好,我們可以在堆的特定位置捕獲這些異常。‘捕獲’使得你可以在程式發生錯誤之後的位置上進行立即進行處理。

一個例子:

function lastElement(array) {
  if (array.length > 0)
    return array[array.length - 1];
  else
    throw "Can not take the last element of an empty array.";
}

function lastElementPlusTen(array) {
  return lastElement(array) + 10;
}

try {
  print(lastElementPlusTen([]));
}
catch (error) {
  print("Something went wrong: ", error);
}

throw 是用來丟擲異常的關鍵字。這個關鍵字提供了一個可捕獲的異常:當程式碼丟擲異常以後,catch程式碼塊將被執行。catch關鍵字中的圓括號為catch程式碼塊的內部指定了代表異常變數的變數名。

注意到函式 lastElementPlusTen 完全的忽略了lastElement 出錯的可能。這就是使用異常處理錯誤的優勢——只在需要的地方執行處理問題的程式碼,對於呼叫這個函式的函式,它們可以忽略這個問題了。

好,差不多了。


考慮到下面的可能:一個函式 processThing 想要在高層建立一個變數currentThing使得其它函式可以訪問,通常你只要傳遞引數就可以解決這個問題,但是現在假設這個方法不適用。在函式結束以後,currentThing會被重新設定為null。

var currentThing = null;

function processThing(thing) {
  if (currentThing != null)
    throw "Oh no! We are already processing a thing!";

  currentThing = thing;
  /* do complicated processing... */
  currentThing = null;
}

但是如果在複雜的處理過程中發生異常怎麼辦?processThing將會因為異常而脫離棧而currentThing將永遠不會被置為null。

還好,try語句塊還可以跟隨一個finally關鍵字,意味著無論發生什麼事內部的程式碼都會在try執行後執行。如果一個函式要在最後做些清理,那麼清理的程式碼就應該放在finally語句塊上:

function processThing(thing) {
  if (currentThing != null)
    throw "Oh no! We are already processing a thing!";

  currentThing = thing;
  try {
    /* do complicated processing... */
  }
  finally {
    currentThing = null;
  }
}

JavaScript環境中的許多錯誤將會導致丟擲一個異常,如:

try {
  print(Sasquatch);
}
catch (error) {
  print("Caught: " + error.message);
}

在這種模式下總是會丟擲一個包含合適錯誤資訊(在message 屬性裡)的錯誤物件。你也可以使用new關鍵字和Error構造器來丟擲類似的物件:

throw new Error("Fire!");

當一個異常被拋到底層而沒有被捕獲的話,它將由環境進行處理,這將因為瀏覽器的不同而不同。有的時候這會被寫到一些日誌裡面去,有的時候會彈出一個描述錯誤的視窗。

由於輸入控制檯引發的錯誤輸出將會由控制檯進行捕獲,並在其他輸出中顯示。


程式設計師們幾乎都把異常看作單純的錯誤處理機制,然而本質上來說,只是另外一種形式的程式流程的控制。比如,他們可以被作為一種break語句來實現的遞迴功能。這裡有一個有點奇怪的函式,實現了判斷輸入引數是不是一個物件,和物件裡是否至少有7個真值:

var FoundSeven = {};

function hasSevenTruths(object) {
  var counted = 0;

  function count(object) {
    for (var name in object) {
      if (object[name] === true) {
        counted++;
        if (counted == 7)
          throw FoundSeven;
      }
      else if (typeof object[name] == "object") {
        count(object[name]);
      }
    }
  }

  try {
    count(object);
    return false;
  }
  catch (exception) {
    if (exception != FoundSeven)
      throw exception;
    return true;
  }
}

這裡函式內部的count函式對輸入object內的每個元素進行了遞迴呼叫。當變數達到7的時候輸出,然後就沒必要繼續數了。然而,只是從數到7的呼叫中返回結果並不會中止整個遞迴呼叫過程,某些呼叫還會進行下去。應此我們使用丟擲異常的方法,這將結束整個遞迴呼叫的過程並跳到catch語句塊內。

然而異常時單單返回true是不正確的。有的時候別的部分會出現錯誤,所以我們首先應當判斷丟擲的異常物件是否為FoundSeven 這個專門新建的異常。如果不是的話,hasSevenTruths的catch並不知道如何處理它,所以萬一在這種情況下,hasSevenTruths 應該丟擲它不能處理的異常。

這種情況在錯誤處理中非常普遍——你必須保證你的catch語句塊只處理它能處理的異常。象某些章節中的一些例子一樣直接丟擲一個字串並不是一個很好的做法,這讓人難以判斷捕獲到的異常的型別。更好的方法是丟擲一個獨特的值,例如上面的FoundSeven物件,或者使用一種新型別的物件,這將在第八章中講解。

相關文章