类型¶
Solidity 是一种静态类型语言,这意味着每个变量(状态变量和局部变量)都需要在编译时指定变量的类型。
Solidity 提供了几种基本类型,并且基本类型可以用来组合出复杂类型。
除此之外,类型之间可以在包含运算符号的表达式中进行交互。 关于各种运算符号,可以参考 运算符优先级 。
“undefined”或“null”值的概念在Solidity中不存在,但是新声明的变量总是有一个 默认值 ,具体的默认值跟类型相关。
要处理任何意外的值,应该使用 错误处理 来恢复整个交易,或者返回一个带有第二个 bool
值的元组表示成功。
值类型¶
以下类型也称为值类型,因为这些类型的变量将始终按值来传递。 也就是说,当这些变量被用作函数参数或者用在赋值语句中时,总会进行值拷贝。
布尔类型¶
bool
:可能的取值为字面常量值 true
和 false
。
运算符:
!
(逻辑非)&&
(逻辑与, “and” )||
(逻辑或, “or” )==
(等于)!=
(不等于)
运算符 ||
和 &&
都遵循同样的短路( short-circuiting )规则。就是说在表达式 f(x) || g(y)
中,
如果 f(x)
的值为 true
,那么 g(y)
就不会被执行,即使会出现一些副作用。
整型¶
int
/ uint
:分别表示有符号和无符号的不同位数的整型变量。
支持关键字 uint8
到 uint256
(无符号,从 8 位到 256 位)以及 int8
到 int256
,以 8
位为步长递增。
uint
和 int
分别是 uint256
和 int256
的别名。
运算符:
比较运算符:
<=
,<
,==
,!=
,>=
,>
(返回布尔值)位运算符:
&
,|
,^
(异或),~
(位取反)移位运算符:
<<
(左移位) ,>>
(右移位)算数运算符:
+
,-
, 一元运算负-
(仅针对有符号整型),*
,/
,%
(取余或叫模运算) ,**
(幂)
对于整形 X
,可以使用 type(X).min
和 type(X).max
去获取这个类型的最小值与最大值。
警告
Solidity中的整数是有取值范围的。 例如 uint32
类型的取值范围是 0
到 2 ** 32-1
。
0.8.0 开始,算术运算有两个计算模式:一个是 “wrapping”(截断)模式或称 “unchecked”(不检查)模式,一个是”checked” (检查)模式。
默认情况下,算术运算在 “checked” 模式下,即都会进行溢出检查,如果结果落在取值范围之外,调用会通过 失败异常 回退。
你也可以通过 unchecked { ... }
切换到 “unchecked”模式,更多可参考 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
,会四舍五入到零,而不是向负无穷。
备注
对于移位操作不会像算术运算那样执行溢出检查,其结果总是被截断。
加、减、乘法运算¶
加法,减法和乘法和通常理解的语义一样,不过有两种模式来应对溢出(上溢及下溢):
默认情况下,算术运算都会进行溢出检查,但是也可以禁用检查,可以通过 unchecked block 来禁用检查,此时会返回截断的结果,更多的详情可以前往链接查看。
备注
溢出的检查功能是在 0.8.0 版本加入的,在此版本之前,请使用 OpenZepplin SafeMath 库。
表达式 -x
相当于 (T(0) - x)
这里 T
是指 x
的类型。 -x
只能应用在有符号型的整数上。
如果 x
为负数, -x
为正数。 由于使用两进制补码表示数据,你还需要小心:
如果有 int x = type(int).min;
, 那 -x
将不在正数取值的范围内。
这意味着这个检测 unchecked { assert(-x == x); }
是可以通过的(即这种情况下,不能假设它的负数会是正数),如果是 checked 模式,则会触发异常。
除法运算¶
除法运算结果的类型始终是其中一个操作数的类型,整数除法总是产生整数。
在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 { … }`` 。
幂运算¶
幂运算仅适用于无符号类型。 结果的类型总是等于基数的类型. 请注意类型足够大以能够容纳幂运算的结果,要么发生潜在的assert异常或者使用截断模式。
备注
在“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 之间的任意数。
ufixed
和 fixed
分别是 ufixed128x19
和 fixed128x19
的别名。
运算符:
比较运算符:
<=
,<
,==
,!=
,>=
,>
(返回值是布尔型)算术运算符:
+
,-
, 一元运算-
, 一元运算+
,*
,/
,%
(取余数)
备注
浮点型(在许多语言中的 float
和 double
类型,更准确地说是 IEEE 754 类型)和定长浮点型之间最大的不同点是,
在前者中整数部分和小数部分(小数点后的部分)需要的位数是灵活可变的,而后者中这两部分的长度受到严格的规定。
一般来说,在浮点型中,几乎整个空间都用来表示数字,但只有少数的位来表示小数点的位置。
地址类型 Address¶
地址类型有两种形式,他们大致相同:
address
:保存一个20字节的值(以太坊地址的大小)。
address payable
:可支付地址,与address
相同,不过有成员函数transfer
和send
。
这种区别背后的思想是 address payable
可以向其发送以太币,而不能先一个普通的 address
发送以太币,例如,它可能是一个智能合约地址,并且不支持接收以太币。
类型转换:
允许从 address payable
到 address
的隐式转换,而从 address
到 address payable
必须显示的转换, 通过 payable(<address>)
进行转换。
.. note:
在0.5版本,执行这种转换的唯一方法是使用中间类型,先转换为 ``uint160`` 如, address payable ap = address(uint160(addr));
address
允许和 uint160
、 整型字面常量、bytes20
及合约类型相互转换。
只能通过 payable(...)
表达式把 address
类型和合约类型转换为 address payable
。
只有能接收以太币的合约类型,才能够进行此转换。例如合约要么有 receive 或可支付的回退函数。
注意 payable(0)
是有效的,这是此规则的例外。
备注
如果你需要 address
类型的变量,并计划发送以太币给这个地址,那么声明类型为 address payable
可以明确表达出你的需求。
同样,尽量更早对他们进行区分或转换。
运算符:
<=
,<
,==
,!=
,>=
and>
警告
如果将使用较大字节数组类型转换为 address
,例如 bytes32
,那么 address
将被截断。
为了减少转换歧义,0.4.24及更高编译器版本要求我们在转换中显式截断处理。
以32bytes值 0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCCC
为例, 如果使用 address(uint160(bytes20(b)))
结果是 0x111122223333444455556666777788889999aAaa
, 而使用 address(uint160(uint256(b)))
结果是 0x777788889999AaAAbBbbCcccddDdeeeEfFFfCcCc
。
备注
address
和 address payable
的区别是在 0.5.0 版本引入的,同样从这个版本开始,合约类型不再继承自地址类型,
不过如果合约有可支付的回退( payable fallback )函数或receive 函数,合约类型仍然可以显示转换为
address
或 address payable
。
地址类型成员变量¶
查看所有的成员,可参考 地址成员。
balance
和transfer
成员
可以使用 balance
属性来查询一个地址的余额,
也可以使用 transfer
函数向一个可支付地址(payable address)发送 以太币 (以 wei 为单位):
address x = 0x123;
address myAddress = this;
if (x.balance < 10 && myAddress.balance >= 10) x.transfer(10);
如果当前合约的余额不够多,则 transfer
函数会执行失败,或者如果以太转移被接收帐户拒绝, transfer
函数同样会失败而进行回退。
备注
如果 x
是一个合约地址,它的代码(更具体来说是, 如果有receive函数, 执行 receive 接收以太函数, 或者存在fallback函数,执行 Fallback 回退函数 函数)会跟 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);
此外,为了与不符合 应用二进制接口 的合约交互,于是就有了可以接受任意类型任意数量参数的 call
函数。
这些参数会被打包到以 32 字节为单位的连续区域中存放。
其中一个例外是当第一个参数被编码成正好 4 个字节的情况。
在这种情况下,这个参数后边不会填充后续参数编码,以允许使用函数签名。
address nameReg = 0x72ba7d8e73fe8eb666ea66babc8116a41bfb10e2;
nameReg.call("register", "MyName");
nameReg.call(bytes4(keccak256("fun(uint256)")), a);
警告
所有这些函数都是低级函数,应谨慎使用。 具体来说,任何未知的合约都可能是恶意的,我们在调用一个合约的同时就将控制权交给了它,而合约又可以回调合约,所以要准备好在调用返回时改变相应的状态变量(可参考 可重入 ), 与其他合约交互的常规方法是在合约对象上调用函数(x.f())。
备注
0.5.以前版本的 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)
更便宜。
备注
所有合约都可以转换为 address
类型,因此可以使用 address(this).balance
查询当前合约的余额。
合约类型¶
每一个 contract 定义都有他自己的类型。
您可以隐式地将合约转换为从他们继承的合约。
合约可以显式转换为 address
类型。
只有当合约具有 接收receive函数 或 payable 回退函数时,才能显式和 address payable
类型相互转换
转换仍然使用 address(x)
执行, 如果合约类型没有接收或payable 回退功能,则可以使用 payable(address(x))
转换为 address payable
。
可以参考 地址类型.
备注
在版本0.5.0之前,合约直接从地址类型派生的, 并且 address
和 address payable
之间没有区别。
如果声明一个合约类型的局部变量( MyContract c
),则可以调用该合约的函数。 注意需要赋相同合约类型的值给它。
您还可以实例化合约(即新创建一个合约对象),参考 ‘使用new创建合约’。
合约和 address
的数据表示是相同的, 参考 ABI。
合约不支持任何运算符。
合约类型的成员是合约的外部函数及 public 的 状态变量。
对于合约 C
可以使用 type(C)
获取合约的类型信息,参考 类型信息 。
定长字节数组¶
关键字有:bytes1
, bytes2
, bytes3
, …, bytes32
。
运算符:
比较运算符:
<=
,<
,==
,!=
,>=
,>
(返回布尔型)位运算符:
&
,|
,^
(按位异或),~
(按位取反)移位运算符:
<<
(左移位),>>
(右移位)索引访问:如果
x
是bytesI
类型,那么x[k]
(其中0 <= k < I
)返回第k
个字节(只读)。
该类型可以和作为右操作数的无符号整数类型进行移位运算(但返回结果的类型和左操作数类型相同),右操作数表示需要移动的位数。 进行有符号整数位移运算会引发运行时异常。
成员变量:
.length
表示这个字节数组的长度(只读).
备注
可以将 bytes1[]
当作字节数组使用,但由于填充规则,每个元素会浪费 31 字节(storage存储除外),因此更好地做法是使用 bytes
。
备注
在 0.8.0 之前, byte
用作为 bytes1
的别名。
地址字面常量¶
比如像 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF
这样的通过了地址校验和测试的十六进制字面常量会作为 address
类型。
而没有通过校验测试, 长度在 39 到 41 个数字之间的十六进制字面常量,会产生一个错误,您可以在零前面添加(对于整数类型)或在零后面添加(对于bytesNN类型)以消除错误。
备注
混合大小写的地址校验和格式定义在 EIP-55 中。
有理数和整数字面常量¶
整数字面常量由范围在 0-9 的一串数字组成,表现成十进制。
例如, 69
表示数字 69。
Solidity 中是没有八进制的,因此前置 0 是无效的。
十进制小数字面常量带有一个 .
,至少在其一边会有一个数字。
比如: 1.
, .1
,和 1.3
。
2e10
形式的科学符号也是支持的,尽管指数必须是整数,但底数可以是小数, MeE
的值 M * 10**E
。
比如:, -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
类型中计算的,会溢出。
只要操作数是整型,任意整型支持的运算符都可以被运用在数值字面常量表达式中。 如果两个中的任一个数是小数,则不允许进行位运算。如果指数是小数的话,也不支持幂运算(因为这样可能会得到一个无理数)。
常量作为左(或基)操作数和整数类型的移位和幂运算时总是执行正确的(指数)操作,不管右(指数)操作数的类型如何。
备注
Solidity 对每个有理数都有对应的数值字面常量类型。
整数字面常量和有理数字面常量都属于数值字面常量类型。
除此之外,所有的数值字面常量表达式(即只包含数值字面常量和运算符的表达式)都属于数值字面常量类型。
因此数值字面常量表达式 1 + 2
和 2 + 1
的结果跟有理数三的数值字面常量类型相同。
警告
在早期版本中(0.4.0之前),整数字面常量的除法也会截断,但在现在的版本中,会将结果转换成一个有理数。即 5 / 2
并不等于 2
,而是等于 2.5
。
备注
数值字面常量表达式只要在非字面常量表达式中使用就会转换成非字面常量类型。
在下面的例子中,尽管我们知道 b
的值是一个整数,但 2.5 + a
这部分表达式并不进行类型检查,因此编译不能通过。
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>
(转义实际换行)\\
(反斜杠)\'
(单引号)\"
(双引号)\b
(退格)\f
(换页)\n
(换行符)\r
(回车)\t
(标签 tab)\v
(垂直标签)\xNN
(十六进制转义,见下文)\uNNNN
(unicode 转义,见下文)
\xNN
表示一个 16 进制值,最终转换成合适的字节,而 \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"00112233" hex"44556677"
等同于 hex"0011223344556677"
十六进制字面常量跟 字符串字面常量 很类似,具有相同的转换规则
枚举类型¶
枚举是在Solidity中创建用户定义类型的一种方法。 它们是显示所有整型相互转换,但不允许隐式转换。 从整型显式转换枚举,会在运行时检查整数时候在枚举范围内,否则会导致异常( Panic异常 )。 枚举需要至少一个成员,默认值是第一个成员,枚举不能多于 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;
// Represent a 18 decimal, 256 bit wide fixed point type using a user defined value type.
type UFixed256x18 is uint256;
/// A minimal library to do fixed point operations on UFixed256x18.
library FixedMath {
uint constant multiplier = 10**18;
/// Adds two UFixed256x18 numbers. Reverts on overflow, relying on checked
/// arithmetic on uint256.
function add(UFixed256x18 a, UFixed256x18 b) internal pure returns (UFixed256x18) {
return UFixed256x18.wrap(UFixed256x18.unwrap(a) + UFixed256x18.unwrap(b));
}
/// Multiplies UFixed256x18 and uint256. Reverts on overflow, relying on checked
/// arithmetic on uint256.
function mul(UFixed256x18 a, uint256 b) internal pure returns (UFixed256x18) {
return UFixed256x18.wrap(UFixed256x18.unwrap(a) * b);
}
/// Take the floor of a UFixed256x18 number.
/// @return the largest integer that does not exceed `a`.
function floor(UFixed256x18 a) internal pure returns (uint256) {
return UFixed256x18.unwrap(a) / multiplier;
}
/// Turns a uint256 into a UFixed256x18 of the same value.
/// Reverts if the integer is too large.
function toUFixed256x18(uint256 a) internal pure returns (UFixed256x18) {
return UFixed256x18.wrap(a * multiplier);
}
}
注意 UFixed256x18.wrap
和 FixedMath.toUFixed256x18``的签名相同,但执行的是两个完全不同的操作:
``UFixed256x18.wrap
函数返回一个与输入的数据表示相同的 UFixed256x18
, 而 toUFixed256x18``则返回一个具有相同数值的 ``UFixed256x18
。
函数类型¶
函数类型是一种表示函数的类型。可以将一个函数赋值给另一个函数类型的变量,也可以将一个函数作为参数进行传递,还能在函数调用中返回函数类型变量。 函数类型有两类: - 内部(internal) 函数类型 - 外部(external) 函数类型
内部函数只能在当前合约内被调用(更具体来说,在当前代码块内,包括内部库函数和继承的函数中),因为它们不能在当前合约上下文的外部被执行。 调用一个内部函数是通过跳转到它的入口标签来实现的,就像在当前合约的内部调用一个函数。
外部函数由一个地址和一个函数签名组成,可以通过外部函数调用传递或者返回。
函数类型表示成如下的形式:
function (<parameter types>) {internal|external} [pure|constant|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
函数。
如果当函数类型的变量还没有初始化时就调用它的话会引发一个 Panic 异常。
如果在一个函数被 delete
之后调用它也会发生相同的情况。
如果外部函数类型在 Solidity 的上下文环境以外的地方使用,它们会被视为 function
类型。
该类型将函数地址紧跟其函数标识一起编码为一个 bytes24
类型。。
请注意,当前合约的 public 函数既可以被当作内部函数也可以被当作外部函数使用。
如果想将一个函数当作内部函数使用,就用 f
调用,如果想将其当作外部函数,使用 this.f
。
一个内部函数可以被分配给一个内部函数类型的变量,无论定义在哪里,包括合约和库的私有、内部和public函数,以及自由函数。
另一方面,外部函数类型只与public和外部合约函数兼容。库是不可以的,因为库使用 delegatecall
,并且 他们的函数选择器有不同的 ABI 转换 。
接口中声明的函数没有定义,所以指向它们也没有意义。
成员方法:
public(或 external)函数都有下面的成员:
.address
返回函数的合约地址。.selector
返回 ABI 函数选择器
备注
public(或 external)函数过去有额外两个成员: .gas(uint)
和 .value(uint)
在0.6.2中弃用了,在 0.8.0 中移除了。
用 {gas: ...}
和 {value: ...}
代替, 参考 外部函数调用 了解更多。
下面的例子,显示如何使用成员:
// 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}();
}
}
如果使用内部函数类型的例子:
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;
}
}
另外一个使用外部函数类型的例子:
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)); // known contract
uint private exchangeRate;
function buySomething() public {
ORACLE_CONST.query("USD", this.oracleResponse);
}
function oracleResponse(uint response) public {
require(
msg.sender == address(ORACLE_CONST),
"Only oracle can call this."
);
exchangeRate = response;
}
}
备注
Lambda 表达式或者内联函数的引入在计划内,但目前还没支持。
引用类型¶
引用类型可以通过多个不同的名称修改它的值,而值类型的变量,每次都有独立的副本。因此,必须比值类型更谨慎地处理引用类型。 目前,引用类型包括结构,数组和映射,如果使用引用类型,则必须明确指明数据存储哪种类型的位置(空间)里:
内存 即数据在内存中,因此数据仅在其生命周期内(函数调用期间)有效。不能用于外部调用。
存储 状态变量保存的位置,只要合约存在就一直存储.
调用数据 用来保存函数参数的特殊数据位置,是一个只读位置。
备注
译者注:0.6.9 之前 calldata 仅用于外部函数调用参数,0.6.9之后可用于任意函数。
更改数据位置或类型转换将始终产生自动进行一份拷贝,而在同一数据位置内(对于 存储 来说)的复制仅在某些情况下进行拷贝。
数据位置¶
所有的引用类型,如 数组 和 结构体 类型,都有一个额外注解 数据位置
,来说明数据存储位置。
有三种位置: 内存 、 存储 以及 调用数据 。
调用数据 是不可修改的、非持久的函数参数存储区域,效果大多类似 内存 。
主要用于外部函数的参数,但也可用于其他变量。
备注
如果可以的话,请尽量使用 calldata
作为数据位置,因为它将避免复制,并确保不能修改数据。
函数的返回值中也可以使用 calldata
数据位置的数组和结构,但是无法给其分配空间。
备注
在0.6.9版本之前,引用类型参数的数据位置有限制,外部函数中使用 calldata
,公共函数中使用 memory
,以及内部和私有函数中的 memory
或 storage
。
现在 memory
和 calldata
在所有函数中都被允许使用,无论其可见性如何。
备注
在版本0.5.0之前,数据位置可以省略,并且根据变量的类型,函数类型等有默认数据位置,但是所有复杂类型现在必须提供明确的数据位置。
数据位置与赋值行为¶
数据位置不仅仅表示数据如何保存,它同样影响着赋值行为:
在 存储 和 内存 之间两两赋值(或者从 调用数据 赋值 ),都会创建一份独立的拷贝。
从 内存 到 内存 的赋值只创建引用, 这意味着更改内存变量,其他引用相同数据的所有其他内存变量的值也会跟着改变。
从 存储 到本地存储变量的赋值也只分配一个引用。
其他的向 存储 的赋值,总是进行拷贝。 这种情况的示例如对状态变量或 存储 的结构体类型的局部变量成员的赋值,即使局部变量本身是一个引用,也会进行一份拷贝(译者注:查看下面
ArrayContract
合约 更容易理解)。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
contract Tiny {
uint[] x; // x 的数据存储位置是 storage, 位置可以忽略
// 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;
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]
(注意这里跟其它语言比,数组长度的声明位置是反的)。
备注
译者注:作为对比,如在Java中,声明一个包含5个元素、每个元素都是数组的方式为 int[5][]。
在Solidity中, X[3]
总是一个包含三个 X
类型元素的数组,即使 X
本身就是一个数组,这和其他语言也有所不同,比如 C 语言。
数组下标是从 0 开始的,且访问数组时的下标顺序与声明时相反。
如:如果有一个变量为 uint[][5] memory x
, 要访问第三个动态数组的第7个元素,使用 x[2][6],要访问第三个动态数组使用 x[2]
。
同样,如果有一个 T
类型的数组 T[5] a
, T 也可以是一个数组,那么 a[2]
总会是 T
类型。
数组元素可以是任何类型,包括映射或结构体。对类型的限制是映射只能存储在 存储 中,并且公开访问函数的参数需要是 ABI 类型。
状态变量标记 public
的数组,Solidity创建一个 getter函数 。
小标数字索引就是 getter函数 的参数。
访问超出数组长度的元素会导致异常(assert 类型异常 )。 可以使用 .push()
方法在末尾追加一个新元素,其中 .push()
追加一个零初始化的元素并返回对它的引用。
bytes
和 string
也是数组¶
bytes
和 string
类型的变量是特殊的数组。
bytes
类似于 bytes1[]
,但它在 调用数据 和 内存 中会被“紧打包”(译者注:将元素连续地存在一起,不会按每 32 字节一单元的方式来存放)。
string
与 bytes
相同,但不允许用长度或索引来访问。
Solidity没有字符串操作函数,但是可以使用第三方字符串库,我们可以比较两个字符串通过计算他们的 keccak256-hash ,可使用
keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2))
和使用 string.concat(s1, s2)
来拼接字符串。
我们更多时候应该使用 bytes
而不是 bytes1[]
,因为Gas 费用更低, 在 内存 中使用 bytes1[]
时,会在元素之间添加31个填充字节。
而在 存储 中,由于紧密包装,这没有填充字节, 参考 bytes and string 。
作为一个基本规则,对任意长度的原始字节数据使用 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
。
// 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中的所有变量,新分配的数组元素总是以 默认值 初始化。
pragma solidity >=0.4.16 <0.9.0;
contract TX {
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 LBC {
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;
}
}
目前需要注意的是,定长的 内存 数组并不能赋值给变长的 内存 数组,下面的例子是无法运行的:
pragma solidity >=0.4.0 <0.9.0;
// 这段代码并不能编译。
contract LBC {
function f() public {
// 这一行引发了一个类型错误,因为 unint[3] memory
// 不能转换成 uint[] memory。
uint[] x = [uint(1), 3, 4];
}
}
计划在未来移除这样的限制,但目前数组在 ABI 中传递的问题造成了一些麻烦。
如果要初始化动态长度的数组,则必须显示给各个元素赋值:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <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()
减少长度则依赖移除与元素的大小(size). 如果元素是数组,则成本是很高的,因为它包括已删除的元素的清理,类似于在这些元素上调用 delete 。
备注
如果需要在外部(external)函数中使用多维数组,这需要启用ABI coder v2。 公有(public)函数中是支持的使用多维数组。
备注
在Byzantium(在2017-10-16日4370000区块上进行硬分叉升级)之前的EVM版本中,无法访问从函数调用返回动态数组。 如果要调用返回动态数组的函数,请确保 EVM 在拜占庭模式上运行。
pragma solidity >=0.6.0 <0.9.0;
contract ArrayContract {
uint[2**20] aLotOfIntegers;
// 注意下面的代码并不是一对动态数组,
// 而是一个数组元素为一对变量的动态数组(也就是数组元素为长度为 2 的定长数组的动态数组)。
// 因为 T[] 总是 T 的动态数组, 尽管 T 是数组
// 所有的状态变量的数据位置都是 storage
bool[2][] pairsOfFlags;
// newPairs 存储在 memory 中 (仅当它是公有的合约函数)
function setAllFlagPairs(bool[2][] memory newPairs) public {
// 向一个 storage 的数组赋值会对 ``newPairs`` 进行拷贝,并替代整个 ``pairsOfFlags`` 数组
pairsOfFlags = newPairs;
}
struct StructType {
uint[] contents;
uint moreInfo;
}
StructType s;
function f(uint[] memory c) public {
// 保存引用
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.length = 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);
// 内联(Inline)数组始终是静态大小的,如果只使用字面常量,则必须至少提供一种类型。
arrayOfPairs[0] = [uint(1), 2];
// 创建一个动态字节数组:
bytes memory b = new bytes(200);
for (uint i = 0; i < b.length; i++)
b[i] = byte(uint8(i));
return b;
}
}
数组切片¶
数组切片是数组连续部分的视图,用法如:x[start:end]
, start
和 end
是 uint256 类型(或结果为 uint256 的表达式)。
x[start:end]
的第一个元素是 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 {
/// 被当前合约管理的 客户端合约地址
address client;
constructor(address client_) {
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 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)"
// 因为RHS(right hand side)会创建一个包含映射的内存结构体 "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;
}
}
上面的合约只是一个简化版的 众筹合约,但它已经足以让我们理解结构体的基础概念。 结构体类型可以作为元素用在映射和数组中,其自身也可以包含映射和数组作为成员变量。
尽管结构体本身可以作为映射的值类型成员,但它并不能包含自身。 这个限制是有必要的,因为结构体的大小必须是有限的。
注意在函数中使用结构体时,一个结构体是如何赋值给一个存储位置是 存储 的局部变量。 在这个过程中并没有拷贝这个结构体,而是保存一个引用,所以对局部变量成员的赋值实际上会被写入状态。
当然,你也可以直接访问结构体的成员而不用将其赋值给一个局部变量,就像这样,
campaigns[campaignID].amount = 0
。
备注
在 Solidity 0.7.0 之前,在 内存 结构体包含仅 存储 的类型(例如映射)可以允许类似上例中的 campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0)
赋值,它会直接忽略映射类型。
映射¶
映射类型在声明时的形式为 mapping(KeyType => ValueType)
。
其中 KeyType
可以是任何基本类型,即可以是任何的内建类型, bytes
和 string
或合约类型、枚举类型。
而其他用户定义的类型或复杂的类型如:映射、结构体、即除 bytes
和 string
之外的数组类型是不可以作为 KeyType
的类型的。
ValueType
可以是包括映射类型在内的任何类型。
映射可以视作 哈希表 ,它们在实际的初始化过程中创建每个可能的 key,
并将其映射到字节形式全是零的值:一个类型的 默认值。然而下面是映射与哈希表不同的地方:
在映射中,实际上并不存储 key,而是存储它的 keccak256
哈希值,从而便于查询实际的值。
正因为如此,映射是没有长度的,也没有 key 的集合或 value 的集合的概念。 ,因此如果没有其他信息键的信息是无法被删除(请参阅 清理映射 )。
映射只能是 存储 的数据位置,因此只允许作为状态变量 或 作为函数内的 存储 引用 或 作为库函数的参数。 它们不能用合约公有函数的参数或返回值。
这些限制同样适用于包含映射的数组和结构体。
可以将映射声明为 public
,然后来让 Solidity 创建一个 getter 函数。
KeyType
将成为 getter 的必须参数,并且 getter 会返回 ValueType
。
如果 ValueType
是一个映射。这时在使用 getter 时将需要递归地传入每个 KeyType
参数,
在下面的示例中, MappingExample
合约定义了一个公共 balances
映射,键类型为 address
,值类型为 uint
,
将以太坊地址映射为 无符号整数值。 由于 uint
是值类型,因此getter返回与该类型匹配的值,
可以在 MappingLBC
合约中看到合约在指定地址返回该值。
// 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 MappingLBC {
function f() public returns (uint) {
MappingExample m = new MappingExample();
m.update(100);
return m.balances(this);
}
}
下面的例子是 ERC20 token 的简单版本.
_allowances
是一个嵌套mapping的例子.
_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 {
// Just a struct holding our data.
itmap data;
// Apply library functions to the data type.
using IterableMapping for itmap;
// Insert something
function insert(uint k, uint v) public returns (uint size) {
// This calls IterableMapping.insert(data, k, v)
data.insert(k, v);
// We can still access members of the struct,
// but we should take care not to mess with them.
return data.size;
}
// Computes the sum of all stored data.
function sum() public view returns (uint s) {
for (
Iterator i = data.iterateStart();
data.iterateValid(i);
i = data.iterateNext(i)
) {
(, uint value) = data.iterateGet(i);
s += value;
}
}
}
操作符¶
即使两个操作数的类型不一样,也可以进行算术和位操作运算。
例如,你可以计算 y = x + z
,其中 x
是 uint8
, z
是 int32
类型。
在这些情况下,将使用以下机制来确定运算结果的类型(这在溢出的情况下很重要)。
如果右操作数的类型可以隐含地转换为左操作数的类型的类型,则使用左操作数的类型。
如果左操作数的类型可以隐含地转换为右操作数的类型的类型,则使用右操作数的类型。
否则,该操作不被允许。
如果其中一个操作数是一个 常量数字,会首先被转换为能容纳该值的最小的类型 (相同位数时,无符号类型被认为比有符号类型 “小”)。 如果两者都是常量数字,则以任意的精度进行计算。
操作符的结果类型与执行操作的类型相同,除了比较运算符,其结果总是 bool
。
运算符 **``(幂), ``<<
和 >>
使用左边操作数的类型来作为运算结果类型。
三元运算符¶
三元运算符是一个表达是形式: <expression> ? <trueExpression> : <falseExpression>
。
它根据 <expression>
的执行结果,选择后两个给定表达式中的一个。
如果 <expression>
执行结果 true
,那么 <trueExpression>
将被执行,否则 <falseExpression>
被执行。
三元运算符的结果不会为有理数类型,即使它的所有操作数都是有理数类型。 结果类型是由两个操作数的类型决定的,方法与上面一样,如果需要的话,首先转换为它们的最小可容纳类型(mobile type )。
因此, 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
。即对于整型变量来说,相当于 a = 0
,delete 也适用于数组,对于动态数组来说,是将重置为数组长度为0的数组,而对于静态数组来说,是将数组中的所有元素重置为初始值。对数组而言, delete a[x]
仅删除数组索引 x
处的元素,其他的元素和长度不变,这以为着数组中留出了一个空位。如果打算删除项,映射可能是更好的选择。
如果对象 a
是结构体,则将结构体中的所有属性(成员)重置。
换句话说,在 delete a
之后 a
的值与在没有赋值的情况下声明 a
的情况相同,
但需要注意以下几点:
delete
对整个映射是无效的(因为映射的键可以是任意的,通常也是未知的)。
因此在你删除一个结构体时,结果将重置所有的非映射属性(成员),这个过程是递归进行的,除非它们是映射。
然而,单个的键及其映射的值是可以被删除的。
理解 delete a
的效果就像是给 a
赋值很重要,换句话说,这相当于在 a
中存储了一个新的对象。
当 a
是应用变量时,我们可以看到这个区别, delete a
它只会重置 a
本身,而不是更改它之前引用的值。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract DeleteLBC {
uint data;
uint[] dataArray;
function f() public {
uint x = data;
delete x; // 将 x 设为 0,并不影响数据
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
类型的变量,又进行了另一个隐式转换.
uint8 y;
uint16 z;
uint32 x = y + z;
显式转换¶
如果某些情况下编译器不支持隐式转换,但是你很清楚你要做的结果,这种情况可以考虑显式转换。
注意这可能会发生一些无法预料的后果,因此一定要进行测试,确保结果是你想要的!
下面的示例是将一个 int8
类型的负数转换成 uint
:
int8 y = -3;
uint x = uint(y);
这段代码的最后, x
的值将是 0xfffff..fd
(64 个 16 进制字符),因为这是 -3 的 256 位补码形式。
如果一个类型显式转换成更小的类型,相应的高位将被舍弃:
uint32 a = 0x12345678;
uint16 b = uint16(a); // 此时 b 的值是 0x5678
如果将整数显式转换为更大的类型,则将填充左侧(即在更高阶的位置)。 转换结果依旧等于原来整数:
uint16 a = 0x1234;
uint32 b = uint32(a); // b 为 0x00001234 now
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]);
因为整数和定长字节数组在截断(或填充)时行为是不同的, 如果整数和定长字节数组有相同的大小,则允许他们之间进行显式转换, 如果要在不同的大小的整数和定长字节数组之间进行转换 ,必须使用一个中间类型来明确进行所需截断和填充的规则:
.. code-block:: solidity
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 切片可以显示转换为固定长度的 bytes 类型 (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); // if length of m is greater than 16, truncation will happen
b = bytes16(s); // padded on the right, so result is "abcdefgh\0\0\0\0\0\0\0\0"
bytes3 b1 = bytes3(s); // truncated, b1 equals to "abc"
b = bytes16(c[:8]); // also padded with zeros
return (b, b1);
}
}
字面常量与基本类型的转换¶
整型与字面常量转换¶
十进制和十六进制字面常量可以隐式转换为任何足以表示它而不会截断的整数类型 :
uint8 a = 12; // 可行
uint32 b = 1234; // 可行
uint16 c = 0x123456; // 失败, 会截断为 0x3456
备注
在 0.8.0 之前, 任何十进制和十六进制常量都可以显示转化为整型,不过从0.8.0开始,只有在匹配数据范围时,才能进行这个转换,就像隐式转换那样。
定长字节数组与字面常量转换¶
十进制字面常量不能隐式转换为定长字节数组。十六进制字面常量可以是,但仅当十六进制数字大小完全符合定长字节数组长度。 不过零值例外,零的十进制和十六进制字面常量都可以转换为任何定长字节数组类型:
bytes2 a = 54321; // 不可行
bytes2 b = 0x12; // 不可行
bytes2 c = 0x123; // 不可行
bytes2 d = 0x1234; // 可行
bytes2 e = 0x0012; // 可行
bytes4 f = 0; // 可行
bytes4 g = 0x0; // 可行
字符串字面常量和十六进制字符串字面常量可以隐式转换为定长字节数组,如果它们的字符数与字节类型的大小相匹配:
.. code-block:: solidity
bytes2 a = hex”1234”; // 可行 bytes2 b = “xy”; // 可行 bytes2 c = hex”12”; // 不可行 bytes2 d = hex”123”; // n不可行 bytes2 e = “x”; // 不可行 bytes2 f = “xyz”; // 不可行