类型

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

除此之外,各个类型之间可以在包含运算符号的表达式中进行交互。 关于各种运算符的快速参考,可以参考 运算符的优先顺序

Solidity中不存在"未定义"或"空"值的概念, 但新声明的变量总是有一个取决于其类型的 默认值。 为了处理任何意外的值,您应该使用 revert 函数 来恢复整个事务, 或者返回一个带有第二个 bool 值的元组来表示成功。

值类型

以下被称为值类型,因为它们的变量总是按值传递, 也就是说,当这些变量被用作函数参数或者用在赋值语句中时,总会进行值拷贝。

布尔类型

bool :可能的取值为常数值 truefalse

运算符:

  • ! (逻辑非)

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

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

  • == (等于)

  • != (不等于)

运算符 ||&& 都遵循同样的短路( short-circuiting )规则。 就是说在表达式 f(x) || g(y) 中, 如果 f(x) 的值为 true , 那么 g(y) 就不会被执行,即使会出现一些副作用。

整型

int / uint: 分别表示有符号和无符号的不同位数的整型变量。 关键字 uint8uint256 (无符号整型,从 8 位到 256 位)以及 int8int256, 以 8 位为步长递增。 uintint 分别是 uint256int256 的别名。

运算符:

  • 比较运算符: <=<==!=>=> (返回布尔值)

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

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

  • 算数运算符: +-, 一元运算 - (只适用于有符号的整数), */% (取余), ** (幂)

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

警告

Solidity 中的整数被限制在一个特定的范围内。例如,对于 uint32,这是 02**32 - 1。 有两种模式在这些类型上进行算术。“包装” 或 “不检查” 模式和 “检查” 模式。 默认情况下,算术总是 “检查” 模式的,这意味着如果一个操作的结果超出了该类型的值范围, 调用将通过一个 失败的断言 而被恢复。 您可以用 unchecked { ... } 切换到 “不检查” 模式。更多的细节可以在关于 不检查 的章节中找到。

比较运算

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

位运算

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

移位运算

移位操作的结果具有左操作数的类型,将结果截断以符合类型。 右操作数必须是无符号类型,试图对有符号类型进行移位会产生一个编译错误。

移位可以通过以下方式用2的幂的乘法来 "模拟"。 请注意,对左边操作数类型的截断总是在最后进行,但没有明确提及。

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

  • x >> y 等同于数学表达式 x / 2**y,向负无穷远的方向取整。

警告

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

备注

就像对算术操作那样,对移位操作从不进行溢出检查。相反,结果总是被截断的。

加法、减法和乘法

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

默认情况下,所有的算术都会被检查是否有下溢或上溢,但这可以用 不检查限制 来禁用。 这会导致包装的算术。更多细节可以在那一节中找到。

表达式 -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)

请注意,与此相反,在 字面上 的除法会产生任意精度的分数值。

备注

除以0会导致 异常。这个检查 不能 通过 unchecked { ... } 禁用。

备注

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

取余

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

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

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

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

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

备注

对0取余会导致 异常。这个检查 不能 通过 unchecked { ... } 禁用。

幂运算

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

备注

在检查模式下,幂运算只对小基数使用相对便宜的 exp 操作码。 对于 x**3 的情况,表达式 x*x*x 可能更便宜。 在任何情况下,气体成本测试和使用优化器都是可取的。

备注

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

定长浮点型

警告

Solidity 还没有完全支持定长浮点型。可以声明定长浮点型的变量, 但不能给它们赋值或把它们赋值给其他变量。

fixed / ufixed:表示各种大小的有符号和无符号的定长浮点型。 在关键字 ufixedMxNfixedMxN 中, M 表示该类型占用的位数, N 表示可用的小数位数。 M 必须能整除 8,即 8 到 256 位。 N 则可以是从 0 到 80 之间的任意数。 ufixedfixed 分别是 ufixed128x18fixed128x18 的别名。

运算符:

  • 比较运算符: <=<==!=>=> (返回值是布尔型)

  • 算术运算符: +-, 一元运算 -*/% (取余数)

备注

浮点型(在许多语言中的 floatdouble ,更准确地说是 IEEE 754 类型)和定长浮点型之间最大的不同点是, 在前者中整数部分和小数部分(小数点后的部分)需要的位数是灵活可变的,而后者中这两部分的长度受到严格的规定。 一般来说,在浮点型中,几乎整个空间都用来表示数字,但只有少数的位来表示小数点的位置。

地址类型

地址类型有两种基本相同的类型:

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

  • address payable: 与 address 类型相同,但有额外的方法 transfersend

这种区别背后的想法是, address payable 是一个您可以发送以太币的地址, 而您不应该发送以太币给一个普通的 address,例如,因为它可能是一个智能合约, 而这个合约不是为接受以太币而建立的。

类型转换:

允许从 address payableaddress 的隐式转换, 而从 addressaddress payable 的转换必须通过 payable(<address>) 来明确。

对于 uint160、整数、 bytes20 和合约类型,允许对 address 进行明确的转换和输出。

只有 address 类型和合约类型的表达式可以通过 payable(...) 显式转换为 address payable 类型。 对于合约类型,只有在合约可以接收以太的情况下才允许这种转换,也就是说, 合约要么有一个 receive 函数,要么有一个 payable 类型的 fallback 的函数。 请注意, payable(0) 是有效的,是这个规则的例外。

备注

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

addressaddress payable 之间的区别是从 0.5.0 版本开始的。 同样从该版本开始,合约不能隐式地转换为 address 类型,但仍然可以显式地转换为 addressaddress payable,如果它们有一个 receive 或 payable 类型的 fallback 函数的话。

运算符:

  • <=, <, ==, !=, >=>

警告

如果您使用较大字节的类型转换为 address,例如 bytes32,那么 address 就被截断了。 为了减少转换的模糊性,从 0.4.24 版本开始,编译器将强迫您在转换中明确地进行截断处理。以32字节的值 0x111122333344556677888899AAAABBBBCCCCDDDDEEFFFFCCCC 为例。

您可以使用 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 是一个合约地址,它的代码(更具体地说:它的 接收以太的函数,如果有的话, 或者它的 Fallback 函数,如果有的话)将与 transfer 调用一起执行(这是EVM的一个特性,无法阻止)。 如果执行过程中耗尽了气体或出现了任何故障,以太币的转移将被还原,当前的合约将以异常的方式停止。

  • send

sendtransfer 的低级对应部分。如果执行失败,当前的合约不会因异常而停止,但 send 会返回 false

警告

使用 send 有一些危险:如果调用堆栈深度为1024,传输就会失败(这可以由调用者强制执行), 如果接收者的气体耗尽,也会失败。因此,为了安全地进行以太币转账, 一定要检查 send 的返回值,或者使用 transfer,甚至使用更好的方式: 使用收款人提款的模式。

  • call, delegatecallstaticcall

为了与不遵守ABI的合约对接,或者为了更直接地控制编码, 我们提供了 call, delegatecallstaticcall 函数。 它们都接受一个 bytes memory 参数,并返回成功条件(作为一个 bool) 和返回的数据( bytes memory)。 函数 abi.encode, abi.encodePacked, abi.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 修饰器来调整所提供的气体:

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 中可用。

备注

最好避免在您的智能合约代码中依赖硬编码的气体值,无论状态是读出还是写入, 因为这可能有很多隐患。另外,对气体的访问在未来可能会改变。

  • codecodehash

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

备注

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

合约类型

每个 合约 都定义了自己的类型。 您可以隐式地将一个合约转换为它们所继承的另一个合约。 合约可以显式地转换为 address 类型,也可以从 address 类型中转换。

只有在合约类型具有 receive 或 payable 类型的 fallback 函数的情况下, 才有可能明确转换为 address payable 类型和从该类型转换。 这种转换仍然使用 address(x) 进行转换。如果合约类型没有一个 receive 或 payable 类型的 fallback 函数, 可以使用 payable(address(x)) 来转换为 address payable 。 您可以在 地址类型 一节中找到更多信息。

备注

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

如果您声明了一个本地类型的变量( MyContract c ),您可以调用该合约上的函数。 注意要从相同合约类型的地方将其赋值。

您也可以实例化合约(这意味着它们是新创建的)。 您可以在 '通过关键字new创建合约' 部分找到更多细节。

合约的数据表示与 address 类型相同,该类型也用于 ABI

合约不支持任何运算符。

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

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

定长字节数组

值类型 bytes1, bytes2, bytes3, ..., bytes32 代表从1到32的字节序列。

运算符:

比较运算符:<=, <, ==, !=, >=, > (返回布尔型)

  • 比较运算符: <=<==!=>=> (返回 bool)

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

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

  • 索引访问: 如果 xbytesI 类型,那么当 0 <= k < I 时, x[k] 返回第 k 个字节(只读)。

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

成员变量:

  • .length 表示这个字节数组的长度(只读).

备注

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

备注

在 0.8.0 版本之前, byte 曾经是 bytes1 的别名。

变长字节数组

bytes:

变长字节数组,参见 数组。它并不是值类型!

string:

变长 UTF-8 编码字符串类型,参见 数组。并不是值类型!

地址字面常数(Address Literals)

比如像 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF 这样的 通过了地址校验测试的十六进制字属于 address 类型。 十六进制字数在39到41位之间,并且没有通过校验测试,会产生一个错误。 您可以预加(对于整数类型)或附加(对于bytesNN类型)零来消除该错误。

备注

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

有理数和整数字面常数

整数字面常数由范围在 0-9 的一串数字组成,表现成十进制。 例如, 69 表示十进制数字 69。 Solidity 中是没有八进制的,因此前置 0 是无效的。

