类型

Solidity 是一种静态类型语言,这意味着每个变量(状态和局部变量)的类型都需要指定。Solidity 提供了几种基本类型,可以组合成复杂的类型。

此外,类型可以在包含运算符的表达式中相互交互。有关各种运算符的快速参考,请参阅 运算符优先级顺序.

Solidity 中不存在“未定义”或“空”值的概念,但新声明的变量始终具有一个 默认值,该值取决于其类型。要处理任何意外值,您应该使用 revert 函数 来回滚整个交易,或返回一个元组,该元组的第二个 bool 值表示成功。

值类型

以下称为值类型,因为它们的变量始终按值传递,即在用作函数参数或在赋值中时始终被复制。

布尔值

bool: 可能的值是常量 truefalse

运算符

  • ! (逻辑否定)

  • && (逻辑与,"and")

  • || (逻辑或,"or")

  • == (相等)

  • != (不相等)

运算符 ||&& 应用通用的短路规则。这意味着在表达式 f(x) || g(y) 中,如果 f(x) 评估为 true,则 g(y) 不会被评估,即使它可能具有副作用。

整数

int / uint: 各种大小的有符号和无符号整数。关键字 uint8uint2568 为步长(从 8 位到 256 位的无符号数)和 int8int256uintint 分别是 uint256int256 的别名。

运算符

  • 比较: <=, <, ==, !=, >=, > (评估为 bool)

  • 位运算符: &, |, ^ (按位异或), ~ (按位取反)

  • 移位运算符: << (左移), >> (右移)

  • 算术运算符: +, -, 一元 - (仅适用于有符号整数), *, /, % (模运算), ** (指数运算)

对于整数类型 X,您可以使用 type(X).mintype(X).max 来访问该类型可表示的最小值和最大值。

警告

Solidity 中的整数被限制在一个特定范围内。例如,对于 uint32,范围为 02**32 - 1。这两种模式是在这些类型上执行算术运算的方式:"包装"或"未经检查"模式和"已检查"模式。默认情况下,算术运算始终是"已检查"的,这意味着如果运算结果超出类型的取值范围,则通过 失败断言 来回滚调用。您可以使用 unchecked { ... } 切换到"未经检查"模式。有关更多详细信息,请参阅有关 unchecked 的部分。

比较

比较的值是通过比较整数值获得的值。

位运算

位运算是在数字的二进制补码表示上执行的。这意味着,例如 ~int256(0) == int256(-1)

移位

移位运算的结果具有左操作数的类型,将结果截断以匹配该类型。右操作数必须是无符号类型,尝试对有符号类型进行移位将产生编译错误。

移位可以通过以下方式使用乘以二的幂来"模拟"。请注意,截断到左操作数的类型始终在最后执行,但没有明确提及。

  • x << y 等价于数学表达式 x * 2**y

  • x >> y 等价于数学表达式 x / 2**y,向负无穷大舍入。

警告

在版本 0.5.0 之前,对于负 x 的右移 x >> y 等价于数学表达式 x / 2**y 向零舍入,即右移使用向上舍入(向零舍入)而不是向下舍入(向负无穷大舍入)。

注意

移位运算从不执行溢出检查,因为它们对算术运算进行了检查。相反,结果始终被截断。

加法、减法和乘法

加法、减法和乘法具有通常的语义,在溢出和下溢方面有两种不同的模式

默认情况下,所有算术运算都会检查下溢出或溢出,但可以使用 unchecked 块 禁用此功能,从而导致包装算术。有关更多详细信息,请参阅该部分。

表达式 -x 等效于 (T(0) - x),其中 Tx 的类型。它只能应用于有符号类型。如果 x 为负数,则 -x 的值可以为正数。由于二进制补码表示,还存在另一个注意事项

如果 int x = type(int).min;,那么 -x 不符合正数范围。这意味着 unchecked { assert(-x == x); } 可以正常工作,并且在启用检查模式下使用表达式 -x 将导致断言失败。

除法

由于操作结果的类型始终是其中一个操作数的类型,因此整数的除法始终会得到一个整数。在 Solidity 中,除法会向零取整。这意味着 int256(-5) / int256(2) == int256(-2)

请注意,相比之下,对 字面量 的除法将得到任意精度的分数。

注意

除以零会导致 Panic 错误。此检查**不能**通过 unchecked { ... } 禁用。

注意

表达式 type(int).min / (-1) 是唯一会导致除法溢出的情况。在启用检查算术模式的情况下,这将导致断言失败,而在包装模式下,该值将为 type(int).min

取模

取模运算 a % n 会产生操作数 a 除以操作数 n 后的余数 r,其中 q = int(a / n)r = a - (n * q)。这意味着取模的结果与它的左操作数(或零)具有相同的符号,并且对于负数 aa % n == -(-a % n) 成立。

  • int256(5) % int256(2) == int256(1)

  • int256(5) % int256(-2) == int256(1)

  • int256(-5) % int256(2) == int256(-1)

  • int256(-5) % int256(-2) == int256(-1)

注意

对零取模会导致 Panic 错误。此检查**不能**通过 unchecked { ... } 禁用。

指数运算

指数运算只适用于指数为无符号类型的运算。指数运算的结果类型始终等于底数的类型。请注意,它必须足够大以容纳结果,并为潜在的断言失败或包装行为做好准备。

注意

在启用检查模式下,指数运算仅对较小的底数使用相对便宜的 exp 操作码。对于 x**3 的情况,表达式 x*x*x 可能更便宜。在任何情况下,建议进行燃气成本测试和使用优化器。

注意

请注意,0**0 由 EVM 定义为 1

定点数

警告

Solidity 尚未完全支持定点数。可以声明它们,但不能对其进行赋值或从其赋值。

fixed / ufixed:有符号和无符号定点数,具有多种大小。关键字 ufixedMxNfixedMxN,其中 M 表示类型占用的位数,N 表示可用的小数位数。 M 必须可被 8 整除,范围从 8 到 256 位。 N 必须介于 0 到 80 之间(包括 0 和 80)。 ufixedfixed 分别是 ufixed128x18fixed128x18 的别名。

运算符

  • 比较: <=, <, ==, !=, >=, > (评估为 bool)

  • 算术运算符:+-、一元 -*/%(取模)

注意

浮点数(在许多语言中称为 floatdouble,更准确地说是 IEEE 754 数字)和定点数之间的主要区别在于,前者用于整数和小数部分(小数点后的部分)的位数是灵活的,而后者则是严格定义的。一般来说,在浮点数中,几乎整个空间都用于表示数字,而只有少数几位定义小数点的位数。

地址

地址类型有两种基本相同的变体。

  • address:保存 20 字节的值(以太坊地址的大小)。

  • address payable:与 address 相同,但具有附加成员 transfersend

这种区别背后的理念是,address payable 是一个可以向其发送以太坊的地址,而你应该不要向普通的 address 发送以太坊,例如因为它可能是一个没有被构建为接受以太坊的智能合约。

类型转换

允许从 address payableaddress 的隐式转换,而从 addressaddress payable 的转换必须通过 payable(<address>) 显式进行。

允许对 uint160、整数字面量、bytes20 和合约类型进行显式转换到 address 和从 address 进行显式转换。

只有类型为 address 和合约类型的表达式可以通过显式转换 payable(...) 转换为 address payable 类型。对于合约类型,此转换仅在合约可以接收以太坊时允许,即合约具有 receive 或可支付的回退函数。请注意,payable(0) 是有效的,并且是此规则的例外。

注意

如果你需要一个类型为 address 的变量,并且计划向其发送以太坊,那么将其类型声明为 address payable,以使此要求可见。此外,尝试尽早进行这种区分或转换。

addressaddress payable 之间的区别是在版本 0.5.0 中引入的。从该版本开始,合约不能隐式转换为 address 类型,但如果它们具有接收或可支付的回退函数,则仍然可以显式转换为 addressaddress payable

运算符

  • <=<==!=>=>

警告

如果你将使用更大字节大小的类型转换为 address,例如 bytes32,那么 address 将被截断。为了减少转换歧义,从版本 0.4.24 开始,编译器将强制你在转换中显式进行截断。例如,32 字节的值 0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCCC

你可以使用 address(uint160(bytes20(b))),这将得到 0x111122223333444455556666777788889999aAaa,或者你可以使用 address(uint160(uint256(b))),这将得到 0x777788889999AaAAbBbbCcccddDdeeeEfFFfCcCc

注意

符合 EIP-55 的混合大小写十六进制数字将自动被视为 address 类型的字面量。参见 地址字面量

地址的成员

有关地址所有成员的快速参考,请参见 地址类型的成员

  • balancetransfer

可以使用 balance 属性查询地址的余额,并使用 transfer 函数向可支付地址发送以太坊(以 wei 为单位)。

address payable x = payable(0x123);
address myAddress = address(this);
if (x.balance < 10 && myAddress.balance >= 10) x.transfer(10);

