表达式和控制结构

控制结构

Solidity 中提供了大多数花括号语言中的控制结构。

包括:ifelsewhiledoforbreakcontinuereturn,它们具有与 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} 不会调用函数,并且 valuegas 设置会丢失,只有 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 版本之前,可以将赋值给更小尺寸的元组,要么从左侧填充,要么从右侧填充(无论哪个为空)。现在不允许这样做,因此两侧必须具有相同数量的组件。

警告

当同时对多个变量进行赋值时,如果涉及引用类型,请小心,因为它会导致意外的复制行为。

数组和结构体的复杂性

对于非值类型(如数组和结构体,包括 bytesstring),赋值的语义更加复杂,有关详细信息,请参阅 数据位置和赋值行为

在下面的示例中,对 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 的默认值为 falseuintint 类型的默认值为 0。对于静态大小的数组和 bytes1bytes32,每个单个元素都将初始化为与其类型相对应的默认值。对于动态大小的数组、bytesstring,默认值为空数组或字符串。对于 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 和低级函数 calldelegatecallstaticcall:如果发生异常,它们会将其第一个返回值设置为 false,而不是“冒泡”。

警告

如果调用的帐户不存在,低级函数 calldelegatecallstaticcall 会将其第一个返回值设置为 true,这是 EVM 设计的一部分。如果需要,必须在调用之前检查帐户是否存在。

异常可以包含错误数据,这些数据以 错误实例 的形式传递回调用者。内置错误 Error(string)Panic(uint256) 由特殊函数使用,如下所述。 Error 用于“常规”错误条件,而 Panic 用于在无错误代码中不应存在的错误。

通过 assert 导致恐慌,通过 require 导致错误

便利函数 assertrequire 可用于检查条件,并在条件不满足时抛出异常。

assert 函数会创建一个类型为 Panic(uint256) 的错误。编译器在以下列出的某些情况下也会创建相同的错误。

Assert 仅应用于测试内部错误,并检查不变式。正常运行的代码永远不会创建 Panic,即使在无效外部输入的情况下也是如此。如果发生这种情况,则您的合约中存在错误,您应该修复它。语言分析工具可以评估您的合约,以识别会导致 Panic 的条件和函数调用。

在以下情况下会生成 Panic 异常。随错误数据一起提供的错误代码指示 panic 的类型。

  1. 0x00:用于通用编译器插入的 panic。

  2. 0x01:如果您调用 assert,其参数计算结果为 false。

  3. 0x11:如果算术运算导致溢出或下溢,而不在 unchecked { ... } 块中。

  4. 0x12;如果您除以或模以零(例如 5 / 023 % 0)。

  5. 0x21:如果您将过大或负的值转换为枚举类型。

  6. 0x22:如果您访问了编码不正确的存储字节数组。

  7. 0x31:如果您在空数组上调用 .pop()

  8. 0x32:如果您在超出边界或负索引处访问数组、bytesN 或数组切片(即 x[i],其中 i >= x.lengthi < 0)。

  9. 0x41:如果您分配了过多内存或创建了过大的数组。

  10. 0x51:如果您调用了内部函数类型的零初始化变量。

require 函数会创建无数据的错误或类型为 Error(string) 的错误。它应该用于确保在执行时无法检测到的有效条件。这包括对输入或从外部合约调用返回的值的条件。

注意

目前无法将自定义错误与 require 结合使用。请改用 if (!condition) revert CustomError();

编译器在以下情况下会生成 Error(string) 异常(或无数据异常)

  1. 调用 require(x),其中 x 计算结果为 false

  2. 如果您使用 revert()revert("description")

  3. 如果您执行外部函数调用,目标合约不包含代码。

  4. 如果您的合约通过无 payable 修饰符的公共函数(包括构造函数和回退函数)接收 Ether。

  5. 如果您的合约通过公共 getter 函数接收 Ether。

对于以下情况,外部调用的错误数据(如果提供)将被转发。这意味着它可以导致 ErrorPanic(或任何其他提供的内容)。

  1. 如果 .transfer() 失败。

  2. 如果您通过消息调用调用函数,但该函数未正常完成(即它耗尽了 gas、没有匹配的函数或它本身抛出了异常),除非使用低级操作 callsenddelegatecallcallcodestaticcall。低级操作从不抛出异常,而是通过返回 false 来指示失败。

  3. 如果您使用 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, ...); 是等效的,只要 revertrequire 的参数没有副作用,例如它们只是字符串。

注意

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 子句)而不是前面的子句。

计划在将来支持其他类型的错误数据。字符串 ErrorPanic 当前按原样解析,不被视为标识符。

为了捕获所有错误情况,你必须至少包含 catch { ... } 子句或 catch (bytes memory lowLevelData) { ... } 子句。

returnscatch 子句中声明的变量仅在后面的块中有效。

注意

如果在 try/catch 语句内部解码返回数据时发生错误,这会导致当前执行的合约出现异常,因此不会被 catch 子句捕获。如果解码 catch Error(string memory reason) 时发生错误,并且存在低级 catch 子句,则此错误将在此处被捕获。

注意

如果执行到达 catch 块,则外部调用的状态更改效果将被回滚。如果执行到达成功块,则效果未被回滚。如果效果已回滚,则执行将继续在 catch 块中,或 try/catch 语句本身将回滚(例如,由于如上所述的解码失败,或者由于未提供低级 catch 子句)。

注意

调用失败的原因可能是多种多样的。不要假设错误消息直接来自被调用合约:错误可能发生在调用链中更深的地方,被调用合约只是转发了它。此外,它也可能是由于超出气体限制,而不是故意的错误条件:调用者在调用中始终保留至少 1/64 的气体,因此即使被调用合约耗尽气体,调用者仍然有一些气体剩余。