表达式和控制结构
控制结构
大括号语言中已知的大多数控制结构都具有一致性:
有: if
, else
, while
, do
, for
, break
, continue
, return
,使用C或JavaScript中已知的常规语义。
坚固性还支持以下形式的异常处理 try
/catch
-语句,但仅限于 external function calls 和合同创建电话。可以使用 revert statement 。
括号可以 not 对于条件语句可以省略,但在单个语句体周围可以省略大括号。
注意,没有从非布尔型到布尔型的类型转换,就像在C和JavaScript中一样,所以 if (1) {{ ... }}
是 not 有效 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
无法在构造函数中使用,因为尚未创建实际协定。
其他合同的职能必须从外部调用。对于外部调用,必须将所有函数参数复制到内存中。
注解
从一个契约到另一个契约的函数调用不会创建自己的事务,而是作为整个事务的一部分的消息调用。
在调用其他合同的函数时,可以通过特殊选项指定随调用发送的wei或gas的数量。 {{value: 10, gas: 10000}}
. 请注意,不鼓励明确指定气体值,因为操作码的气体成本将来可能会发生变化。您发送给合同的任何金额都将添加到该合同的总余额中:
// 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}(); }
}
你需要使用修改器 payable
与 info
函数,否则, 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
操作码,用于检查将要调用的约定是否实际存在(它包含代码),如果不存在,则会导致异常。请注意,在以下情况下不执行此检查 low-level calls 它们对地址而不是合同实例进行操作。
如果被调用的协定本身抛出异常或用完气体,函数调用也会导致异常。
警告
任何与另一个合同的交互都会带来潜在的危险,特别是如果事先不知道合同的源代码。当前的合同将控制权移交给被调用的合同,这可能会做任何事情。即使被调用的协定继承自已知的父协定,继承协定也只需要具有正确的接口。然而,合同的执行可能是完全任意的,因此会造成危险。此外,在它调用系统的其他合同或甚至在第一次调用返回之前重新调用调用合同时,请做好准备。这意味着被调用契约可以通过其函数更改调用契约的状态变量。以这样的方式编写函数:例如,对外部函数的调用发生在契约中状态变量的任何更改之后,因此契约不易受到可重入性攻击。
注解
在固含量为0.6.2之前,建议使用指定值和气体的方法 f.value(x).gas(g)()
. 这在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;
}
}
省略函数参数名称
可以省略未使用参数(尤其是返回参数)的名称。这些参数仍将存在于堆栈中,但无法访问。
// 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();
}
}
如示例中所示,可以在创建 D
使用 value
选项,但不可能限制气体量。如果创建失败(由于堆栈不足、平衡不足或其他问题),则抛出异常。
咸合同创建/创建2
当创建一个契约时,契约的地址是根据创建契约的地址和一个随着每次契约创建而增加的计数器来计算的。
如果指定选项 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,
arg
))
)))));
D d = new D{salt: salt}(arg);
require(address(d) == predictedAddress);
}
}
警告
盐渍创作有一些特殊之处。合同被销毁后,可以在同一地址重新创建。然而,即使创建的字节码是相同的(这是一个要求,因为否则地址会改变),新创建的契约可能有不同的部署字节码。这是因为编译器可以查询两个创建之间可能已更改的外部状态,并在存储之前将其合并到已部署的字节码中。
表达式的计算顺序
表达式的求值顺序没有指定(更正式地说,表达式树中一个节点的子节点的求值顺序没有指定,但它们的求值顺序当然是在节点本身之前计算的)。它只保证语句按顺序执行,并完成布尔表达式的短路。
转让
销毁分配并返回多个值
solidity在内部允许使用元组类型,即具有潜在不同类型的对象列表,这些对象的编号在编译时是常量。这些元组可用于同时返回多个值。然后可以将这些变量分配给新声明的变量或预先存在的变量(或通常的lvalue)。
元组不是固定的适当类型,它们只能用于构成表达式的句法分组。
// 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
见 Data location and assignment behaviour 有关详细信息。
在下面的示例中,调用 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循环结束之前才可见。
类似参数的变量(函数参数、修饰符参数、catch参数,…)在后面的代码块中是可见的——函数/修饰符的主体和catch参数的catch块。
在代码块之外声明的变量和其他项(例如函数、协定、用户定义的类型等)即使在声明之前也是可见的。这意味着您可以在声明状态变量之前使用它们,并递归地调用函数。
因此,由于两个变量的名称相同,但作用域不相交,因此下面的示例将编译而不发出警告。
// 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;
}
}
选中或取消选中的算法
上溢或下溢是这样一种情况:当对不受限制的整数执行算术运算时,结果值落在结果类型的范围之外。
在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
挡路。
以下运算符将在溢出或下溢时导致断言失败,如果在未经检查的挡路中使用,它们将不会出现错误:
++
, --
, +
, binary -
, unary -
, *
, /
, %
, **
+=
, -=
, *=
, /=
, %=
警告
属性禁用被零除或以零为模的检查是不可能的。 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 instances 。内置错误 Error(string)
和 Panic(uint256)
由特殊功能使用,如下所述。 Error
用于“常规”错误条件,而 Panic
用于不应出现在无错误代码中的错误。
通过以下途径出现恐慌 assert
和错误通过 require
便利功能 assert
和 require
可用于检查条件,如果条件不满足,则引发异常。
这个 assert
函数创建类型为 Panic(uint256)
。在下面列出的某些情况下,编译器会创建相同的错误。
Assert应该只用于测试内部错误和检查不变量。正常运行的代码永远不会造成死机,即使是在无效的外部输入上也不会。如果发生这种情况,那么您的合同中存在错误,您应该进行修复。语言分析工具可以评估您的约定,以确定将导致恐慌的条件和函数调用。
在以下情况下会生成死机异常。随错误数据提供的错误代码指示死机类型。
0x00:用于泛型编译器插入的死机。
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")
。如果对不包含代码的协定执行外部函数调用。
如果您的合同通过公共职能部门接收到 Ether
payable
修饰符(包括构造函数和回退函数)。如果您的合同通过公共getter函数接收到 Ether 。
对于以下情况,将转发来自外部呼叫(如果提供)的错误数据。这意味着它可以导致 Error 或者是 Panic (或任何其他给予):
如果A
.transfer()
失败。如果您通过消息调用调用一个函数,但它没有正确完成(即,它耗尽了气体,没有匹配的函数,或者抛出异常本身),除非是低级操作
call
,send
,delegatecall
,callcode
或staticcall
使用。低级操作从不引发异常,而是通过返回来指示失败false
.如果使用
new
关键字,但合同创建 does not finish properly .
您可以选择为提供消息字符串 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 money.
assert(address(this).balance == balanceBeforeTransfer - msg.value / 2);
return address(this).balance;
}
}
在内部,SOLIDITY执行恢复操作(指令 0xfd
)。这会导致EVM恢复对状态所做的所有更改。恢复的原因是没有安全的方式继续执行,因为没有出现预期的效果。因为我们希望保持事务的原子性,所以最安全的操作是恢复所有更改,并使整个事务(或至少是调用)不起作用。
在这两种情况下,调用方都可以使用 try
/catch
,但调用方中的更改将始终恢复。
注解
死机异常用于使用 invalid
固体0.8.0之前的操作码,它消耗了呼叫可用的所有气体。使用 require
在大都会放行之前一直消耗所有的汽油。
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())
该函数 f
将被执行,即使在 condition
是真的。
提供的字符串是 abi-encoded 好像是对函数的调用 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
阻碍。
根据错误类型的不同,实心度支持不同类型的CATCH块:
catch Error(string memory reason) {{ ... }}
:如果错误是由以下原因引起的,则执行此CATCH子句revert("reasonString")
或require(false, "reasonString")
(或导致此类异常的内部错误)。catch Panic(uint errorCode) {{ ... }}
:如果错误是由死机(即故障)引起的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子句)。
注解
失败调用背后的原因可能是多方面的。从契约中转发的消息可能并不是直接从契约中调用的错误。此外,这可能是由于没有汽油的情况,而不是故意的错误情况:呼叫者总是在呼叫中保留63/64的气体,因此,即使被呼叫的合同没有汽油,呼叫者仍有一些剩余的气体。