如果当前合约的余额不足或接收账户拒绝以太坊转账,则 transfer 函数会失败。 transfer 函数在失败时会回滚。

注意

如果 x 是一个合约地址,它的代码(更确切地说:它的 接收以太坊函数,如果存在,或者它的 回退函数,如果存在)将与 transfer 调用一起执行(这是 EVM 的一个特性,无法阻止)。如果该执行耗尽了 gas 或以任何方式失败,以太坊转账将被回滚,并且当前合约将停止并抛出异常。

  • 发送

sendtransfer 的低级对应物。如果执行失败,当前合约不会停止并抛出异常,但 send 会返回 false

警告

使用 send 存在一些风险:如果调用栈深度为 1024,则转账会失败(这总是可以被调用者强制执行),如果接收者耗尽了 gas,也会失败。因此,为了进行安全的以太坊转账,请始终检查 send 的返回值,使用 transfer,或者更好的是:使用接收者提取以太坊的模式。

  • calldelegatecallstaticcall

为了与不遵循 ABI 的合约进行交互,或者更直接地控制编码,提供了 calldelegatecallstaticcall 函数。它们都接收一个 bytes memory 参数,并返回成功条件(作为 bool)和返回的数据 (bytes memory)。函数 abi.encodeabi.encodePackedabi.encodeWithSelectorabi.encodeWithSignature 可用于编码结构化数据。

示例

bytes memory payload = abi.encodeWithSignature("register(string)", "MyName");
(bool success, bytes memory returnData) = address(nameReg).call(payload);
require(success);

警告

所有这些函数都是低级函数,应该谨慎使用。具体来说,任何未知的合约都可能是恶意的,如果你调用它,你就将控制权交给了该合约,该合约反过来可能会调用你的合约,因此在调用返回时,要做好你的状态变量发生变化的准备。与其他合约交互的常规方法是调用合约对象上的函数 (x.f())。

注意

早期的 Solidity 版本允许这些函数接收任意参数,并且还会以不同的方式处理类型为 bytes4 的第一个参数。这些边缘情况在 0.5.0 版本中被移除。

可以使用 gas 修饰符调整提供的 gas。

address(nameReg).call{gas: 1000000}(abi.encodeWithSignature("register(string)", "MyName"));

类似地,也可以控制提供的以太坊值。

address(nameReg).call{value: 1 ether}(abi.encodeWithSignature("register(string)", "MyName"));

最后,这些修饰符可以组合使用。它们的顺序无关紧要。

address(nameReg).call{gas: 1000000, value: 1 ether}(abi.encodeWithSignature("register(string)", "MyName"));

以类似的方式,可以使用 delegatecall 函数:区别在于只使用给定地址的代码,所有其他方面(存储、余额、...)都取自当前合约。 delegatecall 的目的是使用存储在另一个合约中的库代码。用户必须确保两个合约中的存储布局适合使用 delegatecall。

注意

在 homestead 之前,只提供了一个有限的变体,称为 callcode,它不提供对原始 msg.sendermsg.value 值的访问。该函数在 0.5.0 版本中被移除。

从 byzantium 开始,也可以使用 staticcall。它基本上与 call 相同,但如果被调用的函数以任何方式修改了状态,就会回滚。

所有三个函数 calldelegatecallstaticcall 都是非常底层的函数,应该只在万不得已时使用,因为它们破坏了 Solidity 的类型安全性。

gas 选项在所有三种方法上都可用,而 value 选项只在 call 上可用。

注意

无论是否从状态读取或写入状态,最好避免在智能合约代码中依赖硬编码的 gas 值,因为这会带来很多陷阱。此外,对 gas 的访问在将来可能会改变。

  • codecodehash

你可以查询任何智能合约的部署代码。使用 .code 获取 EVM 字节码作为 bytes memory,它可能为空。使用 .codehash 获取该代码的 Keccak-256 哈希值(作为 bytes32)。请注意,addr.codehash 比使用 keccak256(addr.code) 更便宜。

注意

所有合约都可以转换为 address 类型,因此可以使用 address(this).balance 查询当前合约的余额。

合约类型

每个 合约 定义了自己的类型。你可以将合约隐式转换为它们继承的合约。合约可以显式转换为和从 address 类型进行转换。

只有当合约类型具有接收或可支付回退函数时,才能显式转换为和从 address payable 类型进行转换。转换仍然使用 address(x) 进行。如果合约类型没有接收或可支付回退函数,可以使用 payable(address(x)) 将其转换为 address payable。你可以在关于 地址类型 的部分找到更多信息。

注意

在 0.5.0 版本之前,合约直接从地址类型派生,并且 addressaddress payable 之间没有区别。

如果你声明一个合约类型的局部变量 (MyContract c),你可以调用该合约上的函数。注意从同一个合约类型的地方对其进行赋值。

你也可以实例化合约(意味着它们是新创建的)。你可以在 “通过 new 创建合约” 部分找到更多详细信息。

合约的数据表示与 address 类型的表示相同,这种类型也用在 ABI 中。

合约不支持任何运算符。

合约类型的成员是合约的外部函数,包括任何标记为 public 的状态变量。

对于一个合约 C,可以使用 type(C) 访问合约的 类型信息

固定大小的字节数组

值类型 bytes1bytes2bytes3,...,bytes32 保存一个从一到最多 32 个字节的字节序列。

运算符

  • 比较: <=, <, ==, !=, >=, > (评估为 bool)

  • 位运算符: &, |, ^ (按位异或), ~ (按位取反)

  • 移位运算符: << (左移), >> (右移)

  • 索引访问:如果 x 的类型为 bytesI,则 x[k] 对于 0 <= k < I 返回第 k 个字节(只读)。

移位运算符使用无符号整数类型作为右操作数(但返回左操作数的类型),它表示要移位的位数。使用有符号类型移位会产生编译错误。

成员

  • .length 返回字节数组的固定长度(只读)。

注意

类型 bytes1[] 是一个字节数组,但由于填充规则,它会浪费每个元素 31 字节的空间(存储除外)。最好使用 bytes 类型。

注意

在 0.8.0 版本之前,byte 用于作为 bytes1 的别名。

地址字面量

通过地址校验测试的十六进制字面量,例如 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF,属于 address 类型。长度在 39 到 41 位之间但未通过校验测试的十六进制字面量会产生错误。您可以为整数类型添加前缀(0)或为 bytesNN 类型添加后缀(0)来消除此错误。

注意

混合大小写的地址校验格式在 EIP-55 中定义。

有理数和整数字面量

整数字面量由 0 到 9 范围内的数字序列组成。它们被解释为十进制数。例如,69 表示六十九。Solidity 中不存在八进制字面量,前导零无效。

十进制小数字面量由一个 . 组成,其后至少有一个数字。例如 .11.3(但不包括 1.)。

也支持 2e10 形式的科学记数法,其中尾数可以是小数,但指数必须是整数。字面量 MeE 等于 M * 10**E。例如 2e10-2e102e-102.5e1

可以使用下划线来分隔数字字面量的数字以提高可读性。例如,十进制 123_000、十六进制 0x2eff_abde、科学十进制记数法 1_2e345_678 都是有效的。下划线只能在两个数字之间使用,并且只允许一个连续的下划线。包含下划线的数字字面量没有额外的语义含义,下划线会被忽略。

数字字面量表达式在被转换为非字面量类型之前(即,通过将它们与除数字字面量表达式(如布尔字面量)之外的任何内容一起使用或通过显式转换)会保留任意精度。这意味着数字字面量表达式中的计算不会溢出,除法不会截断。

例如,(2**800 + 1) - 2**800 会生成常量 1(类型为 uint8),尽管中间结果甚至无法容纳机器字长。此外,.5 * 8 会生成整数 4(尽管中间使用了非整数)。

警告

虽然大多数运算符在应用于字面量时会生成字面量表达式,但有一些运算符不遵循这种模式

  • 三元运算符 (... ? ... : ...),

  • 数组下标 (<array>[<index>])。

您可能会期待 255 + (true ? 1 : 0)255 + [1, 2, 3][0] 这样的表达式等效于直接使用字面量 256,但实际上它们是在类型 uint8 中计算的,可能会溢出。

只要操作数是整数,任何可以应用于整数的运算符也可以应用于数字字面量表达式。如果两个操作数中的任何一个是分数,则不允许位运算,如果指数是分数,则不允许指数运算(因为这可能会导致非有理数)。

以字面量作为左(或基数)操作数和整数类型作为右(指数)操作数的移位和指数运算始终在 uint256(对于非负字面量)或 int256(对于负字面量)类型中执行,而与右(指数)操作数的类型无关。

警告

在 Solidity 0.4.0 之前的版本中,整数字面量上的除法会截断,但现在会转换为有理数,即 5 / 2 不等于 2,而是等于 2.5

注意

Solidity 为每个有理数都有一个数字字面量类型。整数字面量和有理数字面量属于数字字面量类型。此外,所有数字字面量表达式(即仅包含数字字面量和运算符的表达式)都属于数字字面量类型。因此,数字字面量表达式 1 + 22 + 1 都属于相同的有理数三的数字字面量类型。

