solidity一个错误将撤消在交易期间对状态所做的所有改变。有三种抛出异常的方法:error,require和assert。
Error是solidity 0.8版本新加的内容,方便且高效(省gas)的向用户解释操作失败的原因。人们可以在contract之外定义异常。
下面,我们定义一个TransferNotOwner异常,当用户不是代币owner的时候尝试转账,会抛出错误:
error TransferNotOwner(); // 自定义errorfunction
transferOwner1(uint256 tokenId, address newOwner) public {
if(_owners[tokenId] != msg.sender){
revert TransferNotOwner();
}
_owners[tokenId] = newOwner;
}
注意:在执行当中,error 必须搭配 revert 命令使用。
// custom error
error InsufficientBalance(uint balance, uint withdrawAmount);
function testCustomError(uint _withdrawAmount) public view {
uint bal = address(this).balance;
if (bal < _withdrawAmount) {
revert InsufficientBalance({balance: bal, withdrawAmount: _withdrawAmount});
}
}
我们也可以直接使用revert:
function testRevert(uint _i) public pure {
// Revert is useful when the condition to check is complex.
// This code does the exact same thing as the example above
if (_i <= 10) {
revert("Input must be greater than 10");
}
}
require命令是solidity 0.8 版本之前抛出异常的常用方法,目前很多主流合约仍然还在使用它。
他很好用,唯一的缺点就是gas随着描述异常的字符串长度增加,比error命令要高。使用方法:require(检查条件,”异常的描述”),当检查条件不成立的时候,就会抛出异常。
我们用require命令重写一下上面的transferOwner函数:
function transferOwner2(uint256 tokenId, address newOwner) public {
require(_owners[tokenId] == msg.sender, "Transfer Not Owner");
_owners[tokenId] = newOwner;
}
function testRequire(uint _i) public pure {
// Require should be used to validate conditions such as:
// - inputs
// - conditions before execution
// - return values from calls to other functions
require(_i > 10, "Input must be greater than 10");
}
assert应该只用于调试,因为他不能解释抛出异常的原因(比require少个字符串)。他的用法很简单,assert(检查条件),当检查条件不成立的时候,就会抛出异常。
我们用assert命令重写一下上面的transferOwner函数:
function transferOwner3(uint256 tokenId, address newOwner) public {
assert(_owners[tokenId] == msg.sender);
_owners[tokenId] = newOwner;
}
function testAssert() public view {
// Assert should only be used to test for internal errors,
// and to check invariants.
// Here we assert that num is always equal to 0
// since it is impossible to update the value of num
assert(num == 0);
}
我们比较一下三种抛出异常的gas消耗,方法很简单,部署合约,分别运行写的transferOwner函数的三个版本。
error方法gas消耗:24445require方法gas消耗:24743assert方法gas消耗:24446
我们可以看到,error方法gas cost最少,其次是assert,require方法消耗gas最多。因此,error是最佳的,既可以告知用户抛出异常的原因,又能省gas。
下列情况将会产生一个 assert 式异常:
如果你访问数组的索引太大或为负数(例如
x[i]其中i >= x.length或i < 0)。如果你访问固定长度
bytesN的索引太大或为负数。如果你用零当除数做除法或模运算(例如
5 / 0或23 % 0)。如果你移位负数位。
如果你将一个太大或负数值转换为一个枚举类型。
如果你调用内部函数类型的零初始化变量。
如果你调用
assert的参数(表达式)最终结算为 false。
下列情况将会产生一个 require 式异常:
调用
throw。如果你调用
require的参数(表达式)最终结算为false。如果你通过消息调用调用某个函数,但该函数没有正确结束(它耗尽了 gas,没有匹配函数,或者本身抛出一个异常),上述函数不包括低级别的操作
call,send,delegatecall或者callcode。低级操作不会抛出异常,而通过返回false来指示失败。如果你使用
new关键字创建合约,但合约没有正确创建(请参阅上条有关”未正确完成“的定义)。如果你对不包含代码的合约执行外部函数调用。
如果你的合约通过一个没有
payable修饰符的公有函数(包括构造函数和 fallback 函数)接收 Ether。如果你的合约通过公有 getter 函数接收 Ether 。
如果
.transfer()失败。
这节学习了处理异常的3种方法,revert(error)、require 和 assert,其中 error 最合适,既能省gas,又可以知道抛出的异常。
Code:
https://github.com/Luca-Hsu/SuperSolidity
Reference:
https://mirror.xyz/ninjak.eth/XhhLu7PV1cAhOp9_m-dk9OoTj7offC7DkYYgsV3e31I