小数字面常数由 . 和小数点后的至少一个数字组成。例如, .11.3``(但不是 ``1.)。

也支持 2e10 形式的科学符号,其中尾数可以是小数,但指数必须是一个整数。 字面的 MeE 相当于 M * 10**E。 例子包括 2e10, -2e10, 2e-10, 2.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 (负数数值字面常数)类型中进行。 无论右(指数)操作数的类型如何。

警告

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

备注

Solidity 对每个有理数都有对应的数值字面常数类型。 整数字面常数和有理数字面常数都属于数值字面常数类型。 除此之外,所有的数值字面常数表达式(即只包含数值字面常数和运算符的表达式)都属于数值字面常数类型。 因此数值字面常数表达式 1 + 22 + 1 的结果跟有理数3的数值字面常数类型相同。

备注

数字字面表达式一旦与非字面表达式一起使用,就会被转换为非字面类型。 不考虑类型,下面分配给 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" 相当于3个字节而不是4个。 和整数字面常数一样,字符串字面常数的类型也可以发生改变, 但它们可以隐式地转换成 bytes1,……, bytes32,如果合适的话,还可以转换成 bytes 以及 string

例如,使用 bytes32 samevar = "stringliteral", 当分配给 bytes32 类型时,字符串字面常数被解释成原始字节形式。

字符串字面常数只能包含可打印的ASCII字符,也就是0x20 ... 0x7E之间的字符。

此外,字符串字元还支持以下转义字符:

  • \<newline> (转义一个实际的换行)

  • \\ (反斜杠)

  • \' (单引号)

  • \" (双引号)

  • \n (换行)

  • \r (回车键)

  • \t (制表)

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

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

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

备注

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

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

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

任何非换行的 Unicode 行结束符(即LF, VF, FF, CR, NEL, LS, PS)都被认为是字符串字面的结束。 换行只在字符串字面内容前面没有 \ 的情况下终止。

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 中创建用户定义类型的一种方式。 它们可以显式地转换为所有整数类型,和从整数类型来转换,但不允许隐式转换。 从整数的显式转换在运行时检查该值是否在枚举的范围内,否则会导致 异常。 枚举要求至少有一个成员,其声明时的默认值是第一个成员。 枚举不能有超过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;
    }

    // 由于枚举类型不属于ABI的一部分,因此对于所有来自 Solidity 外部的调用,
    // "getChoice" 的签名会自动被改成 "getChoice() returns (uint8)"。
    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;

// 使用用户定义的值类型表示一个18位小数,256位宽的定点类型。
type UFixed256x18 is uint256;

/// 一个在UFixed256x18上进行定点操作的最小库。
library FixedMath {
    uint constant multiplier = 10**18;

    /// 将两个UFixed256x18的数字相加。溢出时将返回,依靠uint256的算术检查。
    function add(UFixed256x18 a, UFixed256x18 b) internal pure returns (UFixed256x18) {
        return UFixed256x18.wrap(UFixed256x18.unwrap(a) + UFixed256x18.unwrap(b));
    }
    /// 将UFixed256x18和uint256相乘。溢出时将返回,依靠uint256的算术检查。
    function mul(UFixed256x18 a, uint256 b) internal pure returns (UFixed256x18) {
        return UFixed256x18.wrap(UFixed256x18.unwrap(a) * b);
    }
    /// 对一个UFixed256x18类型的数字相下取整。
    /// @return 不超过 `a` 的最大整数。
    function floor(UFixed256x18 a) internal pure returns (uint256) {
        return UFixed256x18.unwrap(a) / multiplier;
    }
    /// 将一个uint256转化为相同值的UFixed256x18。
    /// 如果整数太大,则恢复计算。
    function toUFixed256x18(uint256 a) internal pure returns (UFixed256x18) {
        return UFixed256x18.wrap(a * multiplier);
    }
}

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

函数类型

函数类型是一种表示函数的类型。可以将一个函数赋值给另一个函数类型的变量, 也可以将一个函数作为参数进行传递,还能在函数调用中返回函数类型变量。 函数类型有两类:- 内部(internal) 函数和 外部(external) 函数:

内部函数只能在当前合约内被调用(更具体来说, 在当前代码块内,包括内部库函数和继承的函数中), 因为它们不能在当前合约上下文的外部被执行。 调用一个内部函数是通过跳转到它的入口标签来实现的, 就像在当前合约的内部调用一个函数。

外部函数由一个地址和一个函数签名组成,可以通过外部函数调用传递或者返回。

函数类型表示成如下的形式:

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

与参数类型相反,返回类型不能为空 —— 如果函数类型不需要返回, 则需要删除整个 returns (<return types>) 部分。