注意

数字字面量表达式在与非字面量表达式一起使用时会立即转换为非字面量类型。不考虑类型,分配给 b 的表达式的值将计算为一个整数。由于 a 的类型为 uint128,表达式 2.5 + a 必须具有一个适当的类型。由于 2.5uint128 的类型没有公共类型,因此 Solidity 编译器无法接受这段代码。

uint128 a = 1;
uint128 b = 2.5 + a + 0.5;

字符串字面量和类型

字符串字面量使用双引号或单引号 ("foo"'bar') 写入,也可以拆分为多个连续的部分 ("foo" "bar" 等效于 "foobar"),这在处理长字符串时很有用。它们不表示像 C 语言中的尾部零;"foo" 表示三个字节,而不是四个。与整数字面量一样,它们的类型可能会有所不同,但它们可以隐式转换为 bytes1、…、bytes32(如果适合),转换为 bytesstring

例如,对于 bytes32 samevar = "stringliteral",字符串字面量在分配给 bytes32 类型时会以原始字节形式解释。

字符串字面量只能包含可打印的 ASCII 字符,这意味着 0x20 到 0x7E 之间(包括)的字符。

此外,字符串字面量还支持以下转义字符

  • \<newline>(转义实际换行符)

  • \\(反斜杠)

  • \'(单引号)

  • \"(双引号)

  • \n(换行符)

  • \r(回车符)

  • \t(制表符)

  • \xNN(十六进制转义,见下文)

  • \uNNNN(Unicode 转义,见下文)

\xNN 接受一个十六进制值并插入相应的字节,而 \uNNNN 接受一个 Unicode 代码点并插入 UTF-8 序列。

注意

在 0.8.0 版本之前,还有三个额外的转义序列:\b\f\v。它们在其他语言中很常见,但在实践中很少需要。如果您确实需要它们,仍然可以通过十六进制转义插入它们,即分别使用 \x08\x0c\x0b,就像任何其他 ASCII 字符一样。

以下示例中的字符串长度为十个字节。它以一个换行符字节开头,后面跟着一个双引号、一个单引号、一个反斜杠字符,然后(没有分隔符)是字符序列 abcdef

"\n\"\'\\abc\
def"

任何不是换行符(即 LF、VF、FF、CR、NEL、LS、PS)的 Unicode 行终止符都被视为终止字符串字面量。如果换行符没有以 \ 为前缀,则只有换行符才会终止字符串字面量。

Unicode 字面量

虽然普通字符串字面量只能包含 ASCII,但 Unicode 字面量(以关键字 unicode 为前缀)可以包含任何有效的 UTF-8 序列。它们还支持与普通字符串字面量完全相同的转义序列。

string memory a = unicode"Hello 😃";

十六进制字面量

十六进制字面量以关键字 hex 为前缀,并用双引号或单引号 (hex"001122FF"hex'0011_22_FF') 括起来。它们的内容必须是十六进制数字,可以选择使用单个下划线作为字节边界之间的分隔符。字面量的值将是十六进制序列的二进制表示。

用空格分隔的多个十六进制字面量会连接成一个字面量:hex"00112233" hex"44556677" 等效于 hex"0011223344556677"

十六进制字面量在某些方面与字符串字面量类似,但不能隐式转换为string类型。

枚举

枚举是 Solidity 中创建用户定义类型的一种方式。它们可以显式地转换为所有整数类型,反之亦然,但不允许隐式转换。从整数进行的显式转换会在运行时检查该值是否在枚举范围之内,如果不在范围之内,则会导致Panic 错误。枚举至少需要一个成员,且声明时其默认值是第一个成员。枚举不能超过 256 个成员。

数据表示与 C 中的枚举相同:选项由从0开始的后续无符号整数值表示。

使用type(NameOfEnum).mintype(NameOfEnum).max可以获取给定枚举的最小值和最大值。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.8;

contract test {
    enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill }
    ActionChoices choice;
    ActionChoices constant defaultChoice = ActionChoices.GoStraight;

    function setGoStraight() public {
        choice = ActionChoices.GoStraight;
    }

    // Since enum types are not part of the ABI, the signature of "getChoice"
    // will automatically be changed to "getChoice() returns (uint8)"
    // for all matters external to Solidity.
    function getChoice() public view returns (ActionChoices) {
        return choice;
    }

    function getDefaultChoice() public pure returns (uint) {
        return uint(defaultChoice);
    }

    function getLargestValue() public pure returns (ActionChoices) {
        return type(ActionChoices).max;
    }

    function getSmallestValue() public pure returns (ActionChoices) {
        return type(ActionChoices).min;
    }
}

注意

枚举也可以在文件级别定义,位于合约或库定义之外。

用户定义的值类型

用户定义的值类型允许在基本值类型上创建零成本抽象。这类似于别名,但类型要求更严格。

用户定义的值类型使用type C is V定义,其中C是新引入类型的名称,V必须是内置值类型(“底层类型”)。函数C.wrap用于从底层类型转换为自定义类型。类似地,函数C.unwrap用于从自定义类型转换为底层类型。

类型C没有任何运算符或附加的成员函数。特别是,即使运算符==也未定义。不允许显式或隐式地转换为其他类型或从其他类型转换。

此类类型的值的数据表示继承自底层类型,底层类型也用于 ABI。

以下示例说明了一个自定义类型UFixed256x18,它表示具有 18 位小数的十进制定点数类型,以及一个用于对该类型执行算术运算的最小库。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.8;

// Represent a 18 decimal, 256 bit wide fixed point type using a user-defined value type.
type UFixed256x18 is uint256;

/// A minimal library to do fixed point operations on UFixed256x18.
library FixedMath {
    uint constant multiplier = 10**18;

    /// Adds two UFixed256x18 numbers. Reverts on overflow, relying on checked
    /// arithmetic on uint256.
    function add(UFixed256x18 a, UFixed256x18 b) internal pure returns (UFixed256x18) {
        return UFixed256x18.wrap(UFixed256x18.unwrap(a) + UFixed256x18.unwrap(b));
    }
    /// Multiplies UFixed256x18 and uint256. Reverts on overflow, relying on checked
    /// arithmetic on uint256.
    function mul(UFixed256x18 a, uint256 b) internal pure returns (UFixed256x18) {
        return UFixed256x18.wrap(UFixed256x18.unwrap(a) * b);
    }
    /// Take the floor of a UFixed256x18 number.
    /// @return the largest integer that does not exceed `a`.
    function floor(UFixed256x18 a) internal pure returns (uint256) {
        return UFixed256x18.unwrap(a) / multiplier;
    }
    /// Turns a uint256 into a UFixed256x18 of the same value.
    /// Reverts if the integer is too large.
    function toUFixed256x18(uint256 a) internal pure returns (UFixed256x18) {
        return UFixed256x18.wrap(a * multiplier);
    }
}

请注意UFixed256x18.wrapFixedMath.toUFixed256x18具有相同的签名,但执行两种截然不同的操作:UFixed256x18.wrap函数返回一个UFixed256x18,它与输入具有相同的数据表示,而toUFixed256x18返回一个UFixed256x18,它具有相同的数值。

函数类型

函数类型是函数的类型。函数类型的变量可以从函数赋值,函数类型的函数参数可用于将函数传递给函数调用,并从函数调用中返回函数。函数类型有两种:内部函数和外部函数

内部函数只能在当前合约中调用(更准确地说,是在当前代码单元中调用,其中还包括内部库函数和继承的函数),因为它们无法在当前合约的上下文之外执行。调用内部函数是通过跳转到其入口标签实现的,就像在当前合约内部调用函数一样。

外部函数由地址和函数签名组成,它们可以通过外部函数调用传递并从外部函数调用返回。

函数类型表示如下

function (<parameter types>) {internal|external} [pure|view|payable] [returns (<return types>)]

与参数类型相反,返回值类型不能为空 - 如果函数类型不应返回任何内容,则必须省略整个returns (<return types>)部分。

默认情况下,函数类型是内部的,因此可以省略internal关键字。请注意,这仅适用于函数类型。在合约中定义的函数必须显式指定可见性,它们没有默认值。

转换

如果且仅当它们的 parameter 类型相同、它们的 return 类型相同、它们的 internal/external 属性相同,并且 A 的状态可变性比 B 的状态可变性更严格,则函数类型 A 可以隐式转换为函数类型 B。特别地

  • pure 函数可以转换为 viewnon-payable 函数

  • view 函数可以转换为 non-payable 函数

  • payable 函数可以转换为 non-payable 函数

函数类型之间没有其他转换。

关于 payablenon-payable 的规则可能有点令人困惑,但实质上,如果一个函数是 payable,这意味着它也接受零 Ether 的支付,因此它也是 non-payable。另一方面,non-payable 函数将拒绝发送给它的 Ether,因此 non-payable 函数不能转换为 payable 函数。为了说明,拒绝以太币比不拒绝以太币更严格。这意味着您可以用 non-payable 函数覆盖 payable 函数,反之则不行。

