智能合约语言 Solidity 教程 - 错误处理
什么是错误处理
错误处理涉及到程序在出现错误时的应对方式,Solidity在这方面与常见编程语言有所不同。Solidity采用回退状态的方式来处理异常情况。如果发生异常,将会撤销当前调用以及所有子调用所做的状态更改,并向调用者返回一个错误标识。值得注意的是,Solidity不支持像其他编程语言中的"try...catch..."这种异常捕捉机制。因此,捕捉异常是不可能的,也就意味着没有类似try...catch...的语法结构可供使用。
区块链可以被视为一个全球共享的分布式事务性数据库。"全球共享" 意味着每个网络参与者都有权读取和写入数据库的记录。但如果要对数据库中的内容进行修改,就必须进行一种特殊的操作,即事务。事务确保对数据库的修改(例如同时修改两个值)要么完全应用,要么一点都不应用。Solidity的错误处理机制旨在确保每个函数调用都是事务性的,这有助于维护数据的一致性和可靠性。
如何处理
Solidity为条件检查提供了两个函数,即assert和require。当条件不满足时,这两个函数均可用于抛出异常。通常,assert用于测试内部错误,而require用于验证输入变量或合同状态变量是否满足条件,以及验证调用外部合约返回值。
正确使用assert有助于利用Solidity分析工具,该工具能够检测智能合约中的错误并帮助发现逻辑错误的bug。这进一步提高了智能合约的安全性和可靠性。
除了使用assert和require这两个函数来进行条件检查,还有两种方式可以引发异常:
- 使用revert函数:revert函数可以被用来标记错误并回滚当前调用的状态。
- 使用throw关键字:在Solidity 0.4.13版本之后,throw关键字已被弃用,并将来会被移除。
需要注意的是,当在子调用中发生异常时,异常会自动向上传播,影响父级调用。然而,也存在一些特例,如send以及底层的函数调用如call、delegatecall和callcode,当它们引发异常时,将返回false,而不会像其他调用一样向上传播异常。
注意:在一个不存在的地址上调用底层的函数call,delegatecall,callcode 也会返回成功,所以我们在进行调用时,应该总是优先进行函数存在性检查。
在下面通过一个示例来说明如何使用require来检查输入条件,以及assert用于内部错误检查:
pragma solidity ^0.4.0;
contract Sharer {
function sendHalf(address addr) public payable returns (uint balance) {
require(msg.value % 2 == 0); // 仅允许偶数
uint balanceBeforeTransfer = this.balance;
addr.transfer(msg.value / 2); // 如果失败,会抛出异常,下面的代码就不是执行
assert(this.balance == balanceBeforeTransfer - msg.value / 2);
return this.balance;
}
}
一、首先打开Remix,贴入代码,点击创建合约。如下图:

二、运行测试1:附加1wei (奇数)去调用sendHalf,这时会发生异常,如下图:
三、运行测试2:附加2wei 去调用sendHalf,运行正常。
四、运行测试3:附加2wei以及sendHalf参数为当前合约本身,在转账是发生异常,因为合约无法接收转账,错误提示上图类似。
assert类型异常
assert类型的异常在以下情况下会自动触发:
- 数组访问越界或使用负的索引值,例如,当尝试访问
x[i]
时,如果i >= x.length
或i < 0
。 - 对定长的bytesN进行访问越界,或者使用负的索引值。
- 当被除数为0时,如
5/0
或23 % 0
。 - 当对一个二进制数进行左移或右移,并使用负的移动值,例如
5 << i
,其中i
为负数。 - 当尝试将整数显式转换为枚举类型,并转换的值过大或为负数时,将抛出异常。
- 如果尝试调用未初始化的内部函数类型的变量。
- 当assert语句的参数为false时,会触发assert异常。
require类型异常
- 使用
throw
关键字明确抛出异常。 - 在调用
require
时,如果参数表达式为false
,将引发异常。 - 当通过消息调用函数,但在调用的过程中未能正确完成,例如,gas不足,没有匹配到对应的函数,或被调用的函数出现异常。值得注意的是,底层操作如call、send、delegatecall或callcode不会抛出异常,而是通过返回
false
来表示失败。 - 在使用
new
创建一个新合约时,如果由于第三点所述的原因未能正常完成,将抛出异常。 - 当调用外部函数时,被调用的对象不包含合约代码时,会触发异常。
- 如果合约没有
payable
修饰符的public
函数用于接收以太币,包括构造函数和回退函数,当尝试向这些函数发送以太币时,异常将发生。 - 如果尝试通过一个
public
的获取函数(getter function)来接收以太币,异常将被触发。 - 当使用
transfer()
执行失败时,将引发异常。
当发生require类型的异常时,Solidity将执行一个回退操作(指令0xfd)来撤销所有状态更改。类似地,当发生assert类型的异常时,Solidity将执行一个无效操作(指令0xfe)。这是因为在这两种情况下,期望的结果没有发生,因此不能继续执行交易以确保安全性。在区块链中,交易必须具备原子性,即要么所有操作都被执行,要么一点改变都不会发生。因此,任何异常情况都需要回滚到初始状态,确保整个交易没有任何影响。这有助于维护数据的一致性和可靠性。