程式設計師通常會努力寫出完美的軟體,但通常都會失敗。
事情總是會出乎意料地出錯,而我們必須有能力處理這些狀況。
有些語言設計者認為,首要之務是盡快偵測到錯誤,然後結束執行,並附上有助於除錯的說明訊息。
資料科學領域的語言則傾向採取更細膩的做法。 有些錯誤嚴重到必須立刻結束程式,但很多時候,比較好的做法是把問題標記起來、留待之後處理,然後繼續執行。
我們在虛無概念中看過,Julia 針對有問題的值提供了各種佔位符號:missing、NaN和Inf。
在特定情況下,這些做法是否比結束程式更好,取決於程式設計師的判斷。
在深入細節之前,先說明一下用詞:Julia 的文件大致上把「error」和「exception」這兩個詞視為可互換。 底下的內容可能也同樣前後不一致。
課程大綱進行到這裡,你一定已經看過不少 Julia 的錯誤訊息了吧。 例如:
julia> Int(3.14)
ERROR: InexactError: Int64(3.14)
把浮點數轉型成整數會損失精確度,因此會得到InexactError。
InexactError是一種型別,是 Julia 眾多內建標準型別(目前有 25 種)的其中之一。
它們全都是Exception的子型別:
julia> supertype(InexactError)
Exception
throw()有些標準錯誤型別,或許在你自己的程式碼裡也用得上。
和所有具體型別一樣,這些錯誤也有建構子。 它們接受的引數各不相同,所以請查閱文件,了解你想用的那個型別。
julia> DomainError(42, "out of range")
DomainError(42, "out of range")
要使用這個錯誤,請把建構子包在throw()函式裡:
julia> throw(DomainError(42, "out of range"))
ERROR: DomainError with 42:
out of range
error()如果想走快速簡便的路線,error()函式會很方便。
它接受一個字串(或字串的組成部分)作為引數:
julia> happy = false;
julia> happy || error("😞 something went wrong")
ERROR: 😞 something went wrong
建立新的錯誤型別,原則上非常簡單。
只要再加上一個Exception的子型別就行了:
julia> struct MyError <: Exception end
julia> throw(MyError)
ERROR: MyError
斷言的基本想法是「這個敘述應該是對的,如果不對就大聲抱怨」。 這個做法主要在除錯時有價值,因為正式環境的程式碼不應該讓斷言失敗。
我們在型別概念中看過,我們可以加上型別斷言,例如檢查函式的回傳型別。
julia> 42::Number
42
julia> "two"::Number
ERROR: TypeError: in typeassert, expected Number, got a value of type String
更一般地說,@assert巨集讓我們可以測試任何算出布林值的運算式:
julia> n = 22;
julia> @assert isodd(n) "n must be odd"
ERROR: AssertionError: n must be odd
try...catch
有些錯誤注定是致命的,但很多時候,我們希望程式能優雅地復原。
根據預設,錯誤會立刻結束目前的函式,並把這個錯誤(連同說明訊息)傳給呼叫它的函式。
這個過程會沿著呼叫堆疊一路往上,直到頂層程式碼帶著錯誤訊息結束為止。
在任何階段,都可以用try...catch區塊攔截這個錯誤,試著處理它。
julia> n = -1;
julia> try
log_n = log(n)
catch problem
if problem isa DomainError # number out of range
# See next section for more on @warn and @info
@warn "you may have supplied a negative real number: $n"
@info "trying with complex argument"
log_n = log(Complex(n)) # fallback calculation
elseif problem isa MethodError # no idea what n is
@error "please supply a valid argument"
else
rethrow() # the error could be anything else
end
end
┌ Warning: you may have supplied a negative real number: -1
└ @ Main REPL[3]:5
[ Info: trying with complex argument
0.0 + 3.141592653589793im # success
在上面的例子裡,log(n)需要n是正實數,或是任意的複數。
try ... catch會攔下負實數造成的問題,並以數學符號回傳正確的複數答案iπ。
如果你傳入的是字串之類的引數,那就沒得補救,只能請使用者修正。
作為最後的萬用防線,我們針對既不是DomainError也不是MethodError的情況加上了rethrow()。
注意: 有時候,try...catch正是你需要的,但請避免過度使用。
如果能改用if...else區塊,效能會比攔截例外好得多。
請注意,前面討論過的error()函式,別和@error巨集搞混了。
這個函式會產生例外,除非被攔截,否則它會沿著呼叫堆疊往上傳。
@error巨集和它的同伴@debug、@info和@warn一樣,都屬於Logging模組,用途是產生說明訊息,而不改變程式的流程。
輸出預設會送到終端機(並依嚴重程度標上顏色),不過在實際的應用程式裡,還有很多其他的可能性。
julia> @warn "Something looks not quite right"
┌ Warning: Something looks not quite right
└ @ Main REPL[55]:1
julia> @error "Panic!"
┌ Error: Panic!
└ @ Main REPL[56]:1
也請參考前面try...catch一節的範例。
Elena 是報紙工廠的新任品質經理。 她才剛加入公司,便決定檢視工廠裡的一些流程,看看有什麼可以改善的地方。 她發現技術人員做了許多手動的品質檢查。她看到自動化的大好機會,於是請你這位自由接案的開發者開發一套軟體來監控部分機器。
你的第一個任務是寫一套軟體來監控生產室的濕度。公司的軟體上已經連接了一個感測器,會定期回傳房間的濕度百分比。
你需要在軟體中實作一個函式,當濕度百分比過高時拋出錯誤。
如果濕度在可接受的範圍內,就會新增一筆 Info 日誌。
這個函式應該命名為 humiditycheck,並接受濕度百分比作為引數。
如果百分比超過 70%,你應該以 ErrorException 中止(確切的訊息不重要,但必須包含測得的濕度值)。
否則,新增一筆 Info 日誌,訊息為 "humidity level check passed: h%",其中 h 是濕度百分比。
julia> humiditycheck(60)
[ Info: humidity level check passed: 60%
julia> humiditycheck(100)
ERROR: humidity check failed: 100%
Elena 對你的第一項任務非常滿意,並請你負責監控機器的溫度。 在和技術人員 Greg 聊天時,你得知如果機器的溫度超過 500°C,技術人員就會開始擔心過熱。
這台機器裝有一個感測器,用來測量它的內部溫度。 你要知道,這個感測器非常敏感,經常會壞掉。 這種情況下,技術人員就需要更換它。
你的工作是實作一個函式 temperaturecheck,它接受溫度作為引數,並在一切正常時新增一筆日誌,或是在感測器故障或機器開始過熱時拋出錯誤。
由於你之後需要根據錯誤做出不同的反應,因此你需要一套機制來區分這兩種錯誤。
nothing。
這種情況下,你應該以 ArgumentError 中止(訊息不重要)。DomainError。"temperature check passed: t °C",其中 t 是溫度。julia> temperaturecheck(nothing)
ERROR: ArgumentError: sensor is broken
julia> temperaturecheck(800)
ERROR: DomainError with 800:
"overheating detected"
julia> temperaturecheck(500)
[ Info: temperature check passed: 500 °C
在下一個任務中,你需要定義一個更通用、能涵蓋所有情況的錯誤。
實作細節不重要,只要它是一個錯誤,且名稱是 MachineError 即可。
你可以自由加入你覺得有幫助的欄位和訊息。
現在你的機器可以偵測錯誤,而且你也有了自訂的機器錯誤,接著就來新增一個包裝函式,用來回報一切運作的情況吧。 除了回傳前面幾個函式的日誌之外,這個包裝函式還需要根據發生的任何一種(或多種)失敗類型來新增日誌。
ErrorException,應該新增一筆 Error 日誌,訊息為 "humidity level check failed: h%",其中 h 是濕度百分比。ArgumentError,應該新增一筆 Warn 日誌,訊息為 "sensor is broken"。DomainError,應該新增一筆 Error 日誌,訊息為 "overheating detected: t °C",其中 t 是溫度。MachineError。humiditycheck 和 temperaturecheck 的日誌。實作一個函式 machinemonitor(),接受濕度和溫度作為引數。
julia> machinemonitor(42, 450)
[ Info: humidity level check passed: 42%
[ Info: temperature check passed: 450 °C
julia> machinemonitor(42, 550)
[ Info: humidity level check passed: 42%
┌ Error: overheating detected: 550 °C
└ @ Main # output truncated
Error: MachineError
julia> machinemonitor(82, 521)
┌ Error: humidity level check failed: 82%
└ @ Main # output truncated
┌ Error: overheating detected: 521 °C
└ @ Main # output truncated
Error: MachineError
julia> machinemonitor(42, nothing)
[ Info: humidity level check passed: 42%
┌ Warning: sensor is broken
└ @ Main # output truncated
Error: MachineError