此外,当您定义 non-payable 函数指针时,编译器不会强制要求所指向的函数实际拒绝以太币。相反,它会强制要求函数指针永远不会用于发送以太币。这使得将 payable 函数指针分配给 non-payable 函数指针成为可能,从而确保两种类型都以相同的方式行为,即它们都无法用于发送以太币。

如果函数类型变量未初始化,则调用它会导致Panic 错误。如果您在对其使用delete之后调用函数,也会发生这种情况。

如果在 Solidity 上下文之外使用外部函数类型,则它们将被视为function类型,该类型将地址和函数标识符一起编码到单个bytes24类型中。

请注意,当前合约的公共函数既可以用作内部函数,也可以用作外部函数。要将 f 用作内部函数,只需使用 f,如果要使用其外部形式,请使用 this.f

无论内部类型函数在何处定义,它都可以分配给内部函数类型变量。这包括合约和库的私有、内部和公共函数,以及自由函数。另一方面,外部函数类型仅与公共和外部合约函数兼容。

注意

具有 calldata 参数的外部函数与具有 calldata 参数的外部函数类型不兼容。它们与具有 memory 参数的相应类型兼容。例如,没有函数可以被类型为 function (string calldata) external 的值指向,而 function (string memory) external 可以指向 function f(string memory) external {}function g(string calldata) external {}。这是因为对于这两个位置,参数都以相同的方式传递给函数。调用者不能直接将其 calldata 传递给外部函数,而是始终将参数 ABI 编码到内存中。将参数标记为 calldata 仅影响外部函数的实现,并且在调用者端的函数指针中毫无意义。

警告

在启用了优化的旧版管道中,内部函数指针的比较可能会有意想不到的结果,因为它可以将相同的函数折叠为一个,然后导致所述函数指针比较为相等,而不是不相等。不建议进行此类比较,并且将在下一个重大版本(0.9.0)之前导致编译器发出警告,此时该警告将升级为错误,从而禁止此类比较。

库被排除在外,因为它们需要 delegatecall 并使用不同的 ABI 约定来表示其选择器。接口中声明的函数没有定义,因此指向它们也没有意义。

成员

外部(或公共)函数具有以下成员

注意

外部(或公共)函数过去还具有附加的成员 .gas(uint).value(uint)。这些成员在 Solidity 0.6.2 中已弃用,并在 Solidity 0.7.0 中删除。请改用 {gas: ...}{value: ...} 分别指定发送给函数的气体量或 wei 量。有关更多信息,请参阅外部函数调用

展示如何使用成员的示例

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.4 <0.9.0;

contract Example {
    function f() public payable returns (bytes4) {
        assert(this.f.address == address(this));
        return this.f.selector;
    }

    function g() public {
        this.f{gas: 10, value: 800}();
    }
}

展示如何使用内部函数类型的示例

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

library ArrayUtils {
    // internal functions can be used in internal library functions because
    // they will be part of the same code context
    function map(uint[] memory self, function (uint) pure returns (uint) f)
        internal
        pure
        returns (uint[] memory r)
    {
        r = new uint[](self.length);
        for (uint i = 0; i < self.length; i++) {
            r[i] = f(self[i]);
        }
    }

    function reduce(
        uint[] memory self,
        function (uint, uint) pure returns (uint) f
    )
        internal
        pure
        returns (uint r)
    {
        r = self[0];
        for (uint i = 1; i < self.length; i++) {
            r = f(r, self[i]);
        }
    }

    function range(uint length) internal pure returns (uint[] memory r) {
        r = new uint[](length);
        for (uint i = 0; i < r.length; i++) {
            r[i] = i;
        }
    }
}


contract Pyramid {
    using ArrayUtils for *;

    function pyramid(uint l) public pure returns (uint) {
        return ArrayUtils.range(l).map(square).reduce(sum);
    }

    function square(uint x) internal pure returns (uint) {
        return x * x;
    }

    function sum(uint x, uint y) internal pure returns (uint) {
        return x + y;
    }
}

另一个使用外部函数类型的示例

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;


contract Oracle {
    struct Request {
        bytes data;
        function(uint) external callback;
    }

    Request[] private requests;
    event NewRequest(uint);

    function query(bytes memory data, function(uint) external callback) public {
        requests.push(Request(data, callback));
        emit NewRequest(requests.length - 1);
    }

    function reply(uint requestID, uint response) public {
        // Here goes the check that the reply comes from a trusted source
        requests[requestID].callback(response);
    }
}


contract OracleUser {
    Oracle constant private ORACLE_CONST = Oracle(address(0x00000000219ab540356cBB839Cbe05303d7705Fa)); // known contract
    uint private exchangeRate;

    function buySomething() public {
        ORACLE_CONST.query("USD", this.oracleResponse);
    }

    function oracleResponse(uint response) public {
        require(
            msg.sender == address(ORACLE_CONST),
            "Only oracle can call this."
        );
        exchangeRate = response;
    }
}

注意

计划使用 Lambda 或内联函数,但目前尚不支持。

引用类型

引用类型的值可以通过多个不同的名称进行修改。与值类型形成对比,在使用值类型变量时,会得到一个独立的副本。因此,引用类型必须比值类型更谨慎地处理。目前,引用类型包括结构体、数组和映射。如果使用引用类型,则必须始终明确提供存储类型的内存区域:memory(其生命周期限制为外部函数调用)、storage(存储状态变量的位置,其生命周期限制为合约的生命周期)或calldata(包含函数参数的特殊数据位置)。

更改数据位置的赋值或类型转换始终会导致自动复制操作,而相同数据位置内的赋值仅在某些情况下才会为存储类型复制。

数据位置

每个引用类型都有一个额外的注释“数据位置”,说明它存储在哪里。有三个数据位置:memorystoragecalldata。Calldata 是一个不可修改的、非持久化的区域,用于存储函数参数,并且行为类似于 memory。

注意

如果可以,尝试使用 calldata 作为数据位置,因为它将避免复制,并且确保数据无法修改。具有 calldata 数据位置的数组和结构体也可以从函数返回,但无法分配此类类型。

注意

在 0.6.9 版本之前,引用类型参数的数据位置限制为外部函数中的 calldata、公共函数中的 memory 以及内部和私有函数中的 memorystorage。现在,memorycalldata 在所有函数中都允许使用,无论其可见性如何。

注意

在 0.5.0 版本之前,数据位置可以省略,并且会根据变量类型、函数类型等默认到不同的位置,但现在所有复杂类型都必须指定显式数据位置。

数据位置和赋值行为

数据位置不仅与数据的持久性相关,还与赋值语义相关。

  • storagememory(或从 calldata)之间的赋值始终会创建独立的副本。

  • memorymemory 的赋值仅创建引用。这意味着对一个 memory 变量的更改也会反映在所有其他引用相同数据的 memory 变量中。

  • storage局部存储变量的赋值也仅分配引用。

  • 所有其他对 storage 的赋值始终会复制。此情况的示例是对状态变量或局部存储结构体类型变量的成员进行赋值,即使局部变量本身只是一个引用。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;

contract C {
    // The data location of x is storage.
    // This is the only place where the
    // data location can be omitted.
    uint[] x;

    // The data location of memoryArray is memory.
    function f(uint[] memory memoryArray) public {
        x = memoryArray; // works, copies the whole array to storage
        uint[] storage y = x; // works, assigns a pointer, data location of y is storage
        y[7]; // fine, returns the 8th element
        y.pop(); // fine, modifies x through y
        delete x; // fine, clears the array, also modifies y
        // The following does not work; it would need to create a new temporary /
        // unnamed array in storage, but storage is "statically" allocated:
        // y = memoryArray;
        // Similarly, "delete y" is not valid, as assignments to local variables
        // referencing storage objects can only be made from existing storage objects.
        // It would "reset" the pointer, but there is no sensible location it could point to.
        // For more details see the documentation of the "delete" operator.
        // delete y;
        g(x); // calls g, handing over a reference to x
        h(x); // calls h and creates an independent, temporary copy in memory
    }

    function g(uint[] storage) internal pure {}
    function h(uint[] memory) public pure {}
}

数组

数组可以具有编译时固定大小,也可以具有动态大小。

大小为 k、元素类型为 T 的数组的类型写为 T[k],动态大小的数组写为 T[]

例如,5 个 uint 动态数组的数组写为 uint[][5]。与其他一些语言相比,表示法是反转的。在 Solidity 中,X[3] 始终是包含三个类型为 X 的元素的数组,即使 X 本身是一个数组。这与 C 等其他语言不同。

索引从零开始,访问方向与声明方向相反。

例如,如果有一个变量 uint[][5] memory x,则使用 x[2][6] 访问第三个动态数组中的第七个 uint,并使用 x[2] 访问第三个动态数组。同样,如果对于可以是数组的类型 T,有一个数组 T[5] a,那么 a[2] 始终具有类型 T

