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
是 transfer
的低级对应部分。如果执行失败,当前的合约不会因异常而停止,但 send
会返回 false
。
使用 send
有一些危险:如果调用堆栈深度为1024,传输就会失败(这可以由调用者强制执行),
如果接收者的气体耗尽,也会失败。因此,为了安全地进行以太币转账,
一定要检查 send
的返回值,或者使用 transfer
,甚至使用更好的方式:
使用收款人提款的模式。
call
, delegatecall
和 staticcall
为了与不遵守ABI的合约对接,或者为了更直接地控制编码,
我们提供了 call
, delegatecall
和 staticcall
函数。
它们都接受一个 bytes memory
参数,并返回成功条件(作为一个 bool
)
和返回的数据( bytes memory
)。
函数 abi.encode
, abi.encodePacked
, abi.encodeWithSelector
和 abi.encodeWithSignature
可以用来编码结构化的数据。
open in Remix
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
修饰器来调整所提供的气体:
open in Remix
address(nameReg).call{gas: 1000000}(abi.encodeWithSignature("register(string)", "MyName"));
同样,所提供的以太值也可以被控制:
open in Remix
address(nameReg).call{value: 1 ether}(abi.encodeWithSignature("register(string)", "MyName"));
最后,这些修饰器可以合并。它们的顺序并不重要:
open in Remix
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
中可用。
最好避免在您的智能合约代码中依赖硬编码的气体值,无论状态是读出还是写入,
因为这可能有很多隐患。另外,对气体的访问在未来可能会改变。
code
和 codehash
您可以查询任何智能合约的部署代码。使用 .code
获得作为 bytes memory
的EVM字节码,
这可能是空的。使用 .codehash
获得该代码的Keccak-256哈希值(作为 bytes32
)。
注意,使用 addr.codehash
比 keccak256(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 版本之前,合约直接从地址类型派生出来,
并且在 address
和 address payable
之间没有区别。
如果您声明了一个本地类型的变量( MyContract c
),您可以调用该合约上的函数。
注意要从相同合约类型的地方将其赋值。
您也可以实例化合约(这意味着它们是新创建的)。
您可以在 '通过关键字new创建合约' 部分找到更多细节。
合约的数据表示与 address
类型相同,该类型也用于 ABI。
合约不支持任何运算符。
合约类型的成员是合约的外部函数,包括任何标记为 public
的状态变量。
对于一个合约 C
,您可以使用 type(C)
来访问
关于该合约的 类型信息 。
定长字节数组
值类型 bytes1
, bytes2
, bytes3
, ..., bytes32
代表从1到32的字节序列。
比较运算符:<=, <, ==, !=, >=, > (返回布尔型)
比较运算符: <=
, <
, ==
, !=
, >=
, >
(返回 bool
)
位运算符: &
, |
, ^
(按位异或), ~
(按位取反)
移位运算符: <<
(左移位), >>
(右移位)
索引访问: 如果 x
是 bytesI
类型,那么当 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 是无效的。
小数字面常数由 .
和小数点后的至少一个数字组成。例如, .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
(负数数值字面常数)类型中进行。
无论右(指数)操作数的类型如何。
在 0.4.0 版本之前,Solidity 中整数字的除法会被截断,但现在它转换为一个有理数,即 5 / 2
不等于 2
,而是 2.5
。
Solidity 对每个有理数都有对应的数值字面常数类型。
整数字面常数和有理数字面常数都属于数值字面常数类型。
除此之外,所有的数值字面常数表达式(即只包含数值字面常数和运算符的表达式)都属于数值字面常数类型。
因此数值字面常数表达式 1 + 2
和 2 + 1
的结果跟有理数3的数值字面常数类型相同。
数字字面表达式一旦与非字面表达式一起使用,就会被转换为非字面类型。
不考虑类型,下面分配给 b
的表达式的值被评估为一个整数。
因为 a
的类型是 uint128
,所以表达式 2.5 + a
必须有一个合适的类型。
由于 2.5
和 uint128
的类型没有共同的类型,Solidity编译器不接受这段代码。
open in Remix
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
, \f
和 v
。
它们在其他语言中通常是可用的,但在实践中很少需要。
如果您确实需要它们,仍然可以通过十六进制转义插入,
即分别为 \x08
, x0c
和 \x0b
,就像其他ASCII字符一样。
下面例子中的字符串的长度为10个字节。
它以一个换行字节开始,接着是一个双引号,一个单引号,一个反斜杠字符,
然后(没有分隔符)是字符序列 abcdef
。
open in Remix
"\n\"\'\\abc\
def"
任何非换行的 Unicode 行结束符(即LF, VF, FF, CR, NEL, LS, PS)都被认为是字符串字面的结束。
换行只在字符串字面内容前面没有 \
的情况下终止。
Unicode 字面量
普通字符串字面常数只能包含ASCII码,而 Unicode 字面常数 - 以关键字 unicode
为前缀 - 可以包含任何有效的UTF-8序列。
它们也支持与普通字符串字面意义相同的转义序列。
open in Remix
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).min
和 type(NameOfEnum).max
您可以得到给定枚举的最小值和最大值。
open in Remix
// 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位小数的十进制定点类型和一个最小的库来对该类型做算术运算。
open in Remix
// 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
。
函数类型
函数类型是一种表示函数的类型。可以将一个函数赋值给另一个函数类型的变量,
也可以将一个函数作为参数进行传递,还能在函数调用中返回函数类型变量。
函数类型有两类:- 内部(internal) 函数和 外部(external) 函数:
内部函数只能在当前合约内被调用(更具体来说,
在当前代码块内,包括内部库函数和继承的函数中),
因为它们不能在当前合约上下文的外部被执行。
调用一个内部函数是通过跳转到它的入口标签来实现的,
就像在当前合约的内部调用一个函数。
外部函数由一个地址和一个函数签名组成,可以通过外部函数调用传递或者返回。
函数类型表示成如下的形式:
open in Remix
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 约定。
接口中声明的函数没有定义,所以指向它们也没有意义。
外部(或公共)函数有以下成员:
.address
返回该函数的合约地址。
.selector
返回 ABI 函数选择器
外部(或公共)函数曾经有额外的成员 .gas(uint)
和 .value(uint)
。
这些在 Solidity 0.6.2 中被废弃,并在 Solidity 0.7.0 中被移除。取而代之的是
使用 {gas: ...}
和 {value: ...}
来分别指定发送到函数的气体量或以太(wei为单位)量。
参见 外部函数调用 以获得更多信息。
以下例子展示如何使用这些成员:
open in Remix
// 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}();
以下例子展示如何使用内部函数类型:
open in Remix
// 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
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
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;
另一个使用外部函数类型的例子:
open in Remix
// 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是一个不可修改的、非持久性的区域,用于存储函数参数,其行为主要类似于memory。
如果可以的话,尽量使用 calldata
作为数据位置,因为这样可以避免复制,
也可以确保数据不能被修改。使用 calldata
数据位置的数组和结构也可以从函数中返回,
但不可能分配这种类型。
在0.6.9版本之前,引用型参数的数据位置被限制在外部函数中的 calldata
,
公开函数中的 memory
,以及内部和私有函数中的 memory
或 storage
。
现在 memory
和 calldata
在所有函数中都被允许使用,无论其可见性如何。
在0.5.0版本之前,数据位置可以省略,并且会根据变量的种类、函数类型等默认为不同的位置,
但现在所有的复杂类型都必须给出一个明确的数据位置。
数据位置和分配行为
数据位置不仅与数据的持久性有关,而且也与分配的语义有关:
在 storage
和 memory
之间的分配(或从 calldata
中分配) 总是创建一个独立的拷贝。
从 memory
到 memory
的赋值只创建引用。
这意味着对一个内存变量的改变在所有其他引用相同数据的内存变量中也是可见的。
从 storage
到 local 存储变量的赋值也只赋值一个引用。
所有其他对 storage
的赋值总是拷贝的。
这种情况的例子是对状态变量或存储结构类型的局部变量成员的赋值,
即使局部变量本身只是一个引用。
open in Remix
// 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 {}
例如,一个由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()
追加一个零初始化的元素并返回它的引用。
动态大小的数组只能在存储中调整大小。
在内存中,这样的数组可以是任意大小的,但是一旦分配了数组,就不能改变数组的大小。
bytes
和 string
类型的数组
bytes
和 string
类型的变量是特殊的数组。 bytes
类似于 bytes1[]
,
但它在 calldata 中会被“紧打包”(译者注:将元素连续地存在一起,不会按每 32 字节一单元的方式来存放)。
string
与 bytes
相同,但不允许用长度或索引来访问。
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
。
如果您能将长度限制在一定的字节数,总是使用 bytes1
到 bytes32
中的一种值类型,因为它们更便宜。
如果想要访问以字节表示的字符串 s
,
请使用 bytes(s).length
/ bytes(s)[7] = 'x';
。
注意这时您访问的是 UTF-8 形式的低级 bytes 类型,而不是单个的字符。
函数 bytes.concat
和 string.concat
您可以使用 string.concat
连接任意数量的 string
值。
该函数返回一个单一的 string memory
数组,其中包含没有填充的参数内容。
如果您想使用不能隐式转换为 string
的其他类型的参数,您需要先将它们转换为 string
。
同样, bytes.concat
函数可以连接任意数量的 bytes
或 bytes1 ... bytes32
值。
该函数返回一个单一的 bytes memory
数组,其中包含没有填充的参数内容。
如果您想使用字符串参数或其他不能隐式转换为 bytes
的类型,
您需要先将它们转换为 bytes
或 bytes1
/.../ bytes32
。
open in Remix
// 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中的所有变量一样,新分配的数组元素总是以 默认值 进行初始化。
open in Remix
// 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
。
open in Remix
// 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]
。
由于不同类型的固定大小的内存数组不能相互转换(即使基类可以),
如果您想使用二维数组字面常数,您必须总是明确指定一个共同的基类:
open in Remix
// 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;
固定大小的内存数组不能分配给动态大小的内存数组,也就是说,以下情况是不可能的:
open in Remix
// 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中数组的传递方式,它产生了一些复杂的问题。
如果您想初始化动态大小的数组,您必须分配各个元素:
open in Remix
// 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;
内存memory数组的大小就是固定的(但却是动态的,也就是说,它依赖于运行时的参数)。
push():动态存储数组和 bytes
(不是 string
)有一个叫 push()
的成员函数,
您可以用它在数组的末尾追加一个零初始化的元素。它返回一个元素的引用,
因此可以像 x.push().t = 2
或 x.push() = b
那样使用。
push(x):动态存储数组和 bytes
(不是 string
)有一个叫 push(x)
的成员函数,
您可以用它在数组的末端追加一个指定的元素。该函数不返回任何东西。
pop():动态存储数组和 bytes
(不是 string
)有一个叫 pop()
的成员函数,
您可以用它来从数组的末端移除一个元素。
这也隐含地在被删除的元素上调用 delete。该函数不返回任何东西。
通过调用 push()
增加存储数组的长度有恒定的气体成本,因为存储是零初始化的,
而通过调用 pop()
减少长度的成本取决于被移除元素的 "大小"。
如果该元素是一个数组,它的成本可能非常高,
因为它包括明确地清除被移除的元素,类似于对它们调用 delete。
要在外部(而不是公开)函数中使用数组的数组,
您需要激活ABI coder v2。
在Byzantium之前的EVM版本中,不可能访问从函数调用返回的动态数组。
如果您调用返回动态数组的函数,请确保使用设置为Byzantium模式的EVM。
open in Remix
// 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()
,就可能发生悬空引用:
open in Remix
// 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 不允许在存储中声明对值类型的引用。
这类显式的悬空引用被限制在嵌套引用类型中。然而,
当在数组赋值中使用复杂表达式时,悬空引用也会短暂发生:
open in Remix
// 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()
操作可能会 在存储中从短布局切换到长布局。
open in Remix
// 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解码在函数参数中传递的二级数据很有用:
open in Remix
// 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 提供了一种以结构形式定义新类型的方法,以下是一个结构体使用的示例:
open in Remix
// 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
可以是任何内置的值类型, 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
,将一个Ethereum地址映射到一个无符号整数值。
由于 uint
是一个值类型,getter 返回一个与该类型相匹配的值,
您可以在 MappingUser
合约中看到它返回指定地址对应的值。
open in Remix
// 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 在 ABI 中设置输入和输出的 name
字段。
open in Remix
// 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
来记录其他人可以从你的账户中提取的金额。
open in Remix
// 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
函数对所有的值进行递归调用去累加这些值。
open in Remix
// 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
的类型为 int32
。
在这种情况下,下面的机制将被用来确定计算操作的类型(这在溢出的情况下很重要)和操作结果的类型:
如果右操作数的类型可以隐式转换为左操作数的类型,则使用左操作数的类型,
如果左操作数的类型可以隐式转换为右操作数的类型,则使用右操作数的类型,
否则的话,该操作不被允许。
如果其中一个操作数是 字面常数,
它首先被转换为其 “移动类型(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
和 ++a
对 a
有同样的作用,但返回改变后的值。
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
本身,而不是它之前引用的值。
open in Remix
// 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);
编译器会自动应用隐式类型转换。一般来说,如果在语义上有意义,
并且不会丢失信息,那么值-类型之间的隐式转换是可能的。
例如, uint8
可以转换为 uint16
, int128
可以转换为 int256
,
但是 int8
不能转换为 uint256
,因为 uint256
不能容纳 -1
这样的值。
如果一个运算符被应用于不同的类型,
编译器会尝试将其中一个操作数隐含地转换为另一个的类型(对于赋值也是如此)。
这意味着操作总是以其中一个操作数的类型进行。
关于哪些隐式转换是可能的,请参考关于类型本身的章节。
在下面的例子中, y
和 z
,即加法的操作数,没有相同的类型,
但是 uint8
可以隐式转换为 uint16
,反之则不行。正因为如此,
y
被转换为 z
的类型,然后在 uint16
类型中进行加法。
结果表达式 y + z
的类型是 uint16
。
因为它被分配到一个 uint32
类型的变量中,所以在加法后又进行了一次隐式转换。
open in Remix
uint8 y;
uint16 z;
uint32 x = y + z;
在这个代码片断的最后, x
变成 0xfffff..fd
的值(64个十六进制字符),
这在256位的二进制补码中表示是-3。
如果一个整数被明确地转换为一个较小的类型,高阶位就会被切断:
open in Remix
uint32 a = 0x12345678;
uint16 b = uint16(a); // b 现在会是 0x5678
如果一个整数被明确地转换为一个更大的类型,它将在左边被填充(即在高阶的一端)。
转换的结果将与原整数比较相等:
open in Remix
uint16 a = 0x1234;
uint32 b = uint32(a); // b 现在会是 0x00001234
assert(a == b);
固定大小的字节类型在转换过程中的行为是不同的。
它们可以被认为是单个字节的序列,转换到一个较小的类型将切断序列:
open in Remix
bytes2 a = 0x1234;
bytes1 b = bytes1(a); // b 现在会是 0x12
如果一个固定大小的字节类型被明确地转换为一个更大的类型,它将在右边被填充。
访问固定索引的字节将导致转换前后的数值相同(如果索引仍在范围内):
open in Remix
bytes2 a = 0x1234;
bytes4 b = bytes4(a); // b 现在会是 0x12340000
assert(a[0] == b[0]);
assert(a[1] == b[1]);
于整数和固定大小的字节数组在截断或填充时表现不同,
只有在整数和固定大小的字节数组具有相同大小的情况下,才允许在两者之间进行显式转换。
如果您想在不同大小的整数和固定大小的字节数组之间进行转换,您必须使用中间转换,
使所需的截断和填充规则明确:
open in Remix
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
)。
如果数组比目标的固定字节类型长,在末端会发生截断的情况。如果数组比目标类型短,它将在末尾被填充零。
open in Remix
// 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);
十进制数字字面常数不能被隐含地转换为固定大小的字节数组。
十六进制数字字面常数是可以的,但只有当十六进制数字的数量正好符合字节类型的大小时才可以。
但是有一个例外,数值为0的十进制和十六进制数字字面常数都可以被转换为任何固定大小的字节类型:
open in Remix
bytes2 a = 54321; // 不允许
bytes2 b = 0x12; // 不允许
bytes2 c = 0x123; // 不允许
bytes2 d = 0x1234; // 可行
bytes2 e = 0x0012; // 可行
bytes4 f = 0; // 可行
bytes4 g = 0x0; // 可行
字符串和十六进制字符串字面常数可以被隐含地转换为固定大小的字节数组,
如果它们的字符数与字节类型的大小相匹配:
open in Remix
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
类型。
只允许从 bytes20
和 uint160
显式转换到 address
。
address a
可以通过 payable(a)
显式转换为 address payable
。
在 0.8.0 版本之前,可以显式地从任何整数类型(任何大小,有符号或无符号)转换为 address
或 address payable
类型。
从 0.8.0 开始,只允许从 uint160
转换。