表达式和控制结构
控制结构
Solidity 中提供了大多数花括号语言中的控制结构。
包括:if
、else
、while
、do
、for
、break
、continue
、return
,它们具有与 C 或 JavaScript 中相同的语义。
Solidity 还支持使用 try
/catch
语句的异常处理,但仅限于 外部函数调用 和合约创建调用。可以使用 revert 语句 创建错误。
条件语句中 *不能* 省略括号,但单语句体周围可以省略花括号。
请注意,与 C 和 JavaScript 不同,Solidity 中没有从非布尔类型到布尔类型的类型转换,因此 if (1) { ... }
*不是* 有效的 Solidity 代码。
函数调用
内部函数调用
当前合约的函数可以 *直接*(“内部”)调用,也可以递归调用,如下面的无意义示例所示
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
// This will report a warning
contract C {
function g(uint a) public pure returns (uint ret) { return a + f(); }
function f() internal pure returns (uint ret) { return g(7) + f(); }
}
这些函数调用被转换为 EVM 中的简单跳转。这意味着当前的内存不会被清除,即向内部调用的函数传递内存引用非常高效。只有同一合约实例的函数可以被内部调用。
您仍然应该避免过度递归,因为每次内部函数调用至少会使用一个栈槽,而只有 1024 个栈槽可用。
外部函数调用
函数还可以使用 this.g(8);
和 c.g(2);
语法调用,其中 c
是一个合约实例,g
是属于 c
的一个函数。无论通过哪种方式调用函数 g
,都会导致它被 “外部” 调用,即使用消息调用而不是直接跳转。请注意,在构造函数中 *不能* 使用 this
上的函数调用,因为实际的合约尚未创建。
其他合约的函数必须被外部调用。对于外部调用,所有函数参数都必须被复制到内存中。
注意
从一个合约到另一个合约的函数调用不会创建自己的交易,它是整体交易的一部分的消息调用。
当调用其他合约的函数时,您可以使用特殊选项 {value: 10, gas: 10000}
指定要发送的 Wei 或 gas 数量。请注意,不鼓励明确指定 gas 值,因为操作码的 gas 成本将来可能会发生变化。您发送给合约的任何 Wei 都会被添加到该合约的总余额中。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;
contract InfoFeed {
function info() public payable returns (uint ret) { return 42; }
}
contract Consumer {
InfoFeed feed;
function setFeed(InfoFeed addr) public { feed = addr; }
function callFeed() public { feed.info{value: 10, gas: 800}(); }
}
您需要在 info
函数中使用修饰符 payable
,因为否则 value
选项将不可用。
警告
请注意,feed.info{value: 10, gas: 800}
仅在本地设置 value
和通过函数调用发送的 gas
数量,而末尾的括号执行实际的调用。因此,feed.info{value: 10, gas: 800}
不会调用函数,并且 value
和 gas
设置会丢失,只有 feed.info{value: 10, gas: 800}()
会执行函数调用。
由于 EVM 认为对不存在的合约的调用总是会成功,Solidity 使用 extcodesize
操作码来检查即将被调用的合约是否确实存在(它包含代码),如果不存在,则会引发异常。如果调用后的返回值将被解码,则会跳过此检查,因此 ABI 解码器会捕获不存在的合约的情况。
请注意,在 低级调用 中不会执行此检查,低级调用是对地址而不是合约实例进行操作。
注意
在使用高级调用到 预编译合约 时要小心,因为编译器根据上述逻辑认为它们不存在,即使它们执行代码并可以返回数据。
如果被调用的合约本身抛出异常或用完 gas,函数调用也会导致异常。
警告
与其他合约的任何交互都存在潜在的风险,尤其是在事先不知道合约的源代码的情况下。当前合约将控制权交给了被调用的合约,该合约可能会做任何事情。即使被调用的合约继承自已知的父合约,继承的合约也仅需具有正确的接口。但是,合约的实现可以完全任意,从而构成风险。此外,要做好准备,以防它在第一个调用返回之前调用系统中的其他合约,甚至反过来调用调用合约。这意味着被调用的合约可以通过其函数更改调用合约的状态变量。以一种方式编写您的函数,例如,在对外部函数的调用发生在您合约中状态变量的任何更改之后,这样您的合约就不会受到重入攻击。
注意
在 Solidity 0.6.2 之前,推荐的方式是使用 f.value(x).gas(g)()
来指定值和 gas。这在 Solidity 0.6.2 中已被弃用,并且自 Solidity 0.7.0 起不再可能。
使用命名参数的函数调用
如果函数调用参数包含在 { }
中,则可以按名称,以任何顺序给出参数,如下面的示例所示。参数列表必须在名称上与函数声明中的参数列表一致,但顺序可以任意。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract C {
mapping(uint => uint) data;
function f() public {
set({value: 2, key: 3});
}
function set(uint key, uint value) public {
data[key] = value;
}
}
函数定义中省略的名称
函数声明中的参数和返回值的名称可以省略。那些省略名称的项目仍然存在于栈中,但无法通过名称访问。省略返回值名称仍然可以通过使用 return
语句向调用者返回一个值。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
contract C {
// omitted name for parameter
function func(uint k, uint) public pure returns(uint) {
return k;
}
}
使用 new
创建合约
合约可以使用 new
关键字创建其他合约。在创建合约的合约被编译时,必须知道被创建合约的完整代码,因此递归创建依赖关系是不可能的。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract D {
uint public x;
constructor(uint a) payable {
x = a;
}
}
contract C {
D d = new D(4); // will be executed as part of C's constructor
function createD(uint arg) public {
D newD = new D(arg);
newD.x();
}
function createAndEndowD(uint arg, uint amount) public payable {
// Send ether along with the creation
D newD = new D{value: amount}(arg);
newD.x();
}
}
如示例所示,可以使用 value
选项在创建 D
的实例时发送 Ether,但无法限制 gas 数量。如果创建失败(由于栈溢出、余额不足或其他问题),则会抛出异常。
加盐合约创建 / create2
创建合约时,合约的地址是根据创建合约的地址和一个计数器计算出来的,该计数器在每次合约创建时都会增加。
如果您指定选项 salt
(一个 bytes32 值),则合约创建将使用不同的机制来生成新合约的地址。
它将根据创建合约的地址、给定的 salt 值、被创建合约的(创建)字节码和构造函数参数计算地址。
特别是,计数器(“nonce”)未使用。这允许在创建合约方面更加灵活:您可以在创建合约之前推导出新合约的地址。此外,即使创建合约在中途创建了其他合约,您也可以依赖此地址。
这里的主要用例是充当链下交互法官的合约,这些合约只有在发生争议时才需要创建。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract D {
uint public x;
constructor(uint a) {
x = a;
}
}
contract C {
function createDSalted(bytes32 salt, uint arg) public {
// This complicated expression just tells you how the address
// can be pre-computed. It is just there for illustration.
// You actually only need ``new D{salt: salt}(arg)``.
address predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
bytes1(0xff),
address(this),
salt,
keccak256(abi.encodePacked(
type(D).creationCode,
abi.encode(arg)
))
)))));
D d = new D{salt: salt}(arg);
require(address(d) == predictedAddress);
}
}
警告
在与加盐创建相关的方面有一些特殊之处。合约在被销毁后可以在同一个地址重新创建。但是,即使创建字节码相同(这是必需的,否则地址将发生变化),新创建的合约也可能具有不同的部署字节码。这是因为构造函数可以查询外部状态,该状态可能在两次创建之间发生了变化,并将该状态合并到部署字节码中,然后再将其存储。
表达式的求值顺序
表达式求值顺序未指定(更正式地说,表达式树中一个节点的子节点求值顺序未指定,但它们当然是在节点本身之前求值的)。只保证语句按顺序执行,并且对布尔表达式的短路求值完成。
赋值
解构赋值和返回多个值
Solidity 在内部允许元组类型,即可能具有不同类型的对象的列表,其数量在编译时是常量。这些元组可用于同时返回多个值。然后,这些值可以分配给新声明的变量,也可以分配给预先存在的变量(或一般而言的左值)。
元组在 Solidity 中不是正确的类型,它们只能用于形成表达式的语法分组。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
contract C {
uint index;
function f() public pure returns (uint, bool, uint) {
return (7, true, 2);
}
function g() public {
// Variables declared with type and assigned from the returned tuple,
// not all elements have to be specified (but the number must match).
(uint x, , uint y) = f();
// Common trick to swap values -- does not work for non-value storage types.
(x, y) = (y, x);
// Components can be left out (also for variable declarations).
(index, , ) = f(); // Sets the index to 7
}
}
无法混合变量声明和非声明赋值,即以下无效: (x, uint y) = (1, 2);
注意
在 0.5.0 版本之前,可以将赋值给更小尺寸的元组,要么从左侧填充,要么从右侧填充(无论哪个为空)。现在不允许这样做,因此两侧必须具有相同数量的组件。
警告
当同时对多个变量进行赋值时,如果涉及引用类型,请小心,因为它会导致意外的复制行为。
数组和结构体的复杂性
对于非值类型(如数组和结构体,包括 bytes
和 string
),赋值的语义更加复杂,有关详细信息,请参阅 数据位置和赋值行为。
在下面的示例中,对 g(x)
的调用对 x
没有影响,因为它在内存中创建了存储值的独立副本。但是,h(x)
成功修改了 x
,因为只传递了引用,而不是副本。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
contract C {
uint[20] x;
function f() public {
g(x);
h(x);
}
function g(uint[20] memory y) internal pure {
y[2] = 3;
}
function h(uint[20] storage y) internal {
y[3] = 4;
}
}
作用域和声明
声明的变量将具有初始默认值,其字节表示形式全为零。变量的“默认值”是该类型典型的“零状态”。例如,bool
的默认值为 false
。uint
或 int
类型的默认值为 0
。对于静态大小的数组和 bytes1
到 bytes32
,每个单个元素都将初始化为与其类型相对应的默认值。对于动态大小的数组、bytes
和 string
,默认值为空数组或字符串。对于 enum
类型,默认值为其第一个成员。
Solidity 中的作用域遵循 C99(以及许多其他语言)的广泛作用域规则:变量从其声明后的位置开始到包含声明的最小 { }
块的末尾可见。作为此规则的例外,在 for 循环的初始化部分声明的变量仅在 for 循环结束之前可见。
类似参数的变量(函数参数、修饰符参数、捕获参数,……)在后续代码块中可见——对于函数和修饰符参数,为函数和修饰符的主体,对于捕获参数,为捕获块。
在代码块之外声明的变量和其他项(例如函数、合约、用户定义类型等)在声明之前可见。这意味着您可以在声明状态变量之前使用它们,并递归调用函数。
因此,以下示例将编译而不会产生警告,因为这两个变量具有相同的名称,但作用域不同。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
contract C {
function minimalScoping() pure public {
{
uint same;
same = 1;
}
{
uint same;
same = 3;
}
}
}
作为 C99 作用域规则的一个特殊示例,请注意,在以下示例中,对 x
的第一次赋值实际上将赋值给外部变量,而不是内部变量。在任何情况下,您都会收到有关外部变量被遮蔽的警告。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
// This will report a warning
contract C {
function f() pure public returns (uint) {
uint x = 1;
{
x = 2; // this will assign to the outer variable
uint x;
}
return x; // x has value 2
}
}
警告
在 0.5.0 版本之前,Solidity 遵循与 JavaScript 相同的作用域规则,即在函数中任何位置声明的变量在整个函数中都处于作用域内,无论它在何处声明。以下示例显示了一个以前可以编译但从 0.5.0 版本开始会导致错误的代码片段。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
// This will not compile
contract C {
function f() pure public returns (uint) {
x = 2;
uint x;
return x;
}
}
checked 或 unchecked 算术
溢出或下溢是指在对不受限制的整数执行算术运算时,结果值落在结果类型范围之外的情况。
在 Solidity 0.8.0 之前,算术运算在发生下溢或上溢时始终会进行环绕,导致广泛使用引入额外检查的库。
从 Solidity 0.8.0 开始,所有算术运算默认情况下在上溢和下溢时都会回滚,从而使使用这些库变得不必要。
要获得之前的行为,可以使用 unchecked
块
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract C {
function f(uint a, uint b) pure public returns (uint) {
// This subtraction will wrap on underflow.
unchecked { return a - b; }
}
function g(uint a, uint b) pure public returns (uint) {
// This subtraction will revert on underflow.
return a - b;
}
}
对 f(2, 3)
的调用将返回 2**256-1
,而 g(2, 3)
将导致断言失败。
可以在块内的任何位置使用 unchecked
块,但不能用作块的替代。它也不能嵌套。
该设置仅影响语法上位于块内的语句。从 unchecked
块中调用的函数不会继承该属性。
注意
为了避免歧义,您不能在 unchecked
块内使用 _;
。
以下运算符将在溢出或下溢时导致断言失败,如果在 unchecked 块内使用,则会进行环绕而不会出现错误
++
,--
,+
,二元 -
,一元 -
,*
,/
,%
,**
+=
, -=
, *=
, /=
, %=
警告
无法使用 unchecked
块禁用对零除或对零求模的检查。
注意
位运算符不执行溢出或下溢检查。当使用位移运算符(<<
,>>
,<<=
,>>=
)代替整数除法和乘以 2 的幂时,这一点尤其明显。例如,type(uint256).max << 3
不会回滚,即使 type(uint256).max * 8
会回滚。
注意
在 int x = type(int).min; -x;
中的第二条语句会导致溢出,因为负范围可以容纳比正范围多一个值。
显式类型转换始终会截断,并且不会导致断言失败,但从整数到枚举类型的转换除外。
错误处理:断言、要求、回滚和异常
Solidity 使用状态回滚异常来处理错误。这种异常会撤消对当前调用(及其所有子调用)中状态所做的所有更改,并向调用者发出错误标记。
当子调用中发生异常时,除非在 try/catch
语句中捕获,否则它们会“冒泡”(即,重新抛出异常)。此规则的例外是 send
和低级函数 call
、delegatecall
和 staticcall
:如果发生异常,它们会将其第一个返回值设置为 false
,而不是“冒泡”。
警告
如果调用的帐户不存在,低级函数 call
、delegatecall
和 staticcall
会将其第一个返回值设置为 true
,这是 EVM 设计的一部分。如果需要,必须在调用之前检查帐户是否存在。
异常可以包含错误数据,这些数据以 错误实例 的形式传递回调用者。内置错误 Error(string)
和 Panic(uint256)
由特殊函数使用,如下所述。 Error
用于“常规”错误条件,而 Panic
用于在无错误代码中不应存在的错误。
通过 assert
导致恐慌,通过 require
导致错误
便利函数 assert
和 require
可用于检查条件,并在条件不满足时抛出异常。
assert
函数会创建一个类型为 Panic(uint256)
的错误。编译器在以下列出的某些情况下也会创建相同的错误。
Assert 仅应用于测试内部错误,并检查不变式。正常运行的代码永远不会创建 Panic,即使在无效外部输入的情况下也是如此。如果发生这种情况,则您的合约中存在错误,您应该修复它。语言分析工具可以评估您的合约,以识别会导致 Panic 的条件和函数调用。
在以下情况下会生成 Panic 异常。随错误数据一起提供的错误代码指示 panic 的类型。
0x00:用于通用编译器插入的 panic。
0x01:如果您调用
assert
,其参数计算结果为 false。0x11:如果算术运算导致溢出或下溢,而不在
unchecked { ... }
块中。0x12;如果您除以或模以零(例如
5 / 0
或23 % 0
)。0x21:如果您将过大或负的值转换为枚举类型。
0x22:如果您访问了编码不正确的存储字节数组。
0x31:如果您在空数组上调用
.pop()
。0x32:如果您在超出边界或负索引处访问数组、
bytesN
或数组切片(即x[i]
,其中i >= x.length
或i < 0
)。0x41:如果您分配了过多内存或创建了过大的数组。
0x51:如果您调用了内部函数类型的零初始化变量。
require
函数会创建无数据的错误或类型为 Error(string)
的错误。它应该用于确保在执行时无法检测到的有效条件。这包括对输入或从外部合约调用返回的值的条件。
注意
目前无法将自定义错误与 require
结合使用。请改用 if (!condition) revert CustomError();
。
编译器在以下情况下会生成 Error(string)
异常(或无数据异常)
调用
require(x)
,其中x
计算结果为false
。如果您使用
revert()
或revert("description")
。如果您执行外部函数调用,目标合约不包含代码。
如果您的合约通过无
payable
修饰符的公共函数(包括构造函数和回退函数)接收 Ether。如果您的合约通过公共 getter 函数接收 Ether。
对于以下情况,外部调用的错误数据(如果提供)将被转发。这意味着它可以导致 Error
或 Panic
(或任何其他提供的内容)。
如果
.transfer()
失败。如果您通过消息调用调用函数,但该函数未正常完成(即它耗尽了 gas、没有匹配的函数或它本身抛出了异常),除非使用低级操作
call
、send
、delegatecall
、callcode
或staticcall
。低级操作从不抛出异常,而是通过返回false
来指示失败。如果您使用
new
关键字创建合约,但合约创建 未正常完成。
您可以选择为 require
提供消息字符串,但不能为 assert
提供。
注意
如果您未为 require
提供字符串参数,它将使用空错误数据恢复,甚至不包括错误选择器。
以下示例显示了如何使用 require
检查输入条件,以及使用 assert
进行内部错误检查。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
contract Sharer {
function sendHalf(address payable addr) public payable returns (uint balance) {
require(msg.value % 2 == 0, "Even value required.");
uint balanceBeforeTransfer = address(this).balance;
addr.transfer(msg.value / 2);
// Since transfer throws an exception on failure and
// cannot call back here, there should be no way for us to
// still have half of the Ether.
assert(address(this).balance == balanceBeforeTransfer - msg.value / 2);
return address(this).balance;
}
}
在内部,Solidity 执行恢复操作(指令 0xfd
)。这会导致 EVM 恢复对状态所做的所有更改。恢复的原因是,没有安全的方式继续执行,因为预期效果没有发生。因为我们想保持事务的原子性,所以最安全的做法是恢复所有更改,使整个事务(或至少调用)没有效果。
在这两种情况下,调用者都可以使用 try
/catch
对此类失败做出反应,但被调用者的更改将始终被恢复。
注意
Panic 异常在 Solidity 0.8.0 之前使用 invalid
操作码,这会消耗调用可用的所有 gas。使用 require
的异常在 Metropolis 版本发布之前会消耗所有 gas。
revert
可以使用 revert
语句和 revert
函数触发直接恢复。
revert
语句将自定义错误作为直接参数(无括号)
revert CustomError(arg1, arg2);
为了向后兼容,还有 revert()
函数,它使用括号并接受字符串
revert(); revert(“description”);
错误数据将被传递回调用者,并可在那里被捕获。使用 revert()
会导致恢复,但没有任何错误数据,而 revert("description")
会创建一个 Error(string)
错误。
使用自定义错误实例通常比字符串描述便宜得多,因为您可以使用错误的名称来描述它,该名称仅用四个字节编码。可以通过 NatSpec 提供更长的描述,不会产生任何成本。
以下示例显示了如何使用错误字符串和自定义错误实例以及 revert
和等效的 require
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract VendingMachine {
address owner;
error Unauthorized();
function buy(uint amount) public payable {
if (amount > msg.value / 2 ether)
revert("Not enough Ether provided.");
// Alternative way to do it:
require(
amount <= msg.value / 2 ether,
"Not enough Ether provided."
);
// Perform the purchase.
}
function withdraw() public {
if (msg.sender != owner)
revert Unauthorized();
payable(msg.sender).transfer(address(this).balance);
}
}
两种方式 if (!condition) revert(...);
和 require(condition, ...);
是等效的,只要 revert
和 require
的参数没有副作用,例如它们只是字符串。
注意
require
函数的计算方式与任何其他函数相同。这意味着所有参数都在函数本身执行之前进行计算。特别是,在 require(condition, f())
中,即使 condition
为真,也会执行函数 f
。
提供的字符串被 abi 编码,就像它对函数 Error(string)
的调用一样。在上面的示例中,revert("Not enough Ether provided.");
返回以下十六进制数作为错误返回数据
0x08c379a0 // Function selector for Error(string)
0x0000000000000000000000000000000000000000000000000000000000000020 // Data offset
0x000000000000000000000000000000000000000000000000000000000000001a // String length
0x4e6f7420656e6f7567682045746865722070726f76696465642e000000000000 // String data
调用者可以使用 try
/catch
检索提供的消息,如下所示。
注意
以前有一个名为 throw
的关键字,其语义与 revert()
相同,该关键字在 0.4.13 版本中已弃用,并在 0.5.0 版本中删除。
try
/catch
可以使用 try/catch 语句捕获外部调用中的错误,如下所示
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.1;
interface DataFeed { function getData(address token) external returns (uint value); }
contract FeedConsumer {
DataFeed feed;
uint errorCount;
function rate(address token) public returns (uint value, bool success) {
// Permanently disable the mechanism if there are
// more than 10 errors.
require(errorCount < 10);
try feed.getData(token) returns (uint v) {
return (v, true);
} catch Error(string memory /*reason*/) {
// This is executed in case
// revert was called inside getData
// and a reason string was provided.
errorCount++;
return (0, false);
} catch Panic(uint /*errorCode*/) {
// This is executed in case of a panic,
// i.e. a serious error like division by zero
// or overflow. The error code can be used
// to determine the kind of error.
errorCount++;
return (0, false);
} catch (bytes memory /*lowLevelData*/) {
// This is executed in case revert() was used.
errorCount++;
return (0, false);
}
}
}
try
关键字后面必须跟一个表示外部函数调用或合约创建的表达式(new ContractName()
)。表达式内部的错误不会被捕获(例如,如果它是一个也涉及内部函数调用的复杂表达式),只有外部调用本身发生的恢复会被捕获。后面的 returns
部分(可选)声明了与外部调用返回的类型匹配的返回值变量。如果没有错误,这些变量将被赋值,并且合约的执行将在第一个成功块内继续。如果到达成功块的末尾,执行将在 catch
块之后继续。
Solidity 支持不同类型的 catch 块,具体取决于错误的类型
catch Error(string memory reason) { ... }
: 如果错误是由revert("reasonString")
或require(false, "reasonString")
(或导致此类异常的内部错误)引起的,则执行此 catch 子句。catch Panic(uint errorCode) { ... }
: 如果错误是由 panic 引起的,例如,由失败的assert
、零除、无效数组访问、算术溢出等引起的,则将执行此 catch 子句。catch (bytes memory lowLevelData) { ... }
: 如果错误签名与任何其他子句不匹配,如果解码错误消息时发生错误,或者如果异常未提供任何错误数据,则执行此子句。在这种情况下,声明的变量可以访问低级错误数据。catch { ... }
: 如果你不关心错误数据,可以使用catch { ... }
(即使作为唯一的 catch 子句)而不是前面的子句。
计划在将来支持其他类型的错误数据。字符串 Error
和 Panic
当前按原样解析,不被视为标识符。
为了捕获所有错误情况,你必须至少包含 catch { ... }
子句或 catch (bytes memory lowLevelData) { ... }
子句。
在 returns
和 catch
子句中声明的变量仅在后面的块中有效。
注意
如果在 try/catch 语句内部解码返回数据时发生错误,这会导致当前执行的合约出现异常,因此不会被 catch 子句捕获。如果解码 catch Error(string memory reason)
时发生错误,并且存在低级 catch 子句,则此错误将在此处被捕获。
注意
如果执行到达 catch 块,则外部调用的状态更改效果将被回滚。如果执行到达成功块,则效果未被回滚。如果效果已回滚,则执行将继续在 catch 块中,或 try/catch 语句本身将回滚(例如,由于如上所述的解码失败,或者由于未提供低级 catch 子句)。
注意
调用失败的原因可能是多种多样的。不要假设错误消息直接来自被调用合约:错误可能发生在调用链中更深的地方,被调用合约只是转发了它。此外,它也可能是由于超出气体限制,而不是故意的错误条件:调用者在调用中始终保留至少 1/64 的气体,因此即使被调用合约耗尽气体,调用者仍然有一些气体剩余。