数组元素可以是任何类型,包括映射或结构体。类型的通用限制适用,映射只能存储在 storage 数据位置,并且公开可见的函数需要 ABI 类型 的参数。

可以将状态变量数组标记为 public,并让 Solidity 创建一个 getter。数字索引将成为 getter 的必需参数。

访问数组末尾后的元素会导致断言失败。方法 .push().push(value) 可用于在动态大小数组的末尾追加新元素,其中 .push() 追加一个零初始化的元素并返回对它的引用。

注意

动态大小数组只能在存储中调整大小。在内存中,此类数组可以具有任意大小,但一旦分配数组,大小就不能更改。

bytesstring 作为数组

类型为 bytesstring 的变量是特殊的数组。bytes 类型类似于 bytes1[],但在 calldata 和 memory 中紧凑地打包。string 等于 bytes,但不允许长度或索引访问。

Solidity 没有字符串操作函数,但存在第三方字符串库。还可以使用 keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2)) 通过其 keccak256 哈希比较两个字符串,并使用 string.concat(s1, s2) 连接两个字符串。

应该使用 bytes 而不是 bytes1[],因为它更便宜,因为在 memory 中使用 bytes1[] 会在元素之间添加 31 个填充字节。请注意,在 storage 中,由于紧凑打包,填充不存在,请参阅 bytes and string。一般来说,对于任意长度的原始字节数据,使用 bytes,对于任意长度的字符串(UTF-8)数据,使用 string。如果可以将长度限制为特定数量的字节,则始终使用其中一个值类型 bytes1bytes32,因为它们便宜得多。

注意

如果要访问字符串 s 的字节表示形式,请使用 bytes(s).length / bytes(s)[7] = 'x';。请记住,您正在访问 UTF-8 表示形式的低级字节,而不是单个字符。

函数 bytes.concatstring.concat

可以使用 string.concat 连接任意数量的 string 值。该函数返回一个包含参数内容的单个 string memory 数组,不带填充。如果要使用无法隐式转换为 string 的其他类型参数,则需要先将其转换为 string

类似地,bytes.concat 函数可以连接任意数量的 bytesbytes1 ... bytes32 值。该函数返回一个包含参数内容的单个 bytes memory 数组,不带填充。如果要使用字符串参数或无法隐式转换为 bytes 的其他类型,则需要先将其转换为 bytesbytes1/…/bytes32

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.12;

contract C {
    string s = "Storage";
    function f(bytes calldata bc, string memory sm, bytes16 b) public view {
        string memory concatString = string.concat(s, string(bc), "Literal", sm);
        assert((bytes(s).length + bc.length + 7 + bytes(sm).length) == bytes(concatString).length);

        bytes memory concatBytes = bytes.concat(bytes(s), bc, bc[:2], "Literal", bytes(sm), b);
        assert((bytes(s).length + bc.length + 2 + 7 + bytes(sm).length + b.length) == concatBytes.length);
    }
}

如果在没有参数的情况下调用 string.concatbytes.concat,则它们将返回一个空数组。

分配内存数组

可以使用 new 运算符创建具有动态长度的内存数组。与存储数组不同,无法调整内存数组的大小(例如,.push 成员函数不可用)。您必须提前计算所需大小,或者创建一个新的内存数组并复制每个元素。

与 Solidity 中的所有变量一样,新分配数组的元素始终使用 默认值 初始化。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract C {
    function f(uint len) public pure {
        uint[] memory a = new uint[](7);
        bytes memory b = new bytes(len);
        assert(a.length == 7);
        assert(b.length == len);
        a[6] = 8;
    }
}

数组字面量

数组字面量是一个由逗号分隔的一个或多个表达式组成的列表,用方括号 ([...]) 括起来。例如 [1, a, f(3)]。数组字面量的类型如下确定

它始终是一个静态大小的内存数组,其长度为表达式的数量。

数组的基本类型是列表中第一个表达式的类型,这样所有其他表达式都可以隐式转换为它。如果这不可能,则为类型错误。

仅仅存在所有元素都可以转换到的类型是不够的。其中一个元素必须是该类型。

在下面的示例中,[1, 2, 3] 的类型为 uint8[3] memory,因为这些常量的类型都是 uint8。如果您希望结果为 uint[3] memory 类型,则需要将第一个元素转换为 uint

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract C {
    function f() public pure {
        g([uint(1), 2, 3]);
    }
    function g(uint[3] memory) public pure {
        // ...
    }
}

数组文字 [1, -1] 无效,因为第一个表达式的类型为 uint8,而第二个表达式的类型为 int8,它们不能相互隐式转换。要使其正常工作,您可以使用 [int8(1), -1],例如。

由于不同类型的固定大小内存数组不能相互转换(即使基本类型可以),因此如果要使用二维数组文字,您始终必须显式指定一个公共基本类型

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract C {
    function f() public pure returns (uint24[2][4] memory) {
        uint24[2][4] memory x = [[uint24(0x1), 1], [0xffffff, 2], [uint24(0xff), 3], [uint24(0xffff), 4]];
        // The following does not work, because some of the inner arrays are not of the right type.
        // uint[2][4] memory x = [[0x1, 1], [0xffffff, 2], [0xff, 3], [0xffff, 4]];
        return x;
    }
}

固定大小内存数组不能分配给动态大小内存数组,即以下操作不可行

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

// This will not compile.
contract C {
    function f() public {
        // The next line creates a type error because uint[3] memory
        // cannot be converted to uint[] memory.
        uint[] memory x = [uint(1), 3, 4];
    }
}

计划将来取消此限制,但由于数组在 ABI 中的传递方式,它会带来一些复杂性。

如果要初始化动态大小的数组,则必须分配各个元素

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract C {
    function f() public pure {
        uint[] memory x = new uint[](3);
        x[0] = 1;
        x[1] = 3;
        x[2] = 4;
    }
}

数组成员

长度:

数组有一个 length 成员,它包含它们的元素数量。内存数组的长度在创建后是固定的(但动态的,即它可以取决于运行时参数)。

push():

动态存储数组和 bytes(不是 string)有一个名为 push() 的成员函数,您可以使用它在数组末尾追加一个零初始化的元素。它返回对元素的引用,以便它可以像 x.push().t = 2x.push() = b 那样使用。

push(x):

动态存储数组和 bytes(不是 string)有一个名为 push(x) 的成员函数,您可以使用它在数组末尾追加一个给定元素。该函数不返回任何内容。

pop():

动态存储数组和 bytes(不是 string)有一个名为 pop() 的成员函数,您可以使用它从数组末尾删除一个元素。这也隐式调用 delete 删除的元素。该函数不返回任何内容。

注意

通过调用 push() 增加存储数组的长度具有恒定的 gas 成本,因为存储是零初始化的,而通过调用 pop() 减少长度的成本取决于被删除元素的“大小”。如果该元素是一个数组,那么它可能非常昂贵,因为它包括显式清除被删除的元素,类似于在它们上调用 delete

注意

要在外部(而不是公共)函数中使用数组数组,您需要激活 ABI 编码器 v2。

注意

在拜占庭之前的 EVM 版本中,无法访问从函数调用返回的动态数组。如果您调用返回动态数组的函数,请确保使用设置为拜占庭模式的 EVM。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

contract ArrayContract {
    uint[2**20] aLotOfIntegers;
    // Note that the following is not a pair of dynamic arrays but a
    // dynamic array of pairs (i.e. of fixed size arrays of length two).
    // In Solidity, T[k] and T[] are always arrays with elements of type T,
    // even if T itself is an array.
    // Because of that, bool[2][] is a dynamic array of elements
    // that are bool[2]. This is different from other languages, like C.
    // Data location for all state variables is storage.
    bool[2][] pairsOfFlags;

    // newPairs is stored in memory - the only possibility
    // for public contract function arguments
    function setAllFlagPairs(bool[2][] memory newPairs) public {
        // assignment to a storage array performs a copy of ``newPairs`` and
        // replaces the complete array ``pairsOfFlags``.
        pairsOfFlags = newPairs;
    }

    struct StructType {
        uint[] contents;
        uint moreInfo;
    }
    StructType s;

    function f(uint[] memory c) public {
        // stores a reference to ``s`` in ``g``
        StructType storage g = s;
        // also changes ``s.moreInfo``.
        g.moreInfo = 2;
        // assigns a copy because ``g.contents``
        // is not a local variable, but a member of
        // a local variable.
        g.contents = c;
    }

    function setFlagPair(uint index, bool flagA, bool flagB) public {
        // access to a non-existing index will throw an exception
        pairsOfFlags[index][0] = flagA;
        pairsOfFlags[index][1] = flagB;
    }

    function changeFlagArraySize(uint newSize) public {
        // using push and pop is the only way to change the
        // length of an array
        if (newSize < pairsOfFlags.length) {
            while (pairsOfFlags.length > newSize)
                pairsOfFlags.pop();
        } else if (newSize > pairsOfFlags.length) {
            while (pairsOfFlags.length < newSize)
                pairsOfFlags.push();
        }
    }

    function clear() public {
        // these clear the arrays completely
        delete pairsOfFlags;
        delete aLotOfIntegers;
        // identical effect here
        pairsOfFlags = new bool[2][](0);
    }

    bytes byteData;

    function byteArrays(bytes memory data) public {
        // byte arrays ("bytes") are different as they are stored without padding,
        // but can be treated identical to "uint8[]"
        byteData = data;
        for (uint i = 0; i < 7; i++)
            byteData.push();
        byteData[3] = 0x08;
        delete byteData[2];
    }

    function addFlag(bool[2] memory flag) public returns (uint) {
        pairsOfFlags.push(flag);
        return pairsOfFlags.length;
    }

    function createMemoryArray(uint size) public pure returns (bytes memory) {
        // Dynamic memory arrays are created using `new`:
        uint[2][] memory arrayOfPairs = new uint[2][](size);

        // Inline arrays are always statically-sized and if you only
        // use literals, you have to provide at least one type.
        arrayOfPairs[0] = [uint(1), 2];

        // Create a dynamic byte array:
        bytes memory b = new bytes(200);
        for (uint i = 0; i < b.length; i++)
            b[i] = bytes1(uint8(i));
        return b;
    }
}

