NOTE阅读文档版本:
语言规约 Cangjie-0.53.18-Spec
具体开发指南 Cangjie-LTS-1.0.3
在阅读 了解仓颉的语言规约时, 难免会涉及到一些仓颉的示例代码, 但 我们对仓颉并不熟悉, 所以可以用 仓颉在线体验 快速验证
有条件当然可以直接 配置Canjie-SDK
WARNING博主在此之前, 基本只接触过C/C++语言, 对大多现代语言都没有了解, 所以在阅读过程中遇到相似的概念, 难免会与C/C++中的相似概念作类比, 见谅
此样式内容, 表示文档原文内容
异常
在编写软件系统时,检测和处理程序中的错误行为往往是十分困难的,为了保证系统的正确性和健壮性,很多软件系统中都包含大量的代码用于错误检测和错误处理
异常, 是一类特殊的可以被程序员捕获并处理的错误,是程序执行时出现的一系列不正常行为的统称,例如,索引越界、除零错误、计算溢出、非法输入等
异常不属于程序的正常功能,一旦发生异常,要求程序必须立即处理,即 将程序的控制权从正常功能的执行处转移至处理异常的部分
仓颉编程语言提供异常处理机制, 用于处理程序运行时 可能出现的各种异常情况
异常处理主要涉及:
try表达式(try expression),包括普通try表达式和try-with-resources表达式
throw表达式(throw expression)由关键字throw以及尾随的表达式组成尾随表达式的类型必须继承于
Exception或Error类下面将分别介绍
try表达式和throw表达式
经典的try和throw异常处理机制, C++中当然也存在这样的机制
但是细节还未了解, 只知道异常是用来做什么的
Try表达式
根据是否涉及资源的自动管理,将
try表达式分为两类: 不涉及资源自动管理的普通try表达式,以及会进行资源自动管理的try-with-resources表达式
try表达式的语法定义为:tryExpression: 'try' block 'finally' block| 'try' block ('catch' '(' catchPattern ')' block)+ ('finally' block)?| 'try' '(' ResourceSpecifications ')' block ('catch' '(' catchPattern ')' block)* ('finally' block)?;catchPattern: wildcardPattern| exceptionTypePattern;exceptionTypePattern: ('_' | identifier) ':' type ('|' type)*;ResourceSpecifications: ResourceSpecification (',' ResourceSpecification)*;ResourceSpecification: identifier (':' classType)? '=' expression;接下来分别对普通
try表达式和try-with-resources表达式进行介绍
仓颉因为涉及资源自动管理, 所以将try分为了两种, 而C++几乎所有非栈资源均为手动管理, 所以没有区分
普通Try表达式
普通
try表达式(本小节提到的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表达式中可以包含一个finally块,也可以不包含finally块(但此时必须至少包含一个catch块)
熟悉的try-catch, 这是C++中拥有的, 仓颉还有一个finally
catch和finally可以共存, 且catch和finally至少有一个
仓颉中的catch是按照模式匹配去捕获异常的
如果存在finally, 则 此块是必执行的 无论try是否抛出异常
其中
catchPattern有两种模式:
通配符模式(“
_”): 可以捕获同级try块内抛出的任意类型的异常,等价于类型模式中的e: Exception,即捕获Exception及其子类所定义的异常示例:
// 使用通配符模式 捕获异常let arrayTest: Array<Int64> = Array<Int64>([0, 1, 2])try {let lastElement = arrayTest[3]} catch (_) {print("catch an exception!")}类型模式: 可以捕获指定类型(或其子类型)的异常,语法上主要有两种格式:
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() {try {throw ArithmeticException()} catch (e: Exception) { // 捕获print("Exception and its subtypes can be caught here")}}// 第二种情况// 用户定义的异常open class Father <: Exception {var father: Int64 = 0func whatFather() {print("I am Father")}}class ChildOne <: Father {var childOne: Int64 = 1func whatChildOne() {print("I am ChildOne")}func whatChild() {print("I am method in ChildOne")}}class ChildTwo <: Father {var childTwo: Int64 = 2func whatChildTwo() {print("I am ChildTwo")}func whatChild() {print("I am method in ChildTwo")}}// main 函数main() {var a = 1func throwE() {if (a == 1) {ChildOne()} else {ChildTwo()}}try {throwE()} catch (e: ChildOne | ChildTwo) {e.whatFather() // ok: e 是一个 Father 对象//e.whatChildOne() // error: e 是一个 Father 对象//e.whatChild() // error: e 是一个 Father 对象print(e.father) // ok: e 是一个 Father 对象//print(e.childOne) // error: e 是一个 Father 对象//print(e.childOTwo) // error: e 是一个 Father 对象}return 0}使用
finally块的例子如下:// 使用异常类型模式 捕获try {throw IndexOutOfBoundsException()} catch (e: ArithmeticException | IndexOutOfBoundsException) {print("exception info: " + e.toString())} catch (e: Exception) {print("neither ArithmeticException nor IndexOutOfBoundsException, exception info: " + e.toString())} finally {print("the finally block is executed")}
异常捕获, catch使用模式匹配, 总是匹配目标异常类及其子类
但catch只会执行一个, finally如果存在总会被执行
一个catch可以匹配多个不同的异常类, 每个异常类使用|分隔, 但此时 如果捕获到目标异常, 此异常将会被转换为 所有异常类的最小公共父类 然后再绑定到标识符
使用时, 也只能访问最小公共父类的成员
猜测, 仓颉中所有异常类 均需要有一个祖先类: Exception
Try表达式的类型
类似于
if表达式:
如果
try表达式的值 没有被读取或者返回,那么整个try表达式的类型为Unit,try块和catch块不要求存在公共父类型;否则,按如下规则检查;在上下文没有明确的类型要求时,要求
try块和所有catch块(如果存在)拥有最小公共父类型整个
try表达式的类型就是该最小公共父类型;在上下文有明确的类型要求时,
try块和任一catch块(如果存在)的类型都必须是上下文所要求的类型的子类型,但此时不要求它们拥有最小公共父类型需要注意的是,虽然
finally块会在try块和catch之后执行,但是它不会对整个try表达式的类型产生影响,并且finally块的类型始终是Unit(即使finally块内表达式的类型不是Unit)
首先, finally表达式的类型恒为Unit, 且不会对整个try表达式的类型有任何影响
try表达式的值 如果没有被读取或返回, 就是Unit类型
否则, 需要根据上下文进行推断, 如果上下文中指定了类型, 那么try和catch的类型必须是指定类型的子类型, 如果没有指定 那么try和catch必须存在最小公共父类型
和if表达式的类型和值是类似的
Try表达式的求值顺序
关于
try {e1} catch (catchPattern) {e2} finally {e3}表达式执行规则的额外规定:
若进入
finally块前执行到return e表达式,则会将e求值至v,然后立刻执行finally block;若进入finally块前执行到break或continue表达式,则会立刻执行finally block
若
finally块中无return表达式,则处理完finally块后会将缓存的结果v返回(或抛出异常)也就是说,即使
finally块中有对上述e中引用变量的赋值,也不会影响已经求值的结果v,举例如下:func f(): Int64 {var x = 1;try {return x + x; // 返回 2} catch (e: Exception) { // 捕获print("Exception and its subtypes can be caught here")} finally {x = 2;} // 返回值会是 2 而不是 4}若在
finally块中执行到return e2或throw e2,则会对e2求值至结果v2,并立即返回或抛出v2;若在finally块中执行到break或continue,则会终止finally的执行并立即跳出循环举例如下:
func f(): Int64 {var x = 1;try {return x + x; // 返回 2} catch (e: Exception) { // 捕获print("Exception and its subtypes can be caught here")} finally {x = 2;return x + x; // 返回 4} // 返回值会是 4 而不是 2}总之,
finally块一定会被执行如果
finally块中有任何控制转移表达式,都会覆盖 进入finally之前的控制转移表达式
Try表达式中throw的处理更为复杂,具体请参考下一小节
仓颉中, try表达式的块内可以执行控制转移表达式: return break continue
在try表达式的块内, 如果执行了return, try表达式执行完之后 会直接返回整个函数
但返回值是多少, 则 根据规则计算
-
如果在进入
finally之前执行了return、break或continue则, 会直接进入
finally, 去执行finally, 且会隐含的记录return的表达式值 作为返回值 -
如果进入
finally之后, 又执行了return、break或continue则 新的
return值会覆盖已经记录的值 作为返回值, 或 结束循环 或 继续下一次循环如果没有执行
return, 那么返回值就 依旧是之前记录的值
Try表达式处理异常的逻辑
关于
try {e1} catch (catchPattern) {e2} finally {e3}表达式执行时抛出异常的规定:
若执行
e1的过程中没有抛出异常(这种情况下不会执行e2):
当执行
e3的过程中亦无异常抛出,那么整个try表达式不会抛出异常;当执行
e3的过程中抛出异常E3,那么整个try表达式抛出异常E3;若执行
e1的过程中抛出异常E1,且执行e3的过程中抛出异常E3,那么整个try表达式抛出异常E3(无论E1是否被catch块所捕获);若执行
e1的过程中抛出异常E1,且执行e3的过程中无异常抛出,那么:
当
E1可以被catch捕获且执行e2的过程中无异常抛出时,整个try表达式无异常抛出;当
E1可以被catch捕获且执行e2的过程中抛出异常E2时,整个try表达式抛出异常E2;当
E1未能被catch捕获时,整个try表达式抛出异常E1
如果finally抛出异常E3, 那么整个try表达式抛出异常E3
如果finally没有抛异常, 那么还要分情况:
-
如果
try抛出异常E1, 且没有catch捕获, 那么try表达式抛出异常E1 -
如果
try抛出异常E1, 被catch捕获了, 且执行时 异常被处理了 没有再抛出异常, 那么try表达式不抛异常 -
如果
try抛出异常E1, 被catch捕获了, 但执行时 又抛出了异常E2, 那么try表达式抛出异常E2
总之, 要看执行时 try中的异常是否被处理, 以及 执行到的最后抛出异常的语句 来结合判断try表达式的异常情况
Try-With-Resources表达式
Try-with-resources表达式主要是为了自动释放非内存资源不同于普通
try表达式,try-with-resources表达式中的catch块和finally块均是可选的,并且try关键字其后的块之间可以**插入一个或者多个ResourceSpecification**用来申请一系列的资源(ResourceSpecification并不会影响整个try表达式的类型)这里所讲的资源对应到语言层面即指 对象,因此
ResourceSpecification其实就是实例化一系列的对象(多个实例化之间使用,分隔)
有些迷糊了, 什么是ResourceSpecification, 暂时没有具体的涉及到 仓颉中的相关内容
但, 非内存资源猜测可能是: 文件描述符? 网络套接字? 数据库句柄? 之类的
不过 finally和catch都可以不存在
使用
try-with-resources表达式的例子如下所示:class MyResource <: Resource {var flag = falsepublic func isClosed() { flag }public func close() { flag = true }public func hasNextLine() { false }public func readLine() { "line" }public func writeLine(_: String) {}}main() {try (input = MyResource(),output = MyResource()) {while (input.hasNextLine()) {let lineString = input.readLine()output.writeLine(lineString)}} catch (e: Exception) {print("Exception happened when executing the try-with-resources expression")} finally {print("end of the try-with-resources expression")}}在
try-with-resources表达式中的ResourceSpecification的类型必须实现Resource接口:interface Resource {func isClosed(): Boolfunc close(): Unit}
看起来, 应该是在try中创建资源, 然后出try块之后, 会自动尝试调用close()接口自动释放资源?
主要的应该依靠 实现Resource接口
Try-with-resources表达式会首先(依声明顺序)执行实例化对应的一系列资源申请(上例中,先实例化input对象,再实例化output对象),在此过程中若某个资源申请失败(例如,output实例化失败),则在它之前申请成功的资源(如input对象)会被全部释放(释放过程中若抛出异常,会被忽略),并抛出申请此资源(output对象)失败的异常如果所有资源均申请成功,则继续执行
try之后紧跟的块在执行块的过程中,无论是否发生异常,之后均会按照资源申请时的逆序依次自动释放资源(上例中,先释放
output对象,再释放input对象)在释放资源的过程中,若某个资源在被释放时发生异常,并不会影响其它资源的继续释放,并且整个
try表达式抛出的异常遵循如下原则:
如果
try之后紧跟的块中有异常抛出,则 释放资源的过程中抛出的异常 会被忽略;如果
try之后紧跟的块中没有抛出异常,释放资源的过程中抛出的首个异常将会被抛出(后续释放资源过程中抛出的异常均会被忽略)
资源会根据声明顺序进行申请, 如果存在一个资源申请失败, 则会抛出申请失败的异常 并 释放已经成功申请的资源(如果释放时抛异常, 此异常会被忽略)
资源释放时抛异常, 并不影响其他资源的释放
Try-with-resources 中, 如果try块抛异常, 则 实际会抛出第一个被抛出的异常, 其他异常会被忽略(实际上 其他异常可能只有释放资源时抛出的异常)
但我有一点不明白, 为什么在执行块的过程中, 申请的资源就会被逆序自动释放? 而不是块执行完毕?
怎么想都应该是 块里执行完毕之后在释放比较合理
需要说明的是,
try-with-resources表达式中一般没有必要再包含catch块和finally块,也不建议用户再手动释放资源因为
try块执行的过程中 无论是否发生异常,所有申请的资源都会被自动释放,并且执行过程中产生的异常均会被向外抛出但是,如果需要显式地捕获
try块 或 资源申请和释放过程中可能抛出的异常 并处理,仍可在try-with-resources表达式中包含catch块和finally块:try (input = MyResource(),output = MyResource()) {while (input.hasNextLine()) {let lineString = input.readLine()output.writeLine(lineString)}} catch (e: Exception) {print("Exception happened when executing the try-with-resources expression")} finally {print("end of the try-with-resources expression")}事实上,上述
try-with-resources表达式等价于下述普通try表达式:try {var freshExc = None<Exception> // 一个可以存储任何异常的新变量let input = MyResource()try {var freshExc = None<Exception>let output = MyResource()try {while (input.hasNextLine()) {let lineString = input.readLine()output.writeLine(lineString)}} catch (e: Exception) {freshExc = e} finally {try {if (!output.isClosed()) {output.close()}} catch (e: Exception) {match (freshExc) {case Some(v) => throw v // 用户代码中引发的异常将被抛出case None => throw e}}match (freshExc) {case Some(v) => throw vcase None => ()}}} catch (e: Exception) {freshExc = e} finally {try {if (!input.isClosed()) {input.close()}} catch (e: Exception) {match (freshExc) {case Some(v) => throw vcase None => throw e}}match (freshExc) {case Some(v) => throw vcase None => ()}}} catch (e: Exception) {print("Exception happened when executing the try-with-resources expression")} finally {print("end of the try-with-resources expression")}可以看到,
try块(即用户代码)中 若抛出的异常会被记录在freshExc变量中,并最终被层层向外抛出,其优先级高于 释放资源的过程中可能出现的异常
try-with-resources表达式的类型是Unit
这应该是个典型代码了, 可以认真分析一下
try { var freshExc = None<Exception> // 一个可以存储任何异常的新变量 let input = MyResource() try { var freshExc = None<Exception> let output = MyResource() try { // 用户代码 while (input.hasNextLine()) { let lineString = input.readLine() output.writeLine(lineString) } } catch (e: Exception) { // 尝试捕获 用户代码异常 freshExc = e // 捕获到了, 记录 用户代码异常 } finally { try { if (!output.isClosed()) { // 尝试释放资源 output.close() } } catch (e: Exception) { // 尝试捕获 释放资源异常 match (freshExc) { // 如果捕获到了, 就模式匹配 用户代码异常 case Some(v) => throw v // 匹配到 用户代码异常存在, 则 抛出用户代码异常 case None => throw e // 匹配到 None, 即 用户代码异常不存在, 则 抛出资源释放异常 } } match (freshExc) { // 如果没有捕获到资源释放异常, 模式匹配 用户代码异常 case Some(v) => throw v // 匹配到 用户代码异常存在, 则 抛出用户代码异常 case None => () // 匹配到 None, 即 用户代码异常不存在, 模式匹配值为() } } } catch (e: Exception) { // 尝试捕获内层代码中的异常 freshExc = e // 捕获到 则记录 内层异常 } finally { try { if (!input.isClosed()) { // 尝试释放资源 input.close() } } catch (e: Exception) { // 尝试捕获 释放资源异常 match (freshExc) { // 如果捕获到了释放资源异常, 模式匹配 内层异常 case Some(v) => throw v // 模式匹配 内层异常存在, 则抛出内层异常 case None => throw e // 模式匹配 None, 即内层异常不存在, 则抛出资源释放异常 } } match (freshExc) { // 如果资源释放没有异常, 则模式匹配 内层异常 case Some(v) => throw v // 模式匹配 内层确实抛出异常, 则 抛出内层异常 case None => () // 模式匹配 None, 则 内层异常不存在, 整个`try`表达式无异常 } }} catch (e: Exception) { print("Exception happened when executing the try-with-resources expression")} finally { print("end of the try-with-resources expression")}其实也不是很复杂, 严格匹配异常并处理
Throw表达式
throw表达式的语法定义为:throwExpression: 'throw' expression;
Throw表达式由关键字throw和一条表达式组成,用于抛出异常
Throw表达式的类型是Nothing需要注意的是,关键字
throw之后的表达式只能是一个继承于Exception或Error的类型的对象
Throw表达式会改变程序的执行逻辑:throw表达式在执行时会抛出一个异常,捕获此异常的代码块将被执行,而非throw后剩余的表达式使用
throw表达式的例子如下:// 使用 异常类型模 捕获异常let listTest = [0, 1, 2]try {throw ArithmeticException()let temp = listTest[0] + 1 // 永远不会被执行} catch (e: ArithmeticException) {print("an arithmeticException happened: " + e.toString())} finally {print("the finally block is executed")}
Throw表达式, 使用throw关键字 抛出异常或Error
异常是继承于Exception的类对象, Error可能是另外一种相同的作用的类
执行过throw之后, 块中之后的代码就不会再执行了, 直到异常被捕获或finally
当
throw表达式抛出一个异常后,必须要能够将其捕获并处理搜寻异常捕获代码的顺序是函数调用链的逆序: 当一个异常被抛出时,首先在抛出异常的函数内搜索匹配的
catch块,如果未找到,则终止此函数的执行,并在调用这个函数的函数内继续寻找相匹配的catch块,若仍未找到,该函数同样需要终止,并继续搜索调用它的函数,以此类推,直到找到相匹配的catch块但是,如果在调用链内的所有函数中均未找到合适的
catch块,则程序跳转到Exception中terminate函数内执行,使得程序非正常退出下面的例子展示了异常在不同位置被捕获的场景:
// 被 catchE() 捕获func catchE() {let listTest = [0, 1, 2]try {throwE() // 被 catchE() 捕获} catch (e: IndexOutOfBoundsException) {print("an IndexOutOfBoundsException happened: " + e.toString())}}// Terminate 函数被执行func notCatchE() {let listTest = [0, 1, 2]throwE()}// 函数 throwE()func throwE() {throw IndexOutOfBoundsException()}
当程序抛出异常之后, 会在函数内寻找匹配的catch, 如果找不到, 函数终止, 然后逐层出栈 直到找到匹配的catch, 或 直到始终找不到 执行Exception.terminate() 非正常退出程序