Assert, Require, Revert 和 Exceptions
Solidity使用state-reverting异常来处理错误。 这种异常将回滚当前调用(及其所有子调用)状态的所有变化,并将错误标志给调用者。
函数assert
和require
可以用于检查条件,如果条件不满足则抛出异常。 assert
函数只能用于测试内部错误,并检查不变量。
应该使用require
函数来确认input或合约状态变量满足条件,或者验证调用外部合约的返回值。
如果正确使用,分析工具可以评估合约和函数调用是否assert
失败。 正常运行的代码不应该assert
失败;如果发生这种情况,则需要修复合约中的bug。
还有另外两种方法可以触发异常:
revert
函数可用于标记错误并回滚当前的调用。throw
关键字也可以用作revert()
的替代方法。
注解 |
---|
从0.4.13版本,throw 关键字已被弃用,将来会被淘汰。 |
当异常发生在子调用中时,它们会自动“冒泡”(即异常被重新抛出)。
但也有例外:send
和底层函数call
, delegatecall
和 callcode
——在异常情况下返回false
,而不是“冒泡”。
警告 |
---|
如果调用的帐户不存在,call , delegatecall 和 callcode 的返回值会是true ,EVM就是这么设计的。如果需要,必须在调用之前检查账户是否存在。 |
捕捉异常目前还做不到。
在下面的示例中,您可以看到如何使用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;
}
}
在以下情况下会生成assert
风格异常:
- 以过大或负数索引访问数组(比如
x[i]
,i >= x.length
或i < 0
) - 以过大或负数索引访问固定长度的
bytesN
。 - 除数或模数为零(例如
5/0
或23%0
)。 - 对负数进行位移
- 将过大或负数转化为枚举类型。
- 调用内部函数类型的0初始化(zero-initialized)变量。
assert
的条件为false
在以下情况下会生成require
风格异常:
- 调用
throw
- 调用
require
并且条件为false
- 通过消息调用来调用函数,但是它没有正确完成(即,用尽了gas,没有匹配的函数,或者抛出了异常),除非使用底层别的操作
call
,send
,delegatecall
或callcode
。 底层别的操作不会抛出异常,而是通过返回false
来表示失败。 - 使用
new
关键字创建合约但是失败。 - 执行一个外部函数调用,其指向不包含代码的合约。
- 合约通过
public
函数接收Ether,但没有payable
修饰符。包括构造函数和回退函数。 - 合约通过一个
public
的getter
函数接收Ether。 - 如果
.transfer()
失败。
在内部,Solidity对require
-style异常执行一个revert操作(0xfd
指令),并执行一个无效操作(指令0xfe
)来抛出一个assert
-style异常。 在这两种情况下,这将导致EVM revert对状态所做的所有更改。 revert的原因是没有安全的方式来继续执行,因为没有发生预期的效果。 因为我们要保留交易的原子性,所以最安全的做法是恢复所有的变化,并使整个事务(或至少调用)无效。 请注意,assert
风格的异常消耗调用中可用的所有gas,而require
风格的异常将不会消耗从Metropolis版本开始的任何gas。