对存储数组元素的悬空引用

在使用存储数组时,您需要注意避免悬空引用。悬空引用是指指向不再存在或已移动但未更新引用的内容的引用。例如,如果您在局部变量中存储对数组元素的引用,然后从包含数组中 .pop(),则可能会出现悬空引用

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;

contract C {
    uint[][] s;

    function f() public {
        // Stores a pointer to the last array element of s.
        uint[] storage ptr = s[s.length - 1];
        // Removes the last array element of s.
        s.pop();
        // Writes to the array element that is no longer within the array.
        ptr.push(0x42);
        // Adding a new element to ``s`` now will not add an empty array, but
        // will result in an array of length 1 with ``0x42`` as element.
        s.push();
        assert(s[s.length - 1][0] == 0x42);
    }
}

ptr.push(0x42) 中的写入将不会恢复,尽管事实上 ptr 不再引用 s 的有效元素。由于编译器假设未使用的存储始终为零,因此随后的 s.push() 不会显式将零写入存储,因此 push() 之后 s 的最后一个元素将具有长度 1 并且包含 0x42 作为它的第一个元素。

请注意,Solidity 不允许在存储中声明对值类型的引用。这些类型的显式悬空引用仅限于嵌套的引用类型。但是,在使用元组分配中的复杂表达式时,悬空引用也可能暂时出现

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;

contract C {
    uint[] s;
    uint[] t;
    constructor() {
        // Push some initial values to the storage arrays.
        s.push(0x07);
        t.push(0x03);
    }

    function g() internal returns (uint[] storage) {
        s.pop();
        return t;
    }

    function f() public returns (uint[] memory) {
        // The following will first evaluate ``s.push()`` to a reference to a new element
        // at index 1. Afterwards, the call to ``g`` pops this new element, resulting in
        // the left-most tuple element to become a dangling reference. The assignment still
        // takes place and will write outside the data area of ``s``.
        (s.push(), g()[0]) = (0x42, 0x17);
        // A subsequent push to ``s`` will reveal the value written by the previous
        // statement, i.e. the last element of ``s`` at the end of this function will have
        // the value ``0x42``.
        s.push();
        return s;
    }
}

始终更安全的是每个语句只分配一次存储,并避免在赋值左侧使用复杂表达式。

在处理对 bytes 数组元素的引用时,您需要格外小心,因为在字节数组上 .push() 可能会在存储中 从短布局切换到长布局

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;

// This will report a warning
contract C {
    bytes x = "012345678901234567890123456789";

    function test() external returns(uint) {
        (x.push(), x.push()) = (0x01, 0x02);
        return x.length;
    }
}

这里,当评估第一个 x.push() 时,x 仍然以短布局存储,因此 x.push() 返回对 x 的第一个存储槽中的元素的引用。但是,第二个 x.push() 将字节数组切换到大型布局。现在,x.push() 所引用的元素位于数组的数据区域中,而引用仍然指向其原始位置,该位置现在是长度字段的一部分,并且赋值将有效地使 x 的长度混乱。为了安全起见,在单个赋值期间最多只将字节数组扩大一个元素,并且不要在同一个语句中同时索引访问数组。

虽然以上描述了编译器当前版本中悬空存储引用的行为,但任何包含悬空引用的代码都应视为具有未定义行为。特别是,这意味着编译器的任何未来版本都可能改变涉及悬空引用的代码的行为。

确保在代码中避免悬空引用!

数组切片

数组切片是对数组连续部分的视图。它们写成 x[start:end],其中 startend 是表达式,它们产生 uint256 类型(或可以隐式转换为它)。切片的第一个元素是 x[start],最后一个元素是 x[end - 1]

如果 start 大于 end,或者如果 end 大于数组的长度,则会抛出异常。

Both start and end are optional: start defaults to 0 and end defaults to the length of the array.

数组切片没有任何成员。它们可以隐式转换为其底层类型的数组,并支持索引访问。索引访问不是绝对的,而是相对于切片开始的。

数组切片没有类型名称,这意味着没有变量可以具有数组切片作为类型,它们只存在于中间表达式中。

注意

截至目前,数组切片仅针对 calldata 数组实现。

数组切片对于在函数参数中传递的 ABI 解码辅助数据很有用

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.5 <0.9.0;
contract Proxy {
    /// @dev Address of the client contract managed by proxy i.e., this contract
    address client;

    constructor(address client_) {
        client = client_;
    }

    /// Forward call to "setOwner(address)" that is implemented by client
    /// after doing basic validation on the address argument.
    function forward(bytes calldata payload) external {
        bytes4 sig = bytes4(payload[:4]);
        // Due to truncating behavior, bytes4(payload) performs identically.
        // bytes4 sig = bytes4(payload);
        if (sig == bytes4(keccak256("setOwner(address)"))) {
            address owner = abi.decode(payload[4:], (address));
            require(owner != address(0), "Address of owner cannot be zero.");
        }
        (bool status,) = client.delegatecall(payload);
        require(status, "Forwarded call failed.");
    }
}

结构体

Solidity 提供了一种以结构体形式定义新类型的方法,如下面的示例所示

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

// Defines a new type with two fields.
// Declaring a struct outside of a contract allows
// it to be shared by multiple contracts.
// Here, this is not really needed.
struct Funder {
    address addr;
    uint amount;
}

contract CrowdFunding {
    // Structs can also be defined inside contracts, which makes them
    // visible only there and in derived contracts.
    struct Campaign {
        address payable beneficiary;
        uint fundingGoal;
        uint numFunders;
        uint amount;
        mapping(uint => Funder) funders;
    }

    uint numCampaigns;
    mapping(uint => Campaign) campaigns;

    function newCampaign(address payable beneficiary, uint goal) public returns (uint campaignID) {
        campaignID = numCampaigns++; // campaignID is return variable
        // We cannot use "campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0)"
        // because the right hand side creates a memory-struct "Campaign" that contains a mapping.
        Campaign storage c = campaigns[campaignID];
        c.beneficiary = beneficiary;
        c.fundingGoal = goal;
    }

    function contribute(uint campaignID) public payable {
        Campaign storage c = campaigns[campaignID];
        // Creates a new temporary memory struct, initialised with the given values
        // and copies it over to storage.
        // Note that you can also use Funder(msg.sender, msg.value) to initialise.
        c.funders[c.numFunders++] = Funder({addr: msg.sender, amount: msg.value});
        c.amount += msg.value;
    }

    function checkGoalReached(uint campaignID) public returns (bool reached) {
        Campaign storage c = campaigns[campaignID];
        if (c.amount < c.fundingGoal)
            return false;
        uint amount = c.amount;
        c.amount = 0;
        c.beneficiary.transfer(amount);
        return true;
    }
}

该合约没有提供众筹合约的全部功能,但它包含了解结构体的基本概念。结构体类型可以在映射和数组中使用,它们本身可以包含映射和数组。

结构体不能包含其自身类型的成员,尽管结构体本身可以是映射成员的值类型,或者它可以包含其类型的一个动态大小的数组。此限制是必要的,因为结构体的大小必须是有限的。

请注意,在所有函数中,结构体类型如何分配给具有数据位置 storage 的局部变量。这不会复制结构体,而只是存储一个引用,以便对局部变量成员的赋值实际上写入状态。

当然,您也可以直接访问结构体成员,而无需将其分配给局部变量,例如在 campaigns[campaignID].amount = 0 中。

注意

在 Solidity 0.7.0 之前,允许包含存储专用类型(例如映射)成员的内存结构体(例如映射),并且像上面的示例中的 campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0) 这样的赋值将起作用并只是静默地跳过这些成员。

映射类型

