定义异常
异常是一类特殊的可以被程序员捕获并处理的错误,是程序执行时出现的一系列不正常行为的统称,例如,数组越界、除零错误、计算溢出、非法输入等。为了保证系统的正确性和健壮性,很多软件系统中都包含大量的代码用于错误检测和错误处理。
异常不属于程序的正常功能,一旦发生异常,要求程序必须立即处理,即将程序的控制权从正常功能的执行处转移至处理异常的部分。仓颉编程语言提供异常处理机制用于处理程序运行时可能出现的各种异常情况。
在仓颉中,异常类有 Error 和 Exception
Error 类描述仓颉语言运行时,系统内部错误和资源耗尽错误,应用程序不应该抛出这种类型错误,如果出现内部错误,只能通知给用户,尽量安全终止程序。
Exception 类描述的是程序运行时的逻辑错误或者 IO 错误导致的异常,例如数组越界或者试图打开一个不存在的文件等,这类异常需要在程序中捕获处理。
- 用户不可以通过继承仓颉语言内置的 Error 或其子类类来自定义异常,但是可以继承内置的 Exception 或其子类来自定义异常
open class FatherException <: Exception {
public init() {
super("This is FatherException.")
}
public open override func getClassName(): String {
"FatherException"
}
}
class ChildException <: FatherException {
public init() {
super("This is ChildException.")
}
public open override func getClassName(): String {
"ChildException"
}
}
throw和处理异常
由于异常是 class 类型,只需要按 class 对象的构建方式去创建异常即可。
仓颉语言提供 throw 关键字,用于抛出异常。用 throw 来抛出异常时,throw 之后的表达式必须是 Exception 的子类型(同为异常的 Error 不可以手动 throw ),如 throw ArithmeticException("I am an Exception!") (被执行到时)会抛出一个算术运算异常。
throw 关键字抛出的异常需要被捕获处理。若异常没有被捕获,则由系统调用默认的异常处理函数。
异常处理由 try 表达式完成,可分为:
不涉及资源自动管理的普通 try 表达式;
会进行资源自动管理 try-with-resources 表达式。
普通 try 表达式
- 普通 try 表达式包括三个部分:try 块,catch 块和 finally 块。
try 块,以关键字 try 开始,后面紧跟一个由表达式与声明组成的块(用一对花括号括起来,定义了新的局部作用域,可以包含任意表达式和声明,后简称“块”),try 后面的块内可以抛出异常,并被紧随的 catch 块所捕获并处理(如果不存在 catch 块或未被捕获,则在执行完 finally 块后,该异常继续被抛出)。
catch 块,一个普通 try 表达式可以包含零个或多个 catch 块(当没有 catch 块时必须有 finally 块)。每个 catch 块以关键字 catch 开头,后跟一条 catchPattern 和一个块,catchPattern 通过模式匹配的方式匹配待捕获的异常。一旦匹配成功,则交由其后跟随的块进行处理,并且忽略它后面的其他 catch 块。当某个 catch 块可捕获的异常类型均可被定义在它前面的某个 catch 块所捕获时,会在此 catch 块处报“catch 块不可达”的 warning。
finally 块,以关键字 finally 开始,后面紧跟一个块。原则上,finally 块中主要实现一些“善后”的工作,如释放资源等,且要尽量避免在 finally 块中再抛异常。并且无论异常是否发生(即无论 try 块中是否抛出异常),finally 块内的内容都会被执行(若异常未被处理,执行完 finally 块后,继续向外抛出异常)。一个 try 表达式在包含 catch 块时可以不包含 finally 块,否则必须包含 finally 块。
- try 后面紧跟的块以及每个 catch 块的的作用域互相独立。
main() {
try {
throw NegativeArraySizeException("I am an Exception!")
} catch (e: NegativeArraySizeException) {
println(e)
println("NegativeArraySizeException is caught!")
}
println("This will also be printed!")
}
/*
NegativeArraySizeException: I am an Exception!
NegativeArraySizeException is caught!
This will also be printed!
*/
- catchPattern 中引入的变量作用域级别与 catch 后面的块中变量作用域级别相同,在 catch 块中再次引入相同名字会触发重定义错误。
main() {
try {
throw NegativeArraySizeException("I am an Exception!")
} catch (e: NegativeArraySizeException) {
println(e)
let e = 0 // Error, redefinition
println(e)
println("NegativeArraySizeException is caught!")
}
println("This will also be printed!")
}
- try 表达式可以出现在任何允许使用表达式的地方。try 表达式的类型的确定方式,与 if、match 表达式等多分支语法结构的类型的确定方式相似,为 finally 分支除外的所有分支的类型的最小公共父类型。例如下面代码中的 try 表达式和变量 x 的类型均为 E 和 D 的最小公共父类型 D;finally 分支中的 C() 并不参与公共父类型的计算(若参与,则最小公共父类型会变为 C)。
open class C { }
open class D <: C { }
class E <: D { }
main () {
let x = try {
E()
} catch (e: Exception) {
D()
} finally {
C()
}
0
}
Try-with-resources 表达式
Try-with-resources 表达式主要是为了自动释放非内存资源。不同于普通 try 表达式,try-with-resources 表达式中的 catch 块和 finally 块均是可选的,并且 try 关键字其后的块之间可以插入一个或者多个 ResourceSpecification 用来申请一系列的资源(ResourceSpecification 并不会影响整个 try 表达式的类型)。这里所讲的资源对应到语言层面即指对象,因此 ResourceSpecification 其实就是实例化一系列的对象(多个实例化之间使用“,”分隔)。
class R <: Resource {
public func isClosed(): Bool {
true
}
public func close(): Unit {
print("R is closed")
}
}
main() {
try (r = R()) {
println("Get the resource")
}
}
/*
Get the resource
*/
- try 关键字和 {} 之间引入的名字,其作用域与 {} 中引入的变量作用域级别相同,在 {} 中再次引入相同名字会触发重定义错误。
class R <: Resource {
public func isClosed(): Bool {
true
}
public func close(): Unit {
print("R is closed")
}
}
main() {
try (r = R()) {
println("Get the resource")
let r = 0 // Error, redefinition
println(r)
}
}
- Try-with-resources 表达式中的 ResourceSpecification 的类型必须实现 Resource 接口,并且尽量保证其中的 isClosed 函数不要再抛异常:
interface Resource {
func isClosed(): Bool
func close(): Unit
}
- try-with-resources 表达式中一般没有必要再包含 catch 块和 finally 块,也不建议用户再手动释放资源。因为 try 块执行的过程中无论是否发生异常,所有申请的资源都会被自动释放,并且执行过程中产生的异常均会被向外抛出。但是,如果需要显式地捕获 try 块或资源申请和释放过程中可能抛出的异常并处理,仍可在 try-with-resources 表达式中包含 catch 块和 finally 块:
class R <: Resource {
public func isClosed(): Bool {
true
}
public func close(): Unit {
print("R is closed")
}
}
main() {
try (r = R()) {
println("Get the resource")
} catch (e: Exception) {
println("Exception happened when executing the try-with-resources expression")
} finally {
println("End of the try-with-resources expression")
}
}
/*
Get the resource
End of the try-with-resources expression
*/
CatchPattern 进阶介绍
大多数时候,我们只想捕获某一类型和其子类型的异常,这时候我们使用 CatchPattern 的类型模式来处理;但有时也需要所有异常做统一处理(如此处不该出现异常,出现了就统一报错),这时可以使用 CatchPattern 的通配符模式来处理。
- 类型模式在语法上有两种格式:
Identifier: ExceptionClass。此格式可以捕获类型为 ExceptionClass 及其子类的异常,并将捕获到的异常实例转换成 ExceptionClass,然后与 Identifier 定义的变量进行绑定,接着就可以在 catch 块中通过 Identifier 定义的变量访问捕获到的异常实例。
Identifier: ExceptionClass_1 | ExceptionClass_2 | ... | ExceptionClass_n。此格式可以通过连接符 | 将多个异常类进行拼接,连接符 | 表示“或”的关系:可以捕获类型为 ExceptionClass_1 及其子类的异常,或者捕获类型为 ExceptionClass_2 及其子类的异常,依次类推,或捕获类型为 ExceptionClass_n 及其子类的异常(假设 n 大于 1)。当待捕获异常的类型属于上述“或”关系中的任一类型或其子类型时,此异常将被捕获。但是由于无法静态地确定被捕获异常的类型,所以被捕获异常的类型会被转换成由 | 连接的所有类型的最小公共父类,并将异常实例与 Identifier 定义的变量进行绑定。因此在此类模式下,catch 块内只能通过 Identifier 定义的变量访问 ExceptionClass_i(1 <= i <= n) 的最小公共父类中的成员变量和成员函数。当然,也可以使用通配符代替类型模式中的 Identifier,差别仅在于通配符不会进行绑定操作。
main(): Int64 {
try {
throw IllegalArgumentException("This is an Exception!")
} catch (e: OverflowException) {
println(e.message)
println("OverflowException is caught!")
} catch (e: IllegalArgumentException | NegativeArraySizeException) {
println(e.message)
println("IllegalArgumentException or NegativeArraySizeException is caught!")
} finally {
println("finally is executed!")
}
return 0
}
/*
This is an Exception!
IllegalArgumentException or NegativeArraySizeException is caught!
finally is executed!
*/
关于“被捕获异常的类型是由 | 连接的所有类型的最小公共父类”的示例:
open class Father <: Exception {
var father: Int32 = 0
}
class ChildOne <: Father {
var childOne: Int32 = 1
}
class ChildTwo <: Father {
var childTwo: Int32 = 2
}
main() {
try {
throw ChildOne()
} catch (e: ChildTwo | ChildOne) {
println("ChildTwo or ChildOne?")
}
}
/*
ChildTwo or ChildOne?
*/
- 通配符模式的语法是 _,它可以捕获同级 try 块内抛出的任意类型的异常,等价于类型模式中的 e: Exception,即捕获 Exception 子类所定义的异常。
// Catch with wildcardPattern.
try {
throw OverflowException()
} catch (_) {
println("catch an exception!")
}
常见运行时异常
在仓颉语言中内置了最常见的异常类,开发人员可以直接使用。
使用Option
- 因为 Option 类型可以同时表示有值和无值两种状态,而无值在某些情况下也可以理解为一种错误,所以 Option 类型也可以用作错误处理。
func getOrThrow(a: ?Int64) {
match (a) {
case Some(v) => v
case None => throw NoneValueException()
}
}
因为 Option 是一种非常常用的类型,所以仓颉为其提供了多种解构方式,以方便 Option 类型的使用,具体包括:模式匹配、getOrThrow 函数、coalescing 操作符(??),以及问号操作符(?)。
模式匹配:因为 Option 类型是一种 enum 类型,所以可以使用上文提到的 enum 的模式匹配来实现对 Option 值的解构。
func getString(p: ?Int64): String{
match (p) {
case Some(x) => "${x}"
case None => "none"
}
}
main() {
let a = Some(1)
let b: ?Int64 = None
let r1 = getString(a)
let r2 = getString(b)
println(r1)
println(r2)
}
/*
1
none
*/
- coalescing 操作符(??):对于 ?T 类型的表达式 e1,如果希望 e1 的值等于 None 时同样返回一个 T 类型的值 e2,可以使用 ?? 操作符。对于表达式 e1 ?? e2,当 e1 的值等于 Some(v) 时返回 v 的值,否则返回 e2 的值。
main() {
let a = Some(1)
let b: ?Int64 = None
let r1: Int64 = a ?? 0
let r2: Int64 = b ?? 0
println(r1)
println(r2)
}
/*
1
0
*/
- 问号操作符(?):? 需要和 . 或 () 或 [] 或 {}(特指尾随 lambda 调用的场景)一起使用,用以实现 Option 类型对 .,(),[] 和 {} 的支持。以 . 为例((),[] 和 {}同理),对于 ?T1 类型的表达式 e,当 e 的值等于 Some(v) 时,e?.b 的值等于 Option<T2>.Some(v.b),否则 e?.b 的值等于 Option<T2>.None,其中 T2 是 v.b 的类型。
struct R {
public var a: Int64
public init(a: Int64) {
this.a = a
}
}
let r = R(100)
let x = Some(r)
let y = Option<R>.None
let r1 = x?.a // r1 = Option<Int64>.Some(100)
let r2 = y?.a // r2 = Option<Int64>.None
- 问号操作符(?)支持多层访问。
struct A {
let b: B = B()
}
struct B {
let c: Option<C> = C()
let c1: Option<C> = Option<C>.None
}
struct C {
let d: Int64 = 100
}
let a = Some(A())
let a1 = a?.b.c?.d // a1 = Option<Int64>.Some(100)
let a2 = a?.b.c1?.d // a2 = Option<Int64>.None
- getOrThrow 函数:对于 ?T 类型的表达式 e,可以通过调用 getOrThrow 函数实现解构。当 e 的值等于 Some(v) 时,getOrThrow() 返回 v 的值,否则抛出异常。
main() {
let a = Some(1)
let b: ?Int64 = None
let r1 = a.getOrThrow()
println(r1)
try {
let r2 = b.getOrThrow()
} catch (e: NoneValueException) {
println("b is None")
}
}
/*
1
b is None
*/