默认情况下,函数类型是内部函数,所以可以省略 internal 关键字。 注意,这只适用于函数类型。对于合约中定义的函数, 必须明确指定其可见性,它们没有默认类型。

转换:

当且仅当它们的参数类型相同,它们的返回类型相同,它们的内部/外部属性相同, 并且 A 的状态可变性比 B 的状态可变性更具限制性时, 一个函数类型 A 就可以隐式转换为一个函数类型 B。特别是:

  • pure 函数可以转换为 view payable 函数

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

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

其他函数类型之间的转换是不可能的。

关于 payable payable 的规则可能有点混乱, 但实质上,如果一个函数是 payable,这意味着 它也接受零以太的支付,所以它也是 payable。 另一方面,一个 payable 的函数将拒收发送给它的以太, 所以 payable 的函数不能被转换为 payable 的函数。 声明一下,拒收以太比不拒收以太更有限制性。 这意味着您可以用一个不可支付的函数覆写一个可支付的函数,但不能反过来。

此外,当您定义一个 payable 的函数指针时, 编译器并不强制要求被指向的函数实际拒收以太。 相反,它强制要求该函数指针永远不会被用来发送以太。 这使得我们有可能将一个 payable 的函数指针分配给一个 payable 的函数指针, 以确保这两种类型的函数表现相同,即都不能用来发送以太。

如果一个函数类型的变量没有被初始化,调用它将导致 会出现 异常。如果您在一个函数上使用了 delete 之后再调用它, 也会发生同样的情况。

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

请注意,当前合约的公共函数既可以被当作内部函数也可以被当作外部函数使用。 如果想将一个函数当作内部函数使用,就用 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 只影响到外部函数的实现,在调用者一方的函数指针中是没有意义的。

库合约被排除在外,因为它们需要 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 {
    // 内部函数可以在内部库函数中使用,因为它们将是同一代码上下文的一部分
    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 {
        // 这里要检查的是调用返回是否来自可信的来源
        requests[requestID].callback(response);
    }
}


contract OracleUser {
    Oracle constant private ORACLE_CONST = Oracle(address(0x00000000219ab540356cBB839Cbe05303d7705Fa)); // 已知的合约
    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 (包含函数参数的特殊数据位置)。

改变数据位置的赋值或类型转换将总是导致自动复制操作, 而同一数据位置内的赋值只在某些情况下对存储类型进行复制。

数据位置

每个引用类型都有一个额外的属性,即 "数据位置", 关于它的存储位置。有三个数据位置。 memory, storagecalldata。 Calldata是一个不可修改的、非持久性的区域,用于存储函数参数,其行为主要类似于memory。

备注

如果可以的话,尽量使用 calldata 作为数据位置,因为这样可以避免复制, 也可以确保数据不能被修改。使用 calldata 数据位置的数组和结构也可以从函数中返回, 但不可能分配这种类型。

备注

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

备注

在0.5.0版本之前,数据位置可以省略,并且会根据变量的种类、函数类型等默认为不同的位置, 但现在所有的复杂类型都必须给出一个明确的数据位置。

数据位置和分配行为

数据位置不仅与数据的持久性有关,而且也与分配的语义有关:

  • storagememory 之间的分配(或从 calldata 中分配) 总是创建一个独立的拷贝。

  • memorymemory 的赋值只创建引用。 这意味着对一个内存变量的改变在所有其他引用相同数据的内存变量中也是可见的。

  • storagelocal 存储变量的赋值也只赋值一个引用。

  • 所有其他对 storage 的赋值总是拷贝的。 这种情况的例子是对状态变量或存储结构类型的局部变量成员的赋值, 即使局部变量本身只是一个引用。

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

contract C {
    // x 的数据存储位置是 storage。
    // 这是唯一可以省略数据位置的地方。
    uint[] x;

    // memoryArray 的数据存储位置是 memory。
    function f(uint[] memory memoryArray) public {
        x = memoryArray; // 将整个数组拷贝到 storage 中,可行
        uint[] storage y = x; // 分配一个指针,其中 y 的数据存储位置是 storage,可行
        y[7]; // 返回第 8 个元素,可行
        y.pop(); // 通过y修改x,可行
        delete x; // 清除数组,同时修改 y,可行
        // 下面的就不可行了;需要在 storage 中创建新的未命名的临时数组,/
        // 但 storage 是“静态”分配的:
        // y = memoryArray;
        // 同样, "delete y" 也是无效的,
        // 因为对引用存储对象的局部变量的赋值只能从现有的存储对象中进行。
        // 它将 “重置” 指针,但没有任何合理的位置可以指向它。
        // 更多细节见 "delete" 操作符的文档。
        // delete y;
        g(x); // 调用 g 函数,同时移交对 x 的引用
        h(x); // 调用 h 函数,同时在 memory 中创建一个独立的临时拷贝
    }

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

数组

数组可以在声明时指定长度,也可以动态调整大小。

一个元素类型为 T,固定长度为 k 的数组可以声明为 T[k], 而动态数组声明为 T[]

例如,一个由5个 uint 的动态数组组成的数组被写成 uint[][5]。 与其他一些语言相比, 这种记法是相反的。 在Solidity中, X[3] 总是一个包含三个 X 类型元素的数组, 即使 X 本身是一个数组。 这在其他语言中是不存在的,如C语言。

索引是基于零的,访问方向与声明相反。

例如,如果您有一个变量 uint[][5] memory x,您用 x[2][6] 访问第三个动态数组中的第七个 uint, 要访问第三个动态数组,用 x[2]。同样,如果您有一个数组 T[5] a 的类型 T, 也可以是一个数组,那么 a[2] 总是有类型 T

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

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

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

备注

动态大小的数组只能在存储中调整大小。 在内存中,这样的数组可以是任意大小的,但是一旦分配了数组,就不能改变数组的大小。

bytesstring 类型的数组

bytesstring 类型的变量是特殊的数组。 bytes 类似于 bytes1[], 但它在 calldata 中会被“紧打包”(译者注:将元素连续地存在一起,不会按每 32 字节一单元的方式来存放)。 stringbytes 相同,但不允许用长度或索引来访问。

Solidity没有字符串操作函数,但有第三方的字符串库。 您也可以用 keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2)) 来比较两个字符串的keccak256-hash,用 string.concat(s1, s2) 来连接两个字符串。