映射类型使用语法 mapping(KeyType KeyName? => ValueType ValueName?), 映射类型变量的声明语法为 mapping(KeyType KeyName? => ValueType ValueName?) VariableNameKeyType 可以是任何内置值类型,bytesstring,或任何合约或枚举类型。 其他用户定义或复杂类型,例如映射,结构体或数组类型是不允许的。 ValueType 可以是任何类型,包括映射,数组和结构体。 KeyNameValueName 是可选的(所以 mapping(KeyType => ValueType) 也适用)并且可以是任何有效的标识符,但不能是类型。

可以将映射视为 哈希表,这些哈希表被虚拟地初始化,以使每个可能的键都存在,并且被映射到一个值为字节表示为全零的值,即类型的 默认值。 它们的相似性到此为止,键数据不会存储在映射中,只有它的 keccak256 哈希被用于查找值。

因此,映射没有长度或键或值被设置的概念,因此在没有有关已分配键的额外信息的情况下无法被删除(请参阅 清除映射)。

映射只能具有 storage 的数据位置,因此允许用于状态变量,作为函数中的存储引用类型,或作为库函数的参数。 它们不能用作公开可见的合约函数的参数或返回值参数。 这些限制也适用于包含映射的数组和结构体。

可以将映射类型的状态变量标记为 public, Solidity 会为你创建一个 getterKeyType 变为 getter 的参数,名称为 KeyName(如果指定)。 如果 ValueType 是值类型或结构体,则 getter 返回 ValueType,名称为 ValueName(如果指定)。 如果 ValueType 是数组或映射,则 getter 对每个 KeyType 都有一个参数,递归地。

在下面的示例中,MappingExample 合约定义了一个公开的 balances 映射,键类型为 address,值类型为 uint,将以太坊地址映射到无符号整数。 由于 uint 是值类型,因此 getter 返回一个与类型匹配的值,你可以在 MappingUser 合约中看到,该合约返回指定地址的值。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

contract MappingExample {
    mapping(address => uint) public balances;

    function update(uint newBalance) public {
        balances[msg.sender] = newBalance;
    }
}

contract MappingUser {
    function f() public returns (uint) {
        MappingExample m = new MappingExample();
        m.update(100);
        return m.balances(address(this));
    }
}

下面的示例是 ERC20 代币 的简化版本。 _allowances 是另一个映射类型内的映射类型示例。

在下面的示例中,为映射提供了可选的 KeyNameValueName。 它不影响任何合约功能或字节码,它只是为映射 getter 的 ABI 中的输入和输出设置 name 字段。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.18;

contract MappingExampleWithNames {
    mapping(address user => uint balance) public balances;

    function update(uint newBalance) public {
        balances[msg.sender] = newBalance;
    }
}

下面的示例使用 _allowances 来记录其他人从你的帐户中提取的金额。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;

