程序员一般都力求写出完美的软件,但往往做不到。
事情总会出乎意料地出错,而我们需要有能力应对。
有些语言设计者认为,首要任务是尽快检测到错误,然后终止执行,并给出一条有助于调试的提示信息。
数据科学类的语言往往采取更细腻的做法。有些错误非常严重,必须立即终止;但很多时候,更好的做法是把问题标记出来留待以后处理,然后继续执行。
我们在空值概念中看到,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π。
比如,如果你传入的是字符串实参,那就无法恢复,只能请用户改正。
作为最后的兜底,我们加了 rethrow(),用来处理既不是 DomainError 也不是 MethodError 的情况。
***注意:***有时候 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