您应该使用 bytes 而不是 bytes1[],因为它更便宜, 因为在 memory 中使用 bytes1[] 会在元素之间增加31个填充字节。 请注意,在 storage 中,由于紧打包,没有填充,参见 字节和字符串。 一般来说,对于任意长度的原始字节数据使用 bytes,对于任意长度的字符串(UTF-8)数据使用 string。 如果您能将长度限制在一定的字节数,总是使用 bytes1bytes32 中的一种值类型,因为它们更便宜。

备注

如果想要访问以字节表示的字符串 s, 请使用 bytes(s).length / bytes(s)[7] = 'x';。 注意这时您访问的是 UTF-8 形式的低级 bytes 类型,而不是单个的字符。

函数 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]];
        // 下面的方法不会起作用,因为一些内部数组的类型不对。
        // 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;

// 这不会被编译。
contract C {
    function f() public {
        // 下一行会产生一个类型错误,因为uint[3]内存不能被转换为uint[]内存。
        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:

数组有 length 成员变量表示当前数组的长度。一经创建, 内存memory数组的大小就是固定的(但却是动态的,也就是说,它依赖于运行时的参数)。

push():

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

push(x):

动态存储数组和 bytes (不是 string )有一个叫 push(x) 的成员函数, 您可以用它在数组的末端追加一个指定的元素。该函数不返回任何东西。

pop():

动态存储数组和 bytes (不是 string )有一个叫 pop() 的成员函数, 您可以用它来从数组的末端移除一个元素。 这也隐含地在被删除的元素上调用 delete。该函数不返回任何东西。

备注

通过调用 push() 增加存储数组的长度有恒定的气体成本,因为存储是零初始化的, 而通过调用 pop() 减少长度的成本取决于被移除元素的 "大小"。 如果该元素是一个数组,它的成本可能非常高, 因为它包括明确地清除被移除的元素,类似于对它们调用 delete

备注

要在外部(而不是公开)函数中使用数组的数组, 您需要激活ABI coder v2。

备注

在Byzantium之前的EVM版本中,不可能访问从函数调用返回的动态数组。 如果您调用返回动态数组的函数,请确保使用设置为Byzantium模式的EVM。

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

contract ArrayContract {
    uint[2**20] aLotOfIntegers;
    // 请注意,下面不是一对动态数组,
    // 而是一个动态数组对(即长度为2的固定大小数组)。
    // 在 Solidity 中,T[k]和T[]总是具有T类型元素的数组,
    // 即使T本身是一个数组。
    // 正因为如此,bool[2][]是一个动态数组对,其元素是bool[2]。
    // 这与其他语言不同,比如C,
    // 所有状态变量的数据位置都是存储。
    bool[2][] pairsOfFlags;

    // newPairs被存储在memory中--这是公开合约函数参数的唯一可能性。
    function setAllFlagPairs(bool[2][] memory newPairs) public {
        // 赋值到一个存储数组会执行 ``newPairs`` 的拷贝,
        // 并替换完整的数组 ``pairsOfFlags``。
        pairsOfFlags = newPairs;
    }

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

    function f(uint[] memory c) public {
        // 在 ``g`` 中存储一个对 ``s`` 的引用。
        StructType storage g = s;
        // 也改变了 ``s.moreInfo``.
        g.moreInfo = 2;
        // 指定一个拷贝,因为 ``g.contents`` 不是一个局部变量,
        // 而是一个局部变量的成员。
        g.contents = c;
    }

    function setFlagPair(uint index, bool flagA, bool flagB) public {
        // 访问一个不存在的数组索引会引发一个异常
        pairsOfFlags[index][0] = flagA;
        pairsOfFlags[index][1] = flagB;
    }

    function changeFlagArraySize(uint newSize) public {
        // 使用push和pop是改变数组长度的唯一方法。
        if (newSize < pairsOfFlags.length) {
            while (pairsOfFlags.length > newSize)
                pairsOfFlags.pop();
        } else if (newSize > pairsOfFlags.length) {
            while (pairsOfFlags.length < newSize)
                pairsOfFlags.push();
        }
    }

    function clear() public {
        // 这些完全清除了数组
        delete pairsOfFlags;
        delete aLotOfIntegers;
        // 这里有相同的效果
        pairsOfFlags = new bool[2][](0);
    }

    bytes byteData;

    function byteArrays(bytes memory data) public {
        // 字节数组("byte")是不同的,因为它们的存储没有填充,
        // 但可以与 "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) {
        // 使用 `new` 创建动态 memory 数组:
        uint[2][] memory arrayOfPairs = new uint[2][](size);

        // 内联数组总是静态大小的,如果您只使用字面常数表达式,您必须至少提供一种类型。
        arrayOfPairs[0] = [uint(1), 2];

        // 创建一个动态字节数组:
        bytes memory b = new bytes(200);
        for (uint i = 0; i < b.length; i++)
            b[i] = bytes1(uint8(i));
        return b;
    }
}

对存储数组元素的悬空引用(Dangling References)

当使用存储数组时,您需要注意避免悬空引用。 悬空引用是指一个指向不再存在的或已经被移动而未更新引用的内容的引用。 例如,如果您将一个数组元素的引用存储在一个局部变量中, 然后从包含数组中使用 .pop(),就可能发生悬空引用:

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

contract C {
    uint[][] s;

    function f() public {
        // 存储一个指向s的最后一个数组元素的指针。
        uint[] storage ptr = s[s.length - 1];
        // 删除s的最后一个数组元素。
        s.pop();
        // 写入已不在数组内的数组元素。
        ptr.push(0x42);
        // 现在向 ``s`` 添加一个新元素不会添加一个空数组,
        // 而是会产生一个长度为1的数组,元素为 ``0x42``。
        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() {
        // 向存储数组推送一些初始值。
        s.push(0x07);
        t.push(0x03);
    }

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

    function f() public returns (uint[] memory) {
        // 下面将首先评估 ``s.push()` 到一个索引为1的新元素的引用。
        // 之后,调用 ``g`` 弹出这个新元素,
        // 导致最左边的元组元素成为一个悬空的引用。
        // 赋值仍然发生,并将写入 ``s`` 的数据区域之外。
        (s.push(), g()[0]) = (0x42, 0x17);
        // 随后对 ``s`` 的推送将显示前一个语句写入的值,
        // 即在这个函数结束时 ``s`` 的最后一个元素将有 ``0x42`` 的值。
        s.push();
        return s;
    }
}

每条语句只对存储进行一次赋值,并避免在赋值的左侧使用复杂的表达式,这样做总是比较安全的。

您需要特别小心处理对 bytes 数组元素的引用, 因为 bytes 数组的 .push() 操作可能会 在存储中从短布局切换到长布局

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

// 这将报告一个警告
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 大于数组的长度, 就会出现异常。

startend 都是可选的: start 默认为 0end 默认为数组的长度。

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

数组切片没有类型名,这意味着任何变量都不能以数组切片为类型, 它们只存在于中间表达式中。

备注

到现在为止,数组切片只有calldata数组可以实现。

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

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.5 <0.9.0;
contract Proxy {
    /// @dev 由代理管理的客户合约的地址,即本合约的地址
    address client;

    constructor(address client_) {
        client = client_;
    }

    /// 转发对 "setOwner(address)" 的调用,
    /// 该调用在对地址参数进行基本验证后由客户端执行。
    function forward(bytes calldata payload) external {
        bytes4 sig = bytes4(payload[:4]);
        // 由于截断行为,bytes4(payload)的表现是相同的。
        // 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;

// 定义一个包含两个属性的新类型。
// 在合约之外声明一个结构,
// 可以让它被多个合约所共享。
// 在这里,这并不是真的需要。
struct Funder {
    address addr;
    uint amount;
}

contract CrowdFunding {
    // 结构体也可以被定义在合约内部,这使得它们只在本合约和派生合约中可见。
    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 作为一个变量返回
        // 我们不能使用 "campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0)"
        // 因为右侧创建了一个内存结构 "Campaign",其中包含一个映射。
        Campaign storage c = campaigns[campaignID];
        c.beneficiary = beneficiary;
        c.fundingGoal = goal;
    }

    function contribute(uint campaignID) public payable {
        Campaign storage c = campaigns[campaignID];
        // 以给定的值初始化,创建一个新的临时 memory 结构体,
        // 并将其拷贝到 storage 中。
        // 注意您也可以使用 Funder(msg.sender, msg.value) 来初始化。
        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?) VariableName 声明。 KeyType 可以是任何内置的值类型, bytesstring,或任何合约或枚举类型。 其他用户定义的或复杂的类型,如映射,结构体或数组类型是不允许的。 ValueType 可以是任何类型,包括映射,数组和结构体。 KeyNameValueName 是可选的(所以 mapping(KeyType => ValueType) 也可以使用), 可以是任何有效的标识符,而不是一个类型。

您可以把映射想象成 哈希表, 它实际上被初始化了,使每一个可能的键都存在, 并将其映射到字节形式全是零的值,一个类型的 默认值。 相似性到此为止,键数据不存储在映射中,而是它的 keccak256 哈希值被用来查询。

正因为如此,映射没有长度,也没有被设置的键或值的概念, 因此,如果没有关于分配的键的额外信息,就不能被删除(见 清除映射)。

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

您可以把映射类型的状态变量标记为 public, Solidity 会为您创建一个 getter 函数。 KeyType 成为 getter 函数的参数,名称为 KeyName (如果指定)。 如果 ValueType 是一个值类型或一个结构,getter 返回 ValueType, 名称为 ValueName (如果指定)。 如果 ValueType 是一个数组或映射,getter 对每个 KeyType 递归出一个参数。

在下面的例子中, MappingExample 合约定义了一个公共的 balances 映射, 键类型是 address,值类型是 uint,将一个Ethereum地址映射到一个无符号整数值。 由于 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);
    }
}

// 如何使用
contract User {
    // 只是一个保存我们数据的结构体。
    itmap data;
    // 对数据类型应用库函数。
    using IterableMapping for itmap;

    // 插入一些数据
    function insert(uint k, uint v) public returns (uint size) {
        // 这将调用 IterableMapping.insert(data, k, v)
        data.insert(k, v);
        // 我们仍然可以访问结构中的成员,
        // 但我们应该注意不要乱动他们。
        return data.size;
    }

    // 计算所有存储数据的总和。
    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,其中 xuint8z 的类型为 int32。 在这种情况下,下面的机制将被用来确定计算操作的类型(这在溢出的情况下很重要)和操作结果的类型:

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

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

  3. 否则的话,该操作不被允许。

如果其中一个操作数是 字面常数, 它首先被转换为其 “移动类型(mobile type)”,也就是能容纳该值的最小类型 (相同位宽的无符号类型被认为比有符号类型 “小”)。 如果两者都是字面常数,那么运算的精度实际上是无限的, 因为表达式被转换到任何必要的精度,所以当结果被用于非字面类型时,没有任何损失。

操作符的结果类型与操作的类型相同,除了比较操作符,其结果总是 bool

运算符 ** (幂运算), <<>> 使用左边操作数的类型进行运算和以其作为结果。

三元运算符

三元运算符用于形式为 <条件表达式> ? <true条件表达式> : <false条件表达式>。 它根据主要的 <条件表达式> 的评估结果,计算后两个给定表达式中的一个。 如果 <条件表达式> 评估为 true,那么 <true条件表达式> 将被计算,否则 <false条件表达式> 被被计算。

三元运算符的结果没有有理数类型,即使它的操作数都是有理数字。 结果类型是由两个操作数的类型决定的,方法同上,如果需要的话,首先转换为它们的可移动计算的类型。

因此, 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, 但是它也可以用于数组,它指定一个长度为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; // 将 x 设为 0,并不影响data变量
        delete data; // 将 data 设为 0,并不影响 x
        uint[] storage y = dataArray;
        delete dataArray; // 将 dataArray.length 设为 0,但由于 uint[] 是一个复杂的对象,
        // y 也将受到影响,它是一个存储位置是 storage 的对象的别名。
        // 另一方面:"delete y" 是非法的,引用了 storage 对象的局部变量只能由已有的 storage 对象赋值。
        assert(y.length == 0);
    }
}

运算符的优先顺序

以下是按评估顺序列出的操作符优先级。

优先级

描述

操作符

1

后置自增和自减

++--

创建类型实例

new <类型名>

数组元素

<数组>[<索引>]

访问成员

<对象>.<成员名>

函数调用

<函数>(<参数...>)

小括号

(<表达式>)

2

前置自增和自减

++--

一元运算减

-

一元操作符

delete

逻辑非

!

按位非

~

3

乘方

**

4

乘、除和模运算

*/%

5

算术加和减

+-

6

移位操作符

<<>>

7

按位与

&

8

按位异或

^

9

按位或

|

10

非等操作符

<><=>=

11

等于操作符

==!=

12

逻辑与

&&

13

逻辑或

==

14

三元操作符

<判断条件> ? <如果为真时执行的表达式> : <如果为假时执行的表达式>

赋值操作符

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

15

逗号

,

基本类型之间的转换

隐式转换

在某些情况下,在赋值过程中,在向函数传递参数和应用运算符时, 编译器会自动应用隐式类型转换。一般来说,如果在语义上有意义, 并且不会丢失信息,那么值-类型之间的隐式转换是可能的。

例如, uint8 可以转换为 uint16int128 可以转换为 int256, 但是 int8 不能转换为 uint256,因为 uint256 不能容纳 -1 这样的值。

如果一个运算符被应用于不同的类型, 编译器会尝试将其中一个操作数隐含地转换为另一个的类型(对于赋值也是如此)。 这意味着操作总是以其中一个操作数的类型进行。

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

在下面的例子中, yz,即加法的操作数,没有相同的类型, 但是 uint8 可以隐式转换为 uint16,反之则不行。正因为如此, y 被转换为 z 的类型,然后在 uint16 类型中进行加法。 结果表达式 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 现在会是 0x5678

如果一个整数被明确地转换为一个更大的类型,它将在左边被填充(即在高阶的一端)。 转换的结果将与原整数比较相等:

uint16 a = 0x1234;
uint32 b = uint32(a); // b 现在会是 0x00001234
assert(a == b);

固定大小的字节类型在转换过程中的行为是不同的。 它们可以被认为是单个字节的序列,转换到一个较小的类型将切断序列:

bytes2 a = 0x1234;
bytes1 b = bytes1(a); // b 现在会是 0x12

如果一个固定大小的字节类型被明确地转换为一个更大的类型,它将在右边被填充。 访问固定索引的字节将导致转换前后的数值相同(如果索引仍在范围内):

bytes2 a = 0x1234;
bytes4 b = bytes4(a); // b 现在会是 0x12340000
assert(a[0] == b[0]);
assert(a[1] == b[1]);

于整数和固定大小的字节数组在截断或填充时表现不同, 只有在整数和固定大小的字节数组具有相同大小的情况下,才允许在两者之间进行显式转换。 如果您想在不同大小的整数和固定大小的字节数组之间进行转换,您必须使用中间转换, 使所需的截断和填充规则明确:

bytes2 a = 0x1234;
uint32 b = uint16(a); // b 将会是 0x00001234
uint32 c = uint32(bytes4(a)); // c 将会是 0x12340000
uint8 d = uint8(uint16(a)); // d 将会是 0x34
uint8 e = uint8(bytes1(a)); // e 将会是 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);  // 如果m的长度大于16,将发生截断。
        b = bytes16(s);  // 右边进行填充,所以结果是 "abcdefgh\0\0\0\0\0\0\0\0"
        bytes3 b1 = bytes3(s); // 发生截断, b1 相当于 "abc"
        b = bytes16(c[:8]);  // 同样用0进行填充
        return (b, b1);
    }
}

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