contract MappingExample {

    mapping(address => uint256) private _balances;
    mapping(address => mapping(address => uint256)) private _allowances;

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    function allowance(address owner, address spender) public view returns (uint256) {
        return _allowances[owner][spender];
    }

    function transferFrom(address sender, address recipient, uint256 amount) public returns (bool) {
        require(_allowances[sender][msg.sender] >= amount, "ERC20: Allowance not high enough.");
        _allowances[sender][msg.sender] -= amount;
        _transfer(sender, recipient, amount);
        return true;
    }

    function approve(address spender, uint256 amount) public returns (bool) {
        require(spender != address(0), "ERC20: approve to the zero address");

        _allowances[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    function _transfer(address sender, address recipient, uint256 amount) internal {
        require(sender != address(0), "ERC20: transfer from the zero address");
        require(recipient != address(0), "ERC20: transfer to the zero address");
        require(_balances[sender] >= amount, "ERC20: Not enough funds.");

        _balances[sender] -= amount;
        _balances[recipient] += amount;
        emit Transfer(sender, recipient, amount);
    }
}

可迭代映射

你不能遍历映射,也就是说你不能枚举它们的键。 但是,可以在它们之上实现一个数据结构并遍历它。 例如,下面的代码实现了一个 IterableMapping 库,然后 User 合约向其中添加数据,并且 sum 函数遍历以对所有值求和。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.8;

struct IndexValue { uint keyIndex; uint value; }
struct KeyFlag { uint key; bool deleted; }

struct itmap {
    mapping(uint => IndexValue) data;
    KeyFlag[] keys;
    uint size;
}

type Iterator is uint;

library IterableMapping {
    function insert(itmap storage self, uint key, uint value) internal returns (bool replaced) {
        uint keyIndex = self.data[key].keyIndex;
        self.data[key].value = value;
        if (keyIndex > 0)
            return true;
        else {
            keyIndex = self.keys.length;
            self.keys.push();
            self.data[key].keyIndex = keyIndex + 1;
            self.keys[keyIndex].key = key;
            self.size++;
            return false;
        }
    }

    function remove(itmap storage self, uint key) internal returns (bool success) {
        uint keyIndex = self.data[key].keyIndex;
        if (keyIndex == 0)
            return false;
        delete self.data[key];
        self.keys[keyIndex - 1].deleted = true;
        self.size --;
    }

    function contains(itmap storage self, uint key) internal view returns (bool) {
        return self.data[key].keyIndex > 0;
    }

    function iterateStart(itmap storage self) internal view returns (Iterator) {
        return iteratorSkipDeleted(self, 0);
    }

    function iterateValid(itmap storage self, Iterator iterator) internal view returns (bool) {
        return Iterator.unwrap(iterator) < self.keys.length;
    }

    function iterateNext(itmap storage self, Iterator iterator) internal view returns (Iterator) {
        return iteratorSkipDeleted(self, Iterator.unwrap(iterator) + 1);
    }

    function iterateGet(itmap storage self, Iterator iterator) internal view returns (uint key, uint value) {
        uint keyIndex = Iterator.unwrap(iterator);
        key = self.keys[keyIndex].key;
        value = self.data[key].value;
    }

    function iteratorSkipDeleted(itmap storage self, uint keyIndex) private view returns (Iterator) {
        while (keyIndex < self.keys.length && self.keys[keyIndex].deleted)
            keyIndex++;
        return Iterator.wrap(keyIndex);
    }
}

// How to use it
contract User {
    // Just a struct holding our data.
    itmap data;
    // Apply library functions to the data type.
    using IterableMapping for itmap;

    // Insert something
    function insert(uint k, uint v) public returns (uint size) {
        // This calls IterableMapping.insert(data, k, v)
        data.insert(k, v);
        // We can still access members of the struct,
        // but we should take care not to mess with them.
        return data.size;
    }

    // Computes the sum of all stored data.
    function sum() public view returns (uint s) {
        for (
            Iterator i = data.iterateStart();
            data.iterateValid(i);
            i = data.iterateNext(i)
        ) {
            (, uint value) = data.iterateGet(i);
            s += value;
        }
    }
}

运算符

即使两个操作数没有相同的类型,也可以应用算术运算符和位运算符。 例如,可以计算 y = x + z,其中 x 是一个 uint8 并且 z 的类型为 uint32。 在这些情况下,将使用以下机制来确定执行运算的类型(这在发生溢出时很重要)以及运算符结果的类型

  1. 如果右操作数的类型可以隐式转换为左操作数的类型,则使用左操作数的类型,

  2. 如果左操作数的类型可以隐式转换为右操作数的类型,则使用右操作数的类型,

  3. 否则,运算将不被允许。

如果操作数之一是 字面量数字,则它首先被转换为它的“移动类型”,它是可以容纳该值的最小类型(相同位宽的无符号类型被认为“小于”有符号类型)。 如果两者都是字面量数字,则运算将以无限精度执行,即表达式将以任何必要的精度进行计算,以便在结果与非字面量类型一起使用时不会丢失任何精度。

运算符的结果类型与执行运算的类型相同,除了比较运算符,它们的结果始终为 bool

运算符 **(求幂)、<<>> 使用左操作数的类型进行运算和结果。

三元运算符

三元运算符用于 <expression> ? <trueExpression> : <falseExpression> 形式的表达式。 它根据主 <expression> 评估结果来评估后两个表达式之一。 如果 <expression> 评估为 true,则将评估 <trueExpression>,否则将评估 <falseExpression>

三元运算符的结果没有有理数类型,即使它的所有操作数都是有理数字面量。 结果类型根据两个操作数的类型以与上述相同的方式确定,如果需要,首先转换为它们的移动类型。

因此,255 + (true ? 1 : 0) 将由于算术溢出而恢复。 原因是 (true ? 1 : 0) 的类型为 uint8,这将强制加法也在 uint8 中执行,并且 256 超出了此类型允许的范围。

另一个结果是像 1.5 + 1.5 这样的表达式是有效的,但 1.5 + (true ? 1.5 : 2.5) 不是。 这是因为前者是一个以无限精度计算的有理表达式,只有它的最终值很重要。 后者涉及将分数有理数转换为整数,这在目前是不允许的。

复合运算符和增量/减量运算符

如果 a 是一个 LValue(即一个变量或可以被分配的值),则可以使用以下运算符作为简写

a += e 等效于 a = a + e。 运算符 -=*=/=%=|=&=^=<<=>>= 类似地定义。 a++a-- 等效于 a += 1 / a -= 1,但表达式本身仍然具有 a 的先前值。 相反,--a++aa 的影响相同,但返回更改后的值。

删除

delete a 将该类型的初始值分配给 a。 也就是说,对于整数,它等效于 a = 0,但它也可以用于数组,它将分配一个长度为零的动态数组或一个长度相同的静态数组,其中所有元素都被设置为它们的初始值。 delete a[x] 删除数组中索引为 x 的项,并保持所有其他元素和数组的长度不变。 这尤其意味着它在数组中留下了空隙。 如果你计划删除项目,映射 可能是一个更好的选择。

对于结构体,它将分配一个结构体,并重置所有成员。换句话说,在 delete a 之后 a 的值与未赋值声明 a 的值相同,但存在以下例外情况。

delete 对映射没有影响(因为映射的键可以是任意的,通常是未知的)。因此,如果删除结构体,它将重置所有不是映射的成员,并且也会递归到成员中,除非它们是映射。但是,可以删除单个键及其映射到的值:如果 a 是映射,则 delete a[x] 将删除存储在 x 处的的值。

需要注意的是,delete a 的行为类似于对 a 的赋值,也就是说,它将一个新的对象存储在 a 中。当 a 是引用变量时,这种区别就很明显:它只会重置 a 本身,而不是它先前引用的值。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

contract DeleteExample {
    uint data;
    uint[] dataArray;

    function f() public {
        uint x = data;
        delete x; // sets x to 0, does not affect data
        delete data; // sets data to 0, does not affect x
        uint[] storage y = dataArray;
        delete dataArray; // this sets dataArray.length to zero, but as uint[] is a complex object, also
        // y is affected which is an alias to the storage object
        // On the other hand: "delete y" is not valid, as assignments to local variables
        // referencing storage objects can only be made from existing storage objects.
        assert(y.length == 0);
    }
}

运算符优先级顺序

以下是运算符的优先级顺序,按求值顺序排列。

优先级

描述

运算符

1

后缀自增和自减

++, --

新表达式

new <typename>

数组下标

<array>[<index>]

成员访问

<object>.<member>

函数式调用

<func>(<args...>)

圆括号

(<statement>)

2

前缀自增和自减

++, --

一元负号

-

一元运算符

delete

逻辑非

!

按位非

~

3

指数运算

**

4

乘法、除法和取模

*, /, %

5

加法和减法

+, -

6

按位移位运算符

<<, >>

7

按位与

&

8

按位异或

^

9

按位或

|

10

不等运算符

<, >, <=, >=

11

等式运算符

==, !=

12

逻辑与

&&

13

逻辑或

||

14

三元运算符

<conditional> ? <if-true> : <if-false>

赋值运算符

=, |=, ^=, &=, <<=, >>=, +=, -=, *=, /=, %=

15

逗号运算符

,

基本类型之间的转换

隐式转换

在某些情况下,编译器会在赋值、向函数传递参数以及应用运算符时自动应用隐式类型转换。通常,如果从语义上讲,隐式转换在值类型之间是可能的,并且不会丢失信息。

例如,uint8 可以转换为 uint16,而 int128 可以转换为 int256,但 int8 不能转换为 uint256,因为 uint256 不能容纳诸如 -1 之类的值。

如果对不同类型的运算符应用,编译器将尝试隐式地将其中一个操作数转换为另一个操作数的类型(赋值也是如此)。这意味着运算始终在其中一个操作数的类型中执行。

有关哪些隐式转换是可能的,请参阅有关类型本身的章节。

在下面的示例中,yz 是加法的操作数,它们没有相同的类型,但是 uint8 可以隐式转换为 uint16,反之则不行。因此,在 uint16 类型中执行加法之前,y 会被转换为 z 的类型。表达式 y + z 的结果类型为 uint16。因为它被赋值给一个类型为 uint32 的变量,所以在加法之后会执行另一个隐式转换。

uint8 y;
uint16 z;
uint32 x = y + z;

显式转换

如果编译器不允许隐式转换,但你确信转换会成功,则有时可以使用显式类型转换。这可能会导致意外行为,并允许你绕过编译器的一些安全功能,因此请务必测试结果是否符合你的预期!

以下示例将一个负数 int 转换为 uint

int  y = -3;
uint x = uint(y);

在此代码片段的末尾,x 将具有值 0xfffff..fd(64 个十六进制字符),这是 256 位的二进制补码表示中的 -3。

如果将整数显式转换为更小的类型,则会截断高阶位。

uint32 a = 0x12345678;
uint16 b = uint16(a); // b will be 0x5678 now

如果将整数显式转换为更大的类型,则会在左侧(即高阶端)进行填充。转换的结果将与原始整数进行比较时相等。

uint16 a = 0x1234;
uint32 b = uint32(a); // b will be 0x00001234 now
assert(a == b);

固定大小的字节类型在转换期间的行为有所不同。可以将它们视为单个字节的序列,转换为更小的类型会截断序列。

bytes2 a = 0x1234;
bytes1 b = bytes1(a); // b will be 0x12

如果将固定大小的字节类型显式转换为更大的类型,则会在右侧进行填充。访问固定索引处的字节将导致转换前后具有相同的值(如果索引仍在范围内)。

bytes2 a = 0x1234;
bytes4 b = bytes4(a); // b will be 0x12340000
assert(a[0] == b[0]);
assert(a[1] == b[1]);

由于整数和固定大小的字节数组在截断或填充时的行为不同,因此仅当两者具有相同大小时,才允许在整数和固定大小的字节数组之间进行显式转换。如果你想在大小不同的整数和固定大小的字节数组之间进行转换,则必须使用中间转换,使所需的截断和填充规则明确。

bytes2 a = 0x1234;
uint32 b = uint16(a); // b will be 0x00001234
uint32 c = uint32(bytes4(a)); // c will be 0x12340000
uint8 d = uint8(uint16(a)); // d will be 0x34
uint8 e = uint8(bytes1(a)); // e will be 0x12

bytes 数组和 bytes calldata 片段可以显式转换为固定字节类型 (bytes1/…/bytes32)。如果数组比目标固定字节类型更长,则会在末尾进行截断。如果数组比目标类型短,则会在末尾用零进行填充。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.5;

contract C {
    bytes s = "abcdefgh";
    function f(bytes calldata c, bytes memory m) public view returns (bytes16, bytes3) {
        require(c.length == 16, "");
        bytes16 b = bytes16(m);  // if length of m is greater than 16, truncation will happen
        b = bytes16(s);  // padded on the right, so result is "abcdefgh\0\0\0\0\0\0\0\0"
        bytes3 b1 = bytes3(s); // truncated, b1 equals to "abc"
        b = bytes16(c[:8]);  // also padded with zeros
        return (b, b1);
    }
}

字面值和基本类型之间的转换

整数类型

十进制和十六进制数字字面值可以隐式转换为任何足够大的整数类型,以便在不截断的情况下表示它。

uint8 a = 12; // fine
uint32 b = 1234; // fine
uint16 c = 0x123456; // fails, since it would have to truncate to 0x3456

注意

在 0.8.0 版之前,任何十进制或十六进制数字字面值都可以显式转换为整数类型。从 0.8.0 开始,此类显式转换与隐式转换一样严格,也就是说,只有当字面值适合结果范围时才允许。

固定大小的字节数组

十进制数字字面值不能隐式转换为固定大小的字节数组。十六进制数字字面值可以,但前提是十六进制数字的个数必须与字节类型的尺寸完全匹配。作为例外,十进制和十六进制字面值的值为零可以转换为任何固定大小的字节类型。

bytes2 a = 54321; // not allowed
bytes2 b = 0x12; // not allowed
bytes2 c = 0x123; // not allowed
bytes2 d = 0x1234; // fine
bytes2 e = 0x0012; // fine
bytes4 f = 0; // fine
bytes4 g = 0x0; // fine

字符串字面值和十六进制字符串字面值可以隐式转换为固定大小的字节数组,前提是它们的字符数必须与字节类型的尺寸匹配。

bytes2 a = hex"1234"; // fine
bytes2 b = "xy"; // fine
bytes2 c = hex"12"; // not allowed
bytes2 d = hex"123"; // not allowed
bytes2 e = "x"; // not allowed
bytes2 f = "xyz"; // not allowed

地址

地址字面值 中所述,通过校验和测试的正确尺寸的十六进制字面值为 address 类型。没有其他字面值可以隐式转换为 address 类型。

仅允许从 bytes20uint160 显式转换为 address

一个 address a 可以通过 payable(a) 显式转换为 address payable

注意

在 0.8.0 版之前,可以从任何整数类型(任何大小,有符号或无符号)显式转换为 addressaddress payable。从 0.8.0 开始,只允许从 uint160 进行转换。