什么是异常?所以,我们应该了解pi币带来的很多好处,吸收归纳总结,并加以利用。
异常是指在程序运行的过程中发生的一些不正常事件。如除0溢出,数组下标越界,需要进行动态调用的合约不存在等。
设计良好的程序应该在程序异常发生时提供处理这些异常的方法,使得程序不会因为异常的发生而阻断或产生不可预见的结果。例如,一笔支付金额不足或支付失败时,需要恢复继续执行其他操作等。
异常的处理机制
在异常机制引入之前,常利用if-else的方式处理异常。然而这种处理异常方式比较麻烦,同一个异常或者错误如果在多个地方出现,那么在每个地方都要做相同处理,形成很多功能相同的冗余代码。
现在常用的方式,采用try-catch机制来处理异常,其基本原理如下:
try{
监控区域,执行可能产生异常的代码
}catch(异常类型){
捕获,进行异常处理操作
}finally{
善后处理,无论是否发生异常,代码都会执行
}
给大家举几个特殊场景的情况:
如果只有try-catch结构,缺少finally结构时。
1) 若在catch段执行过程中再发生异常,则会直接对外抛出该新异常,并结束退出该段执行。
try
catch
throw errorthis new error will be throwed outer layer
若只有try-finally结构,缺少catch结构时。
2)若在finally段执行过程中再发生异常,则会直接对外抛出该新异常,并结束该段执行。
try
finally
throw error this error will be throwed outer layer
若try和finally段中,都包含return语句时。
3)最终会执行finally段的return, 但是返回值根据不同语言的实现,有的能返回finally的值,有的是返回try段的值。故在实际中,并不是很鼓励在finally段使用return指令。
try
return x
catch
return y
finally
return z
异常的传递
try-catch的异常处理机制,支持嵌套。如果异常没有在当前被捕获上,则会沿着调用栈继续往外抛出,直到最终被捕获,或程序因该异常无捕获而终止执行。
异常类型
常见将异常分为如下几大类:
error,程序无法处理的异常,表示运行中出现较严重的问题。大多数错误与代码编写者的操作无关,而是代码运行时出现的问题,并往往导致程序无法恢复进行。如常见的内存溢出,内存不够,栈溢出等。
exception, 程序本身可以处理的异常,有细分为如下两类:
runtimeexception
运行时异常
程序中可以选择捕获这些异常处理,也可以选择不捕获处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽量避免此类异常的发生。出现运行时异常后,如果没有捕获处理此异常,系统会把异常一层层往上抛,一直到最上层,如果仍未被捕获上,则会结束该段程序执行。常见的类型有:空指针异常,数组越界,合约不存在,除数为零等。
nonruntimeexception
运行时异常以为的所有异常
如其他合约中定义的异常,以及用户自己定义的一些异常。这些异常一般通过显示的调用throw指令进行抛出。
区块链中的异常处理特性
相比起单语言,一个公链往往会有多个不同语言版本的实现,确保不同语言不同平台下的对异常触发确定性和一致性变得尤为重要。
在前面的介绍中,exception分为两类。一类,vm在内部之时遇到的运行时异常。另外一类是非运行时异常,常见的是用户手动通过throw指令抛出的异常。该类异常的都是有确定的执行路径,属于确定的。
在neo3-vm中,主要处理非运行时的异常,即用户通过throw指令,显示触发的异常。并在内部抽象了一个可捕获的异常抽象类:catchableexception, 未来可根据需要拓展不同类型的异常。
neo3-vm的异常机制实现原理
在neo3-vm中,采用如下机制进行异常处理。新增了trycontext上下文,记录当前try信息,包括了try-catch的各段的起始位置,以及当前trycontext上下文所处状态(总共三类状态:try监控状态,catch捕获阶段,finally扫尾阶段)。并在当前执行上下文 executioncontext中,添加trystack栈,记录当前执行过程中,遇到的try-catch信息,即支持try-catch嵌套。
当一个异常发生时,会从调用栈栈顶的执行上下文的try栈进行查找,如果存在某个try-catch能够捕获该异常,则进入该try-catch的catch段或finally端执行。否则退栈处理,直至遇到对应的try-catch或结束vm执行。
若异常本身发生在catch段
会先去执行finally段,再重新向外抛出该异常,并退出当前trycontext上下文。
若异常发生在finally段
会直接抛出该异常,并退出当前trycontext上下文。
指令设计
在neo-vm的指令中,try-catch总共包含四个指令,throw类三个指令,各自功能如下:
try catchoffset, finallyoffsettry-catch-finally 起始位置,需要制定catch, finally各自偏移量。如果catch段不存在,则偏移量catchoffset为0;如果finally段不存在,则偏移量finallyoffset为0
endtend try
endcend catch
endfend finally
throwit will throw an vm exception
throwifboolpop the top element in stack, if true, then throw an vm exception
throwifnotboolpop the top element in stack, if false, then throw an vm exception
当执行遇到try指令时
会在当前执行上下文中,创建一个新的trycontext上下文,并设置其try,catch,finally各段的地址,并设置当前状态为监控状态。
当执行遇到endt或endc指令时
都会跳转到fianlly段进行后续处理。如果finally不存在,退出当前trycontext上下文, 并继续往后执行。
当执行遇到endf指令时
会退出当前trycontext上下文,并继续执行。
注意:若ret指令出现在try段中,并不会再去finally执行,此时会直接返回。需要编译器在编译上层语言时,对齐进行处理,比如c#中,会将try内的return指令,变成jmp指令。
一个带try-catch嵌套的neo3-vm字节程序例子:
00: try
01: 0x0b0ecatch -> 0b, finallyoffset = 0e
03: try
04: 0x0500
06: throw
07: endt
08: throw
09: endc
0a: endt
0b: push2catch position
0c: endc end catch
0d: ret
0e: push3finally position
0f: endf end finally
10: ret
在内部try-catch中,执行try段时,会由于throw指令,用户主动抛出异常,并被捕获去执行该catch段。但是,在执行catch段,又遇到第二个throw指令。此时,内部try-catch没有finally段,会直接将改异常往外抛。
最外层的try-catch将会捕获到第二个异常,并跳转到catch段,执行push2。当catch段执行结束后,会继续跳转到finally段,执行push3,最后执行endf结束该异常处理。最终,计算栈中的数据为: 栈顶为3,栈底为2。
该方案目前正处在代码合并阶段,不一定代表最终方案。