整数类型

十进制和十六进制的数字字面常数可以隐含地转换为任何足够大的整数类型去表示它而不被截断:

uint8 a = 12; // 可行
uint32 b = 1234; // 可行
uint16 c = 0x123456; // 报错, 因为这将会截断成 0x3456

备注

在0.8.0版本之前,任何十进制或十六进制的数字字面常数都可以显式转换为整数类型。 从0.8.0开始,这种显式转换和隐式转换一样严格,也就是说,只有当字面意义符合所产生的范围时,才允许转换。

固定大小的字节数组

十进制数字字面常数不能被隐含地转换为固定大小的字节数组。 十六进制数字字面常数是可以的,但只有当十六进制数字的数量正好符合字节类型的大小时才可以。 但是有一个例外,数值为0的十进制和十六进制数字字面常数都可以被转换为任何固定大小的字节类型:

bytes2 a = 54321; // 不允许
bytes2 b = 0x12; // 不允许
bytes2 c = 0x123; // 不允许
bytes2 d = 0x1234; // 可行
bytes2 e = 0x0012; // 可行
bytes4 f = 0; // 可行
bytes4 g = 0x0; // 可行

字符串和十六进制字符串字面常数可以被隐含地转换为固定大小的字节数组, 如果它们的字符数与字节类型的大小相匹配:

bytes2 a = hex"1234"; // 可行
bytes2 b = "xy"; // 可行
bytes2 c = hex"12"; // 不允许
bytes2 d = hex"123"; // 不允许
bytes2 e = "x"; // 不允许
bytes2 f = "xyz"; // 不允许

地址类型

正如在 地址字面常数(Address Literals) 中所描述的那样,正确大小并通过校验测试的十六进制字是 address 类型。 其他字面常数不能隐含地转换为 address 类型。

只允许从 bytes20uint160 显式转换到 address

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

备注

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