类型
Solidity 是一种静态类型语言,这意味着每个变量(状态变量和局部变量)的类型都需要被指定。 Solidity 提供了几种基本类型,可以组合形成复杂类型。
此外,类型可以在包含运算符的表达式中相互作用。有关各种运算符的快速参考,请参见 运算符优先级顺序。
“undefined”或“null”值的概念在 Solidity 中不存在,但新声明的变量总是具有依赖于其类型的 默认值。
为了处理任何意外值,应该使用 revert function 来回滚整个交易,或者返回一个包含第二个 bool
值表示成功的元组。
值类型
以下被称为值类型,因为它们的变量总是按值传递,即在用作函数参数或赋值时总是被复制。
与 引用类型 不同,值类型声明不指定数据位置,因为它们足够小,可以存储在栈上。 唯一的例外是 状态变量。 这些变量默认位于存储中,但也可以标记为 瞬态、常量或不可变。
布尔类型
bool
:可能的值是常量 true
和 false
。
运算符:
!
(逻辑非)&&
(逻辑与,“和”)||
(逻辑或,“或”)==
(相等)!=
(不相等)
运算符 ||
和 &&
遵循常见的短路规则。
这意味着在表达式 f(x) || g(y)
中,如果 f(x)
计算为 true
,则 g(y)
将不会被计算,即使它可能有副作用。
整型
int
/ uint
:各种大小的有符号和无符号整数。
关键字 uint8
到 uint256
以 8
为步长(无符号从 8 位到 256 位)和 int8
到 int256
。
uint
和 int
分别是 uint256
和 int256
的别名。
运算符:
比较:
<=
、<
、==
、!=
、>=
、>``(计算为 ``bool
)移位运算符:
<<``(左移)、
>>``(右移)
对于整数类型 X
,可以使用 type(X).min
和 type(X).max
来访问该类型可表示的最小值和最大值。
警告
Solidity 中的整数限制在某个范围内。例如,对于 uint32
,范围是 0
到 2**32 - 1
。
对这些类型的算术运算有两种模式:”wrapping”(截断)模式或称 “unchecked”(不检查)模式和”checked” (检查)模式。
默认情况下,算术运算始终是 “checked” 的,这意味着如果操作的结果超出该类型的值范围,则调用通过 失败的断言 被回退。
可以使用 unchecked { ... }
切换到 “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)
,其中 T
是 x
的类型。它只能应用于有符号类型。
如果 x
为负,则 -x
的值可以为正。还有另一个警告也源于二进制补码表示:
如果有 int x = type(int).min;
,那 -x
将不在正数取值的范围内。
这意味着 unchecked { assert(-x == x); }
是有效的,而在检查模式下使用表达式 -x
将导致断言失败。
除法
由于操作的结果类型始终是其中一个操作数的类型,因此整数上的除法始终会产生一个整数。
在 Solidity 中,除法向零舍入。这意味着 int256(-5) / int256(2) == int256(-2)
。
请注意,相比之下, 字面量 上的除法会产生任意精度的分数值。
备注
除以 0 会导致 Panic 错误。此检查 不能 通过 unchecked { ... }
禁用。
备注
表达式 type(int).min / (-1)
是唯一导致除法溢出的情况。
在算术检查模式下,这将导致断言失败,而在截断模式下,值将是 type(int).min
。
模运算
模运算 a % n
产生操作数 a
除以操作数 n
后的余数 r
,其中 q = int(a / n)
和 r = a - (n * q)
。
这意味着模运算结果与其左操作数(或零)具有相同的符号,并且对于负数 a
,a % 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)
备注
对 0 取模会导致 Panic 错误。此检查 不能 通过 unchecked { ... }
禁用。
指数
指数运算仅适用于无符号类型的指数。指数运算的结果类型始终等于基数的类型。 请确保它足够大以容纳结果,并准备好潜在的断言失败或包装行为。
备注
在“checked”模式下,指数运算仅对小基数使用相对便宜的 exp
操作码。
对于 x**3
的情况,表达式 x*x*x
可能更便宜。
在任何情况下,都建议进行 gas 成本测试并使用优化器。
备注
请注意,0**0
被 EVM 定义为 1
。
定长浮点型
警告
Solidity 中尚未完全支持定长浮点型。它们可以被声明,但不能被赋值。
fixed
/ ufixed
:各种大小的有符号和无符号定长浮点型。
关键字 ufixedMxN
和 fixedMxN
,其中 M
表示类型占用的位数,N
表示可用的小数位数。
M
必须是 8 的倍数,范围从 8 到 256 位。
N
必须在 0 到 80 之间(包括 0 和 80)。
ufixed
和 fixed
分别是 ufixed128x18
和 fixed128x18
的别名。
运算符:
备注
浮点型(在许多语言中为 float
和 double
, 更准确地说是 IEEE 754 数字)与定长浮点型之间的主要区别在于,
前者用于整数和小数部分(小数点后的部分)所使用的位数是灵活的,而后者则是严格定义的。
一般来说,在浮点型中,几乎整个空间都用于表示数字,而只有少量位数定义小数点的位置。
地址类型
地址类型有两种基本相同的变体:
address
:保存一个 20 字节的值(以太坊地址的大小)。address payable
:与address
相同,但具有额外的成员transfer
和send
。
这种区分的想法是 address payable
是一个可以发送以太币的地址,
而不应该向普通的 address
发送以太币,例如因为它可能是一个不支持接受以太币的智能合约。
类型转换:
允许从 address payable
到 address
的隐式转换,
而从 address
到 address payable
的转换必须通过 payable(<address>)
显式进行。
对于 uint160
、整数字面量、bytes20
和合约类型,允许显式转换为 address
。
只有类型为 address
和合约类型的表达式可以通过显式转换 payable(...)
转换为 address payable
类型。
对于合约类型,只有在合约可以接收以太币时才允许此转换,即合约要么具有 receive,要么具有可支付的回退函数。
请注意 payable(0)
是有效的,并且是此规则的例外。
备注
如果你需要 address
类型的变量并计划向其发送以太币,那么将其类型声明为 address payable
可以明确表达出你的需求。
此外,尽量尽早进行这种区分或转换。
address
和 address payable
之间的区分是在 0.5.0 版本中引入的。
从该版本开始,合约不再隐式转换为 address
类型,但仍可以显式转换为 address
或 address payable
,如果它们具有 receive 或 payable 回退函数。
运算符:
<=
,<
,==
,!=
,>=
和>
警告
如果使用更大字节大小的类型转换为 address
,例如 bytes32
,则 address
会被截断。
为了减少转换歧义,从 0.4.24 版本开始,编译器将强制在转换中显式进行截断。
例如 32 字节值 0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCCC
。
可以使用 address(uint160(bytes20(b)))
,结果为 0x111122223333444455556666777788889999aAaa
,
或者使用 address(uint160(uint256(b)))
,结果为 0x777788889999AaAAbBbbCcccddDdeeeEfFFfCcCc
。
地址类型的成员变量
有关地址所有成员的快速参考,请参见 地址类型的成员。
balance
和transfer
可以使用 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 或因任何原因失败,则以太币转账将被回退,前的合约也会在终止的同时抛出异常。
send
send
是 transfer
对应的低级函数。如果执行失败,当前合约不会以异常停止,但 send
将返回 false
。
警告
使用 send
存在一些危险:如果调用栈深度达到 1024,则转账失败(这总是可以被调用者强制),如果接收者耗尽 gas 也会失败。
因此,为了安全地转账以太币,请始终检查 send
的返回值,使用 transfer
或更好地方式:让接收者提取以太币的模式。
call
、delegatecall
和staticcall
为了与不符合 应用二进制接口 的合约交互,或者要更直接地控制编码,提供了 call
、delegatecall
和 staticcall
函数。
它们都接受一个 bytes memory
参数并返回执行成功状态(bool
)和数据(bytes memory
)。
可以使用 abi.encode
、abi.encodePacked
、abi.encodeWithSelector
和 abi.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.sender
和 msg.value
值。此功能在 0.5.0 版本中被移除。
自拜占庭(byzantium)版本 起,staticcall
也可以使用。这基本上与 call
相同,但如果被调用的函数以任何方式修改状态,则会回退。
这三种函数 call
、delegatecall
和 staticcall
都是非常底层的函数,应该仅当作 最后的手段 来使用,因为它们破坏了 Solidity 的类型安全。
gas
选项在所有三种方法中都可用,而 value
选项仅在 call
中可用。
备注
最好避免在智能合约代码中依赖硬编码的 gas 值,无论是读取状态还是写入状态,因为这可能会有许多陷阱。此外,未来 gas 的使用可能会发生变化。
code
和codehash
你可以查询任何智能合约的已部署代码。使用 .code
获取 EVM 字节码作为 bytes memory
,这可能是空的。
使用 .codehash
获取该代码的 Keccak-256 哈希(作为 bytes32
)。
请注意,addr.codehash
比使用 keccak256(addr.code)
更便宜。
警告
如果与 addr
关联的账户为空或不存在(即没有代码、零余额和零 nonce,如 EIP-161 所定义),则 addr.codehash
的输出可能为 0
。
如果账户没有代码但有非零余额或 nonce,则 addr.codehash
将输出空数据的 Keccak-256 哈希(即 keccak256("")
,其值等于 c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470
),如 EIP-1052 所定义。
备注
所有合约都可以转换为 address
类型,因此可以使用 address(this).balance
查询当前合约的余额。
合约类型
每个 contract 定义其自己的类型。
你可以隐式地将合约转换为它们继承的合约。
合约可以显式地转换为 address
类型和从 address
类型转换。
只有当合约类型具有接收或可支付的回退函数时,才能显式转换为 address payable
类型。转换仍然使用 address(x)
进行。如果合约类型没有接收或可支付的回退函数,则可以使用 payable(address(x))
进行转换。
你可以在 address type 部分找到更多信息。
备注
在 0.5.0 版本之前,合约直接派生自地址类型,并且 address
和 address payable
之间没有区别。
如果你声明一个合约类型的局部变量(MyContract c
),则可以在该合约上调用函数。请确保从相同的合约类型的某个地方进行赋值。
你还可以实例化合约(这意味着它们是新创建的)。你可以在 ‘Contracts via new’ 部分找到更多详细信息。
合约的数据表示与 address
类型相同,并且该类型也用于 ABI。
合约不支持任何运算符。
合约类型的成员是合约的外部函数,包括任何标记为 public
的状态变量。
对于合约 C
,你可以使用 type(C)
访问有关合约的 type information。
定长字节数组
值类型 bytes1
、bytes2
、bytes3
、…、bytes32
持有从一个到最多 32 的字节序列。
运算符:
比较:
<=
、<
、==
、!=
、>=
、>``(评估为 ``bool
)移位运算符:
<<``(左移)、
>>``(右移)索引访问:如果
x
是类型bytesI
,则x[k]
对于0 <= k < I
返回第k
个字节(只读)。
移位运算符的右操作数必须是无符号整数类型(但返回左操作数的类型),表示要移位的位数。 进行有符号整数位移运算将产生编译错误。
成员:
.length
返回字节数组的长度(只读)。
备注
类型 bytes1[]
是字节数组,但由于填充规则,每个元素浪费 31 字节的空间(存储中除外)。最好使用 bytes
类型。
备注
在 0.8.0 版本之前,byte
曾是 bytes1
的别名。
地址字面量
通过地址校验和测试的十六进制字面量,例如 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF
是 address
类型。
长度在 39 到 41 位之间且未通过校验和测试的十六进制字面量会产生错误。你可以在前面添加(对于整数类型)或在后面添加零(对于 bytesNN 类型)以消除错误。
备注
混合大小写的地址校验和格式在 EIP-55 中定义。
有理数和整数字面量
整数字面量由范围为 0-9 的数字序列组成。它们被解释为十进制。例如,69
表示六十九。
Solidity 中不存在八进制字面量,前导零是无效的。
十进制分数字面量由 .
和小数点后至少一个数字组成。
示例包括 .1
和 1.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``(对于负字面量)类型中执行,而不管右侧(指数)操作数的类型。
警告
在 Solidity 0.4.0 版本之前,整数字面量上的除法会截断,但现在会转换为有理数,即 5 / 2
不等于 2
,而是 2.5
。
备注
Solidity 为每个有理数都有一个数字字面量类型。
整数字面量和有理数字面量属于数字字面量类型。
此外,所有数字字面量表达式(即仅包含数字字面量和运算符的表达式)都属于数字字面量类型。
因此,数字字面量表达式 1 + 2
和 2 + 1
都属于有理数三的同一数字字面量类型。
备注
数字字面量表达式在与非字面量表达式一起使用时会转换为非字面量类型。
不考虑类型,下面赋值给 b
的表达式的值计算为整数。
因为 a
的类型是 uint128
,所以表达式 2.5 + a
必须具有适当的类型。
由于 2.5
和 uint128
没有共同类型,Solidity 编译器不接受此代码。
uint128 a = 1;
uint128 b = 2.5 + a + 0.5;
字符串字面量和类型
字符串字面量用双引号或单引号("foo"
或 'bar'
)书写,并且可以拆分为多个连续部分("foo" "bar"
等价于 "foobar"
),这在处理长字符串时很有帮助。
它们并不意味着像 C 中那样的尾随零;"foo"
代表三个字节,而不是四个。
与整数字面量一样,它们的类型可以变化,但如果适合,它们可以隐式转换为 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
、\f
和 \v
。
它们在其他语言中常见,但在实践中很少需要。
如果确实需要,可以通过十六进制转义插入它们,即 \x08
、\x0c
和 \x0b
,就像任何其他 ASCII 字符一样。
以下示例中的字符串长度为十个字节。
它以换行字节开头,后跟一个双引号、一个单引号、一个反斜杠字符,然后(没有分隔符)是字符序列 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 literals,但不能隐式转换为 string
类型。
枚举
枚举是创建用户定义类型的一种方式。它们可以显式地转换为和从所有整数类型转换,但不允许隐式转换。 显式从整数转换时会在运行时检查值是否在枚举范围内,否则会导致 Panic error。 枚举至少需要一个成员,声明时的默认值是第一个成员。枚举不能有超过 256 个成员。
数据表示与 C 中的枚举相同:选项由从 0
开始的后续无符号整数值表示。
使用 type(NameOfEnum).min
和 type(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.wrap
和 FixedMath.toUFixed256x18
具有相同的签名,但执行两种非常不同的操作:
UFixed256x18.wrap
函数返回一个 UFixed256x18
,其数据表示与输入相同,而 toUFixed256x18
返回一个 UFixed256x18
,其数值相同。
函数类型
函数类型是函数的类型。函数类型的变量可以从函数中赋值,函数类型的函数参数可以用于将函数传递给函数调用并从中返回函数。 函数类型有两种类型 - 内部 和 外部 函数:
内部函数只能在当前合约内部调用(更具体地说,在当前代码单元内部,这也包括内部库函数和继承函数),因为它们不能在当前合约的上下文之外执行。 调用内部函数是通过跳转到其入口标签来实现的,就像在内部调用当前合约的函数一样。
外部函数由地址和函数签名组成,可以通过外部函数调用传递和返回。
函数类型的表示如下:
function (<parameter types>) {internal|external} [pure|view|payable] [returns (<return types>)]
与参数类型不同,返回类型不能为空 - 如果函数类型不应返回任何内容,则必须省略整个 returns (<return types>)
部分。
默认情况下,函数类型是内部的,因此可以省略 internal
关键字。
请注意,这仅适用于函数类型。对于在合约中定义的函数,必须显式指定可见性,它们没有默认值。
类型转换:
函数类型 A
仅在以下情况下可以隐式转换为函数类型 B
:
它们的参数类型相同,它们的返回类型相同,它们的内部/外部属性相同,并且 A
的状态可变性比 B
更严格。
特别是:
pure
函数可以转换为view
和non-payable
函数view
函数可以转换为non-payable
函数payable
函数可以转换为non-payable
函数
其他函数类型之间的转换则不可以。
关于 payable
和 non-payable
的规则可能有点令人困惑,但本质上,如果一个函数是 payable
,这意味着它也接受零以太的支付,因此它也是 non-payable
。
另一方面,non-payable
函数将拒绝发送给它的以太,因此 non-payable
函数不能转换为 payable
函数。
为了澄清,拒绝以太比不拒绝以太更严格。这意味着你可以用非支付函数覆盖支付函数,但不能反过来。
此外,当你定义 non-payable
函数指针时,编译器并不强制要求指向的函数实际上会拒绝以太。
相反,它强制要求函数指针永远不能用于发送以太。这使得可以将 payable
函数指针分配给 non-payable
函数指针,确保这两种类型的行为相同,即都不能用于发送以太。
如果函数类型变量未初始化,调用它会导致 Panic error。如果在对其使用 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 约定用于它们的选择器。
在接口中声明的函数没有定义,因此指向它们也没有意义。
成员:
外部(或公共)函数具有以下成员:
.address
返回函数的合约地址。.selector
返回 ABI 函数选择器
备注
public(或 external)函数曾经有额外的成员 .gas(uint)
和 .value(uint)
。
这些在 Solidity 0.6.2 中被弃用,并在 Solidity 0.7.0 中被移除。
请改用 {gas: ...}
和 {value: ...}
来分别指定发送到函数的 gas 量或 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
、storage
和 calldata
。Calldata 是一个不可修改的、非持久的区域,用于存储函数参数,其行为大致类似于内存。
备注
transient
目前尚不支持作为引用类型的数据位置。
备注
如果可以,尽量使用 calldata
作为数据位置,因为这将避免复制并确保数据无法被修改。
具有 calldata
数据位置的数组和结构体也可以从函数返回,但无法分配此类类型。
备注
在版本 0.6.9 之前,引用类型参数的数据位置仅限于外部函数中的 calldata
、公共函数中的 memory
,以及内部和私有函数中的 memory
或 storage
。
现在 memory
和 calldata
在所有函数中均被允许,无论其可见性如何。
备注
在版本 0.5.0 之前,数据位置可以省略,并且会根据变量类型、函数类型等默认到不同的位置,但现在所有复杂类型必须明确给出数据位置。
数据位置与赋值行为
数据位置不仅与数据的持久性相关,还与赋值的语义相关:
在
storage
和memory
之间(或从calldata
)的赋值总是会创建一个独立的副本。从
memory
到memory
的赋值仅创建引用。这意味着对一个内存变量的更改在所有其他引用相同数据的内存变量中也是可见的。从
storage
到 本地 存储变量的赋值也仅赋值一个引用。所有其他对
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,并在内存中创建一个独立的临时拷贝
}
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 types。
可以将状态变量数组标记为 public
,并让 Solidity 创建一个 getter。
数字索引成为 getter 的必需参数。
访问超出数组长度的元素会导致断言失败。
可以使用方法 .push()
和 .push(value)
在动态大小数组的末尾添加新元素,其中 .push()
会添加一个零初始化的元素并返回对其的引用。
备注
动态大小数组只能在 storage 中调整大小。 在内存中,此类数组可以具有任意大小,但一旦分配,大小无法更改。
bytes
和 string
也是数组
类型为 bytes
和 string
的变量是特殊数组。
bytes
类型类似于 bytes1[]
,但在 calldata 和内存中紧密打包。
string
等同于 bytes
,但不允许长度或索引访问。
Solidity 没有字符串操作函数,但有第三方字符串库。你还可以通过它们的 keccak256 哈希比较两个字符串,
使用 keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2))
并使用 string.concat(s1, s2)
连接两个字符串。
你应该使用 bytes
而不是 bytes1[]
,因为它更便宜,因为在 memory
中使用 bytes1[]
会在元素之间添加 31 个填充字节。
请注意,在 storage
中,由于紧密打包,填充是不存在的,参见 bytes and string。
一般来说,对于任意长度的原始字节数据使用 bytes
,对于任意长度的字符串(UTF-8)数据使用 string
。
如果可以将长度限制为一定数量的字节,始终使用值类型 bytes1
到 bytes32
中的一个,因为它们更便宜。
备注
如果你想访问字符串 s
的字节表示,使用 bytes(s).length
/ bytes(s)[7] = 'x';
。
请记住你正在访问 UTF-8 表示的低级字节,而不是单个字符。
函数 bytes.concat
和 string.concat
你可以使用 string.concat
连接任意数量的 string
值。
该函数返回一个单一的 string memory
数组,包含参数的内容而不进行填充。
如果你想使用其他类型的参数,而这些类型不能隐式转换为 string
,你需要先将它们转换为 string
。
类似地,bytes.concat
函数可以连接任意数量的 bytes
或 bytes1 ... bytes32
值。
该函数返回一个单一的 bytes memory
数组,包含参数的内容而不进行填充。
如果你想使用字符串参数或其他类型,而这些类型不能隐式转换为 bytes
,你需要先将它们转换为 bytes
或 bytes1
/…/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.concat
或 bytes.concat
而不带参数,它们将返回一个空数组。
分配内存数组
可以使用 new
操作符创建动态长度的内存数组。
与存储数组不同,内存数组 不能 调整大小(例如,.push
成员函数不可用)。
你必须提前计算所需的大小或创建一个新的内存数组并复制每个元素。
与 Solidity 中的所有变量一样,新分配数组的元素始终初始化为 default value。
// 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] memory
// 不能转换为 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:
数组有一个
length
成员,包含其元素的数量。 内存数组的长度是固定的(但动态的,即可以依赖于运行时参数),一旦创建就不能更改。- push():
动态存储数组和
bytes
(不是string
)有一个成员函数叫做push()
,你可以用它在数组末尾附加一个零初始化的元素。 它返回对该元素的引用,因此可以像x.push().t = 2
或x.push() = b
一样使用。- push(x):
动态存储数组和
bytes
(不是string
)有一个成员函数叫做push(x)
,你可以用它在数组末尾附加一个给定的元素。 该函数不返回任何内容。- pop():
动态存储数组和
bytes
(不是string
)有一个成员函数叫做pop()
,你可以用它从数组末尾移除一个元素。 这也会隐式调用 delete 来删除被移除的元素。该函数不返回任何内容。
备注
通过调用 push()
增加存储数组的长度具有恒定的 gas 成本,因为存储总是被零初始化,而通过调用 pop()
减少长度的成本取决于被移除元素的“大小”。
如果该元素是一个数组,成本可能非常高,因为它包括显式清除被移除元素,类似于调用 delete。
备注
要在外部(而不是公共)函数中使用数组的数组,你需要激活 ABI 编码器 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;
// 请注意,以下内容不是一对动态数组,
// 而是一个对的动态数组(即固定大小为二的数组)。
// 在 Solidity 中,T[k] 和 T[] 始终是元素类型为 T 的数组,即使 T 本身是一个数组。
// 因此,bool[2][] 是一个动态数组,其元素是 bool[2]。
// 这与其他语言(如 C)不同。
// 所有状态变量的数据位置为存储。
bool[2][] pairsOfFlags;
// newPairs 存储在内存中
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 {
// 将对 ``s`` 的引用存储在 ``g`` 中
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 {
// 字节数组("bytes")不同,因为它们存储时没有填充,
// 但可以被视为与 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` 创建:
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;
}
}
悬空的存储数组元素引用
在处理存储数组时,你需要注意避免悬空引用。
悬空引用是指指向不再存在或已移动而未更新引用的内容的引用。
例如,如果你将对数组元素的引用存储在局部变量中,然后从包含数组中 .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()
不会显式地将零写入存储,因此 s
的最后一个元素在那次 push()
之后将具有长度 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
数组元素的引用时,你需要特别小心,因为对字节数组的 .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]
,其中 start
和 end
是结果为 uint256 类型(或隐式可转换为它)的表达式。
切片的第一个元素是 x[start]
,最后一个元素是 x[end - 1]
。
如果 start
大于 end
或 end
大于数组的长度,将抛出异常。
start
和 end
都是可选的:start
默认为 0
,end
默认为数组的长度。
数组切片没有任何成员。它们隐式可转换为其基础类型的数组并支持索引访问。 索引访问不是在基础数组中的绝对位置,而是相对于切片的起始位置。
数组切片没有类型名称,这意味着没有变量可以将数组切片作为类型,它们仅存在于中间表达式中。
备注
目前,数组切片仅可使用于 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];
// 创建一个新的临时内存结构,使用给定值初始化
// 并将其复制到存储中。
// 注意,你也可以使用 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
可以是任何内置值类型、bytes
、string
,或任何合约或枚举类型。
其他用户定义或复杂类型,如映射、结构体或数组类型是不允许的。
ValueType
可以是任何类型,包括映射、数组和结构体。 KeyName
和 ValueName
是可选的(因此 mapping(KeyType => ValueType)
也可以使用),可以是任何有效的标识符,但不能是类型。
可以将映射视为 哈希表,它们在逻辑上被初始化为每个可能的键都存在,并映射到一个字节表示全为零的值,即类型的 默认值。 相似之处到此为止,键数据并不存储在映射中,,仅其 keccak256 哈希被用来查找值。
因此,映射没有长度或键或值被设置的概念,因此不能在没有关于分配键的额外信息的情况下被擦除(见 清除映射)。
映射只能具有 storage
的数据位置,因此允许作为状态变量、作为函数中的存储引用类型,或作为库函数的参数。
它们不能作为公开可见的合约函数的参数或返回参数。这些限制同样适用于包含映射的数组和结构体。
可以将映射类型的状态变量标记为 public
,Solidity 会为你创建一个 getter。
KeyType
成为 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
是另一个映射类型内部的映射类型的示例。
在下面的示例中,为映射提供了可选的 KeyName
和 ValueName
。这不会影响任何合约功能或字节码,它仅为映射的 getter 的输入和输出设置 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
,其中 x
是 uint8
类型,而 z
的类型是 uint32
。
在这些情况下,将使用以下机制来确定操作计算的类型(在溢出的情况下这很重要)和运算符结果的类型:
如果右操作数的类型可以隐式转换为左操作数的类型,则使用左操作数的类型,
如果左操作数的类型可以隐式转换为右操作数的类型,则使用右操作数的类型,
否则,操作是不允许的。
如果其中一个操作数是 literal number,它首先会被转换为其“移动类型”,即可以容纳该值的最小类型(相同位宽的无符号类型被视为比有符号类型“更小”)。如果两个都是字面量数字,则操作将在有效的无限精度下计算,即表达式会被评估到所需的任何精度,以确保在与非字面量类型一起使用时没有损失。
运算符的结果类型与操作执行的类型相同,比较运算符的结果类型始终为 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
和 ++a
对 a
的影响相同,但返回更改后的值。
delete
delete a
将类型的初始值分配给 a
。即对于整数,它等价于 a = 0
,但它也可以用于数组,在这种情况下,它分配一个长度为零的动态数组或一个所有元素都设置为其初始值的相同长度的静态数组。
delete a[x]
删除数组中索引为 x
的项,并保持所有其他元素和数组的长度不变。这尤其意味着它在数组中留下了一个空隙。如果你计划删除项, mapping 可能是更好的选择。
对于结构体,它会分配一个所有成员重置的结构体。
换句话说,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,并不影响数据
delete data; // 将 data 设为 0,并不影响数据
uint[] storage y = dataArray;
delete dataArray;
// 将 dataArray.length 设为 0,但由于 uint[] 是一个复杂的对象,y 也将受到影响,
// 因为它是一个存储位置是 storage 的对象的别名。
// 另一方面:"delete y" 是非法的,引用了 storage 对象的局部变量只能由已有的 storage 对象赋值。
assert(y.length == 0);
}
}
运算符优先级顺序
以下是运算符的优先级顺序,按求值顺序列出。
Precedence |
Description |
Operator |
---|---|---|
1 |
Postfix increment and decrement |
|
New expression |
|
|
Array subscripting |
|
|
Member access |
|
|
Function-like call |
|
|
Parentheses |
|
|
2 |
Prefix increment and decrement |
|
Unary minus |
|
|
Unary operations |
|
|
Logical NOT |
|
|
Bitwise NOT |
|
|
3 |
Exponentiation |
|
4 |
Multiplication, division and modulo |
|
5 |
Addition and subtraction |
|
6 |
Bitwise shift operators |
|
7 |
Bitwise AND |
|
8 |
Bitwise XOR |
|
9 |
Bitwise OR |
|
10 |
Inequality operators |
|
11 |
Equality operators |
|
12 |
Logical AND |
|
13 |
Logical OR |
|
14 |
Ternary operator |
|
Assignment operators |
|
|
15 |
Comma operator |
|
基本类型之间的转换
隐式转换
隐式类型转换是在某些情况下由编译器自动应用的,例如在赋值时、将参数传递给函数时以及应用运算符时。 一般来说,如果语义上合理且没有信息丢失,则值类型之间可以进行隐式转换。
例如,uint8
可以转换为 uint16
,而 int128
可以转换为 int256
,但 int8
不能转换为 uint256
,因为 uint256
不能表示如 -1
这样的值。
如果运算符应用于不同类型,编译器会尝试将其中一个操作数隐式转换为另一个操作数的类型(赋值时也是如此)。 这意味着操作总是在其中一个操作数的类型下执行。
有关可能的隐式转换的更多详细信息,请查阅关于类型本身的部分。
在下面的示例中,y
和 z
,加法的操作数,类型不同,但 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
)。
如果数组的长度超过目标固定长度的 bytes 类型,则会在末尾截断。
如果数组的长度小于目标类型,则会在末尾用零填充。
// 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]); // 也用零填充
return (b, b1);
}
}
字面量与基本类型之间的转换
整数类型
十进制和十六进制数字字面量可以隐式转换为任何足够大的整数类型,以便不发生截断:
uint8 a = 12; // 可行
uint32 b = 1234; // 可行
uint16 c = 0x123456; // 失败,因为它必须截断为 0x3456
备注
在版本 0.8.0 之前,任何十进制或十六进制数字字面量都可以显式转换为整数类型。从 0.8.0 开始,这种显式转换与隐式转换一样严格,即仅在字面量适合结果范围时才允许。
定长字节数组
十进制字面常量不能隐式转换为定长字节数组。十六进制字面常量可以是,但仅当十六进制数字大小完全符合定长字节数组长度。 不过零值例外,零的十进制和十六进制字面常量都可以转换为任何定长字节数组类型: .. code-block:: solidity
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 e = "x"; // 可行
bytes2 f = "xyz"; // 不可行
地址
如 地址字面量 中所述,正确大小的十六进制字面量通过校验和测试后为 address
类型。
没有其他字面量可以隐式转换为 address
类型。
显式转换为 address
仅允许从 bytes20
和 uint160
。
address a
可以通过 payable(a)
显式转换为 address payable
。
备注
在版本 0.8.0 之前,可以从任何整数类型(无论大小、带符号或无符号)显式转换为 address
或 address payable
。
从 0.8.0 开始,仅允许从 uint160
进行转换。