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
的别名。
运算符:
比较运算符: <=
, <
, ==
, !=
, >=
, >
(返回布尔值)
位运算符: &
, |
, ^
(异或), ~
(位取反)
移位运算符: <<
(左移位) , >>
(右移位)
算数运算符: +
, -
, 一元运算 -
, 一元运算 +
, *
, /
, %
(取余或叫模运算) , **
(幂)
警告
Solidity中的整数是有取值范围的。 例如``uint32``类型的取值范围是 0``到``2 ** 32-1`
。 如果整数的某些操作的结果不在取值范围内,则会被截断。 这些截断可能会让我们承担的严重后果,进一步参考 小心处理溢出问题.
比较运算
比较整型的值
位运算
位运算在数字的二进制补码表示上执行。 这意味着: ~int256(0)== int256(-1)
。
移位
移位操作的结果具有左操作数的类型,同时会截断结果以匹配类型。
不管
x
正还是负,x << y
相当于x * 2 ** y
。如果
x
为正值,x >> y
相当于x / 2 ** y
。如果
x
为负值,x >> y
相当于(x + 1) / 2**y - 1
(与将x
除以2**y
同时向负无穷大四舍五入)。在所有情况下,通过负值的
y
进行移位会引发运行时异常。
警告
在版本 0.5.0
之前,对于负 x
的右移 x >> y
相当于 x / 2 ** y
,即,右移使用向上舍入(向零)而不是向下舍入(向负无穷大)。
加、减、乘法运算
加法,减法和乘法具有通常的语义,值用两进制补码表示,意思是比如:uint256(0) - uint256(1)== 2 ** 256 - 1
。 我们在设计和编写智能合约时必须考虑到溢出问题。
表达式 -x
相当于 (T(0) - x)
这里 T
是指 x
的类型。 这意味着如果 x
的类型的类型是无符号整数类型 -x
不会是负数。 另外,如果 x
为负数, -x
也可以为正数。 由于两进制补码表示还需要小心:
int x = -2**255; assert(-x == x);
这意味着即使数字是负数,也不能假设它的负数会是正数。
除法运算
除法运算结果的类型始终是其中一个操作数的类型,整数除法总是产生整数。 在Solidity中,分数会取零。 这意味着 int256(-5) / int256(2) == int256(-2)
。
注意在智能合约中,在 字面常量 上进行除法会保留精度(保留小数位)。
注解
除以0 会发生错误(assert 类型错误)。
模运算(取余)
模运算 a%n
是在操作数 a
的除以 n
之后产生余数 r
,其中 q = int(a / n)
和 r = a - (n * q)
。 这意味着模运算结果与左操作数相同的符号相同(或零)。 对于 负数的a : a % n == -(abs(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取模会发生错误(assert 类型错误)。
幂运算
幂运算仅适用于无符号类型。 请注意这些类型足够大,以保证能容纳计算结果。
注解
注意 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
:保存一个20字节的值(以太坊地址的大小)。
ddress payable
:可支付地址,与address
相同,不过有成员函数transfer
和send
。
这种区别背后的思想是 address payable
可以接受以太币的地址,而一个普通的 address
则不能。
类型转换:
允许从 address payable
到 address
的隐式转换,而从 address
到 address payable
的转换是不可以的( 执行这种转换的唯一方法是使用中间类型,先转换为 uint160
),如:
address payable ap = address(uint160(addr));
地址字面常量 可以隐式转换为 address payable
。
address
可以显式和整型、整型字面常量、bytes20
及合约类型相互转换。转换时需注意:不允许以 address payable(x)
形式转换。 如果 x
是整型或定长字节数组、字面常量或具有可支付的回退( payable fallback )函数的合约类型,则转换形式 address(x)
的结果是 address payable
类型。 如果 x
是没有可支付的回退( payable fallback )函数的合约类型,则 address(x)
将是 address
类型。 在外部函数签名(定义)中,address
可用来表示 address
和 address payable
类型。
注解
大部分情况下你不需要关心 address
与 address payable
之间的区别,并且到处都使用 address
。 例如,如果你在使用 取款模式, 你可以(也应该)保存地址为 address
类型, 因为可以在msg.sender
对象上调用 transfer
函数, 因为 msg.sender
是 address payable
。
运算符:
<=
, <
, ==
, !=
, >=
and >
警告
如果将使用较大字节数组类型转换为 address
,例如 bytes32
,那么 address
将被截断。 为了减少转换歧义,0.4.24及更高编译器版本要求我们在转换中显式截断处理。 以地址 0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCCC
为例, 如果使用 address(uint160(bytes20(b)))
结果是 0x111122223333444455556666777788889999aAaa
, 而使用 address(uint160(uint256(b)))
结果是 0x777788889999AaAAbBbbCcccddDdeeeEfFFfCcCc
。
注解
address
和 address payable
的区别是在 0.5.0 版本引入的,同样从这个版本开始,合约类型不在继承自地址类型,不过如果合约有可支付的回退( payable fallback )函数,合约类型仍然可以显示转换为 address
或 address payable
。
地址类型成员变量
查看所有的成员,可参考 地址相关。
balance
和 transfer
可以使用 balance
属性来查询一个地址的余额, 也可以使用 transfer
函数向一个可支付地址(payable address)发送 以太币Ether (以 wei 为单位):
address x = 0x123; address myAddress = this; if (x.balance < 10 && myAddress.balance >= 10) x.transfer(10);
如果当前合约的余额不够多,则 transfer
函数会执行失败,或者如果以太转移被接收帐户拒绝, transfer
函数同样会失败而进行回退。
注解
如果 x
是一个合约地址,它的代码(更具体来说是它的 Fallback 函数,如果有的话)会跟 transfer
函数调用一起执行(这是 EVM 的一个特性,无法阻止)。 如果在执行过程中用光了 gas 或者因为任何原因执行失败,以太币Ether 交易会被打回,当前的合约也会在终止的同时抛出异常。
send
send
是 transfer
的低级版本。如果执行失败,当前的合约不会因为异常而终止,但 send
会返回 false
。
警告
在使用 send
的时候会有些风险:如果调用栈深度是 1024 会导致发送失败(这总是可以被调用者强制),如果接收者用光了 gas 也会导致发送失败。 所以为了保证 以太币Ether 发送的安全,一定要检查 send
的返回值,使用 transfer
或者更好的办法: 使用接收者自己取回资金的模式。
call
, delegatecall
和 staticcall
为了与不符合 应用二进制接口Application Binary Interface(ABI) 的合约交互,或者要更直接地控制编码,提供了函数 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);
此外,为了与不符合 应用二进制接口Application Binary Interface(ABI) 的合约交互,于是就有了可以接受任意类型任意数量参数的 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()
修饰器modifier 调整提供的 gas 数量
address(nameReg).call.gas(1000000)(abi.encodeWithSignature("register(string)", "MyName"));
类似地,也能控制提供的 以太币Ether 的值
address(nameReg).call.value(1 ether)(abi.encodeWithSignature("register(string)", "MyName"));
最后一点,这些 修饰器modifier 可以联合使用。每个修改器出现的顺序不重要
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()
选项,而 delegatecall
不支持 .value()
选项。
注解
所有合约都可以转换为 address
类型,因此可以使用 address(this).balance
查询当前合约的余额。
每一个 contract 定义都有他自己的类型。
您可以隐式地将合约转换为从他们继承的合约。 合约可以显式转换为 address
类型。
只有当合约具有可支付回退函数时,才能显式和 address payable
类型相互转换 转换仍然使用 address(x)
执行,而不是使用 address payable(x)
。
可以参考 地址类型.
注解
在版本0.5.0之前,合约直接从地址类型派生的, 并且 address
和 address payable
之间没有区别。
如果声明一个合约类型的局部变量(MyContract c),则可以调用该合约的函数。 注意需要赋相同合约类型的值给它。
您还可以实例化合约(即新创建一个合约对象),参考 ‘使用new创建合约’。
合约和 address
的数据表示是相同的, 参考 ABI。
合约不支持任何运算符。
合约类型的成员是合约的外部函数及 public 的 状态变量。
对于合约 C
可以使用 type(C)
获取合约的类型信息,参考 类型信息 。
关键字有:bytes1
, bytes2
, bytes3
, …, bytes32
。 byte
是 bytes1
的别名。
运算符:
比较运算符:<=
, <
, ==
, !=
, >=
, >
(返回布尔型)
位运算符: &
, |
, ^
(按位异或), ~
(按位取反)
移位运算符: <<
(左移位), >>
(右移位)
索引访问:如果 x
是 bytesI
类型,那么 x[k]
(其中 0 <= k < I
)返回第 k
个字节(只读)。
该类型可以和作为右操作数的任何整数类型进行移位运算(但返回结果的类型和左操作数类型相同),右操作数表示需要移动的位数。 进行负数位移运算会引发运行时异常。
成员变量:
.length
表示这个字节数组的长度(只读).
注解
可以将 byte[]
当作字节数组使用,但这种方式非常浪费存储空间,准确来说,是在传入调用时,每个元素会浪费 31 字节。 更好地做法是使用 bytes
。
bytes
:
变长字节数组,参见 数组。它并不是值类型。
string
:
变长 UTF-8 编码字符串类型,参见 数组。并不是值类型。
比如像 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF
这样的通过了地址校验和测试的十六进制字面常量会作为 address payable
类型。 十六进制字面常量长度在 39 到 41 个数字之间,而没有通过校验测试(我们也会收到一个警告)的十六进制字面常量则会视为正常的有理数字面常量。
注解
混合大小写的地址校验和格式定义在 EIP-55 中。
整数字面常量由范围在 0-9 的一串数字组成,表现成十进制。 例如,69 表示数字 69。 Solidity 中是没有八进制的,因此前置 0 是无效的。
十进制小数字面常量带有一个 .
,至少在其一边会有一个数字。 比如:1.
,.1
,和 1.3
。
科学符号也是支持的,尽管指数必须是整数,但底数可以是小数。 比如:2e10
, -2e10
, 2e-10
, 2.5e1
。
为了提高可读性可以在数字之间加上下划线。 例如,十进制``123_000``,十六进制 0x2eff_abde
,科学十进制表示1_2e345_678都是有效的。 下划线仅允许在两位数之间,并且不允许下划线连续出现。添加到数字文字中下划线没有额外的语义,下划线会被编译器忽略。
数值字面常量表达式本身支持任意精度,除非它们被转换成了非字面常量类型(也就是说,当它们出现在变量表达式中时就会发生转换)。 这意味着在数值常量表达式中, 计算不会溢出而除法也不会截断。
例如, (2**800 + 1) - 2**800
的结果是字面常量 1
(属于 uint8
类型),尽管计算的中间结果已经超过了 以太坊虚拟机Ethereum Virtual Machine(EVM) 的机器字长度。 此外, .5 * 8
的结果是整型 4
(尽管有非整型参与了计算)。
只要操作数是整型,任意整型支持的运算符都可以被运用在数值字面常量表达式中。 如果两个中的任一个数是小数,则不允许进行位运算。如果指数是小数的话,也不支持幂运算(因为这样可能会得到一个无理数)。
注解
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'
)。 不像在 C 语言中那样带有结束符;"foo"
相当于 3 个字节而不是 4 个。 和整数字面常量一样,字符串字面常量的类型也可以发生改变,但它们可以隐式地转换成 bytes1
,……,bytes32
,如果合适的话,还可以转换成 bytes
以及 string
。
例如: bytes32 samevar = "stringliteral"
字符串字面常量在赋值给 bytes32
时被解释为原始的字节形式。
字符串字面常量支持下面的转义字符:
\
(转义实际换行)
\\
(反斜杠)
\'
(单引号)
\"
(双引号)
\b
(退格)
\f
(换页)
\n
(换行符)
\r
(回车)
\t
(标签 tab)
\v
(垂直标签)
\xNN
(十六进制转义,见下文)
\uNNNN
(unicode 转义,见下文)
\xNN
表示一个 16 进制值,最终转换成合适的字节,而 \uNNNN
表示 Unicode 编码值,最终会转换为 UTF-8 的序列。
以下示例中的字符串长度为十个字节,它以换行符开头,后跟双引号,单引号,反斜杠字符,以及(没有分隔符)字符序列 abcdef
。
"\n\"\'\\abc\ def"
任何unicode行终结符(即LF,VF,FF,CR,NEL,LS,PS)都不会被当成字符串字面常量的终止符。 如果前面没有前置 \
,则换行符仅终止字符串字面常量。
十六进制字面常量以关键字 hex
打头,后面紧跟着用单引号或双引号引起来的字符串(例如,hex"001122FF"
)。 字符串的内容必须是一个十六进制的字符串,它们的值将使用二进制表示。
十六进制字面常量跟 字符串字面常量 很类似,具有相同的转换规则
枚举是在Solidity中创建用户定义类型的一种方法。 它们是显示所有整型相互转换,但不允许隐式转换。 从整型显式转换枚举,会在运行时检查整数时候在枚举范围内,否则会导致异常( assert 类型异常 )。 枚举需要至少一个成员。
数据表示与C中的枚举相同:选项从“0”开始的无符号整数值表示。
pragma solidity >=0.4.16 <0.7.0; 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)"。 // 整数类型的大小已经足够存储所有枚举类型的值,随着值的个数增加, // 可以逐渐使用 `uint16` 或更大的整数类型。 function getChoice() public view returns (ActionChoices) { return choice; } function getDefaultChoice() public pure returns (uint) { return uint(defaultChoice); } }
函数类型是一种表示函数的类型。可以将一个函数赋值给另一个函数类型的变量,也可以将一个函数作为参数进行传递,还能在函数调用中返回函数类型变量。 函数类型有两类:
内部(internal) 函数类型
外部(external) 函数类型
内部函数只能在当前合约内被调用(更具体来说,在当前代码块内,包括内部库函数和继承的函数中),因为它们不能在当前合约上下文的外部被执行。 调用一个内部函数是通过跳转到它的入口标签来实现的,就像在当前合约的内部调用一个函数。
外部函数由一个地址和一个函数签名组成,可以通过外部函数调用传递或者返回。
函数类型表示成如下的形式
function () {internal|external} [pure|constant|view|payable] [returns ( )]
与参数类型相反,返回类型不能为空 —— 如果函数类型不需要返回,则需要删除整个 returns (
部分。
函数类型默认是内部函数,因此不需要声明 internal
关键字。
请注意,这仅适用于函数类型,合约中定义的函数明确指定可见性,它们没有默认值。
类型转换:
外部函数类型的值可以显式转换为某个合约地址 address
的函数。
函数类型 A
可以隐式转换为函数类型 B
当且仅当: 它们的参数类型相同,返回类型相同,它们的内部/外部属性是相同的,并且 A
的状态可变性并不比 B
的状态可变性更具限制性,比如:
pure
函数可以转换为view
和non-payable
函数
view
函数可以转换为non-payable
函数
payable
函数可以转换为non-payable
函数
其他的转换则不可以。
关于 payable
和 non-payable
的规则可能有点令人困惑,但实质上,如果一个函数是 payable
,这意味着它 也接受零以太的支付,因此它也是 non-payable
。 另一方面,non-payable
函数将拒绝发送给它的 以太币Ether , 所以 non-payable
函数不能转换为 payable
函数。
如果当函数类型的变量还没有初始化时就调用它的话会引发一个异常。 如果在一个函数被 delete
之后调用它也会发生相同的情况。
如果外部函数类型在 Solidity 的上下文环境以外的地方使用,它们会被视为 function
类型。 该类型将函数地址紧跟其函数标识一起编码为一个 bytes24
类型。。
请注意,当前合约的 public 函数既可以被当作内部函数也可以被当作外部函数使用。 如果想将一个函数当作内部函数使用,就用 f
调用,如果想将其当作外部函数,使用 this.f
。
成员方法:
public(或 external)函数都有下面的成员:
.selector
返回 ABI 函数选择器
.gas(uint)
返回一个可调用的函数对象,当被调用时,它将指定函数运行的Gas。参考 外部函数调用 了解更多。
.value(uint)
返回一个可调用的函数对象,当被调用时,它将向目标函数发送指定数量的 以太币Ether (单位 wei)。 参考 外部函数调用 了解更多。
下面的例子,显示如何使用成员:
pragma solidity >=0.4.16 <0.7.0; contract Example { function f() public payable returns (bytes4) { return this.f.selector; } function g() public { this.f.gas(10).value(800)(); } }
如果使用内部函数类型的例子:
pragma solidity >=0.4.16 <0.7.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.7.0; contract Oracle { struct Request { bytes data; function(uint) external callback; } Request[] 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 oracle = Oracle(0x1234567); // known contract uint exchangeRate; function buySomething() public { oracle.query("USD", this.oracleResponse); } function oracleResponse(uint response) public { require( msg.sender == address(oracle), "Only oracle can call this." ); exchangeRate = response; } }
注解
Lambda 表达式或者内联函数的引入在计划内,但目前还没支持。
引用类型可以通过多个不同的名称修改它的值,而值类型的变量,每次都有独立的副本。因此,必须比值类型更谨慎地处理引用类型。 目前,引用类型包括结构,数组和映射,如果使用引用类型,则必须明确指明数据存储哪种类型的位置(空间)里:
内存memory 其生命周期只存在与函数调用期间
存储storage 状态变量保存的位置,一直存在区块链中
调用数据calldata 函数参数的特殊数据位置,仅适用于外部函数调用参数
更改数据位置或类型转换将始终产生自动进行一份拷贝,而在同一数据位置内(对于 存储storage 来说)的复制仅在某些情况下进行拷贝。
所有的引用类型,如 数组 和 结构体 类型,都有一个额外注解 数据位置
,来说明数据存储位置。 有三种位置: 内存memory 、 存储storage 以及 调用数据calldata 。 调用数据calldata 仅对外部合约函数的参数有效,同时也是必须的。 调用数据calldata 是不可修改的、非持久的函数参数存储区域,效果大多类似 内存memory 。
注解
在版本0.5.0之前,数据位置可以省略,并且根据变量的类型,函数类型等有默认数据位置,但是所有复杂类型现在必须提供明确的数据位置。
数据位置与赋值行为
数据位置不仅仅表示数据如何保存,它同样影响着赋值行为:
在 存储storage 和 内存memory 之间两两赋值(或者从 调用数据calldata 赋值 ),都会创建一份独立的拷贝。
从 内存memory 到 内存memory 的赋值只创建引用, 这意味着更改内存变量,其他引用相同数据的所有其他内存变量的值也会跟着改变。
从 存储storage 到本地存储变量的赋值也只分配一个引用。
其他的向 存储storage 的赋值,总是进行拷贝。 这种情况的示例如对状态变量或 存储storage 的结构体类型的局部变量成员的赋值,即使局部变量本身是一个引用,也会进行一份拷贝(译者注:查看下面 ArrayContract
合约 更容易理解)。
pragma solidity >=0.4.0 <0.7.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.length = 2; // 通过 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] x memory
, 要访问第三个动态数组的第二个元素,使用 x[2][1],要访问第三个动态数组使用 x[2]
。 同样,如果有一个 T
类型的数组 T[5] a
, T 也可以是一个数组,那么 a[2]
总会是 T
类型。
数组元素可以是任何类型,包括映射或结构体。对类型的限制是映射只能存储在 存储storage 中,并且公开访问函数的参数需要是 ABI 类型。
状态变量标记 public
的数组,Solidity创建一个 访问器 。 小标数字索引就是 访问器 函数的参数。
访问超出数组长度的元素会导致异常(assert 类型异常 )。 可以使用 .push()
方法在末尾追加一个新元素,或者给 .length
赋值来改变大小,参考 数组成员 (参见下面的注意事项)。
bytes
和 strings
也是数组
bytes
和 string
类型的变量是特殊的数组。 bytes
类似于 byte[]
,但它在 调用数据calldata 和 内存memory 中会被“紧打包”(译者注:将元素连续地存在一起,不会按每 32 字节一单元的方式来存放)。string
与 bytes
相同,但不允许用长度或索引来访问。
Solidity没有字符串操作函数,但是可以使用第三方字符串库,我们可以比较两个字符串通过计算他们的 keccak256-hash ,可使用 keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2))
或者使用 abi.encodePacked(s1, s2)
来拼接字符串。
我们更多时候应该使用 bytes
而不是 byte[]
,因为Gas 费用更低, byte[]
会在元素之间添加31个填充字节。作为一个基本规则, 对任意长度的原始字节数据使用 bytes
,对任意长度字符串(UTF-8)数据使用 string
。
如果使用一个长度限制的字节数组,应该使用一个 bytes1
到 bytes32
的具体类型,因为它们便宜得多。
注解
如果想要访问以字节表示的字符串 s
,请使用 bytes(s).length
/ bytes(s)[7] = 'x';
。 注意这时你访问的是 UTF-8 形式的低级 bytes 类型,而不是单个的字符。
创建内存数组
可使用 new
关键字在 内存memory 中基于运行时的长度创建数组。 与 存储storage 数组相反的是,你 不能 通过修改成员变量 .length
改变 内存memory 数组的大小。
必须提前计算所需的大小或者创建一个新的内存数组并复制每个元素。
pragma solidity >=0.4.16 <0.7.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)]
。 必须有一个所有元素都可以隐式转换到普通的类型,这个类型就是数组的基本类型。 数组字面常量总是静态固定大小的 内存memory 数组。
在下面的例子中,[1, 2, 3]
的类型是 uint8[3] memory
。 因为每个常量的类型都是 uint8
,如果你希望结果是 uint [3] memory
类型,你需要将第一个元素转换为 uint
。
pragma solidity ^0.4.16; contract LBC { function f() public pure { g([uint(1), 2, 3]); } function g(uint[3] _data) public pure { // ... } }
目前需要注意的是,定长的 内存memory 数组并不能赋值给变长的 内存memory 数组,下面的例子是无法运行的:
// 这段代码并不能编译。 pragma solidity >=0.4.0 <0.7.0; contract LBC { function f() public { // 这一行引发了一个类型错误,因为 unint[3] memory // 不能转换成 uint[] memory。 uint[] x = [uint(1), 3, 4]; } }
计划在未来移除这样的限制,但目前数组在 ABI 中传递的问题造成了一些麻烦。
数组成员
length:
数组有 length
成员变量表示当前数组的长度。 一经创建,内存memory 数组的大小就是固定的(但却是动态的,也就是说,它可以根据运行时的参数创建)。 动态数组(仅存在于 存储storage 中)通过改变成员变量 .length
改变数组大小。 并不能通过访问超出当前数组长度的方式实现自动扩展数组的长度(这会导致异常)。
增加长度会为数组添加新的零初始化元素, 减少长度会在每个上面执行一个隐含的 delete 删除元素。 如果您尝试调整不在 存储storage 中的非动态数组的大小,你会收到一个 Value must be an lvalue
错误。
push:
变长的 存储storage 数组以及 bytes
类型( string
类型不可以)都有一个 push
的成员函数,它用来附加新的元素到数组末尾,元素将初始化为零。 这个函数将返回新的数组长度。
pop:
变长的 存储storage 数组以及 bytes
类型( string
类型不可以)都有一个 push
的成员函数, 它用来从数组末尾删除元素。 同样的会在移除的元素上隐含调用 delete 。
警告
如果在空数组上 .length--
会导致向下溢出,长度将设置为 2**256-1
。
注解
增加 存储storage 数组的长度具有固定的 gas 消耗,因为 存储storage 总是被零初始化,而减少长度至少是线性成本(大多数情况下比线性成本更差),
因为它包括清理已删除的元素,类似于在这些元素上调用 delete 。
注解
在外部(external)函数中目前还不能使用多维数组,但是在公有(public)函数中是支持的。
注解
在Byzantium(在2017-10-16日4370000区块上进行硬分叉升级)之前的EVM版本中,无法访问从函数调用返回动态数组。 如果要调用返回动态数组的函数,请确保 EVM 在拜占庭模式上运行。
pragma solidity >=0.4.16 <0.7.0; contract ArrayContract { uint[2**20] m_aLotOfIntegers; // 注意下面的代码并不是一对动态数组, // 而是一个数组元素为一对变量的动态数组(也就是数组元素为长度为 2 的定长数组的动态数组)。 // 因为 T[] 总是 T 的动态数组, 尽管 T 是数组 // 所有的状态变量的数据位置都是 storage bool[2][] m_pairsOfFlags; // newPairs 存储在 memory 中 (仅当它是公有的合约函数) function setAllFlagPairs(bool[2][] memory newPairs) public { // 向一个 storage 的数组赋值会对 ``newPairs`` 进行拷贝,并替代整个 ``m_pairsOfFlags`` 数组 m_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 { // 访问不存在的索引将引发异常 m_pairsOfFlags[index][0] = flagA; m_pairsOfFlags[index][1] = flagB; } function changeFlagArraySize(uint newSize) public { // 如果新大小较小,则将清除已删除的数组元素 m_pairsOfFlags.length = newSize; } function clear() public { // 这些完全清除了数组 delete m_pairsOfFlags; delete m_aLotOfIntegers; // 效果相同(和上面) m_pairsOfFlags.length = 0; } bytes m_byteData; function byteArrays(bytes memory data) public { // 字节数组(bytes)不一样,它们在没有填充的情况下存储。 // 可以被视为与 uint8 [] 相同 m_byteData = data; m_byteData.length += 7; m_byteData[3] = 0x08; delete m_byteData[2]; } function addFlag(bool[2] memory flag) public returns (uint) { return m_pairsOfFlags.push(flag); } 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; } }
Solidity 支持通过构造结构体的形式定义新的类型,以下是一个结构体使用的示例:
pragma solidity >=0.4.11 <0.7.0; contract CrowdFunding { // 定义的新类型包含两个属性。 struct Funder { address addr; uint amount; } 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 作为一个变量返回 // 在 memory 中创建新结构体并将其复制到storage 。 // 我们省略了映射类型,因为它在 memory 中无效(它存储在 storage 中)。 // 如果结构体被复制(甚至从 storage 到 storage )映射类型也始终会省略,因为它们无法枚举。 campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0); } 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
。
映射类型在声明时的形式为 mapping(_KeyType => _ValueType)
。 其中 _KeyType
可以是除了映射、变长数组、合约、枚举以及结构体以外的几乎所有类型。 _ValueType
可以是包括映射类型在内的任何类型。
映射可以视作 哈希表 ,它们在实际的初始化过程中创建每个可能的 key, 并将其映射到字节形式全是零的值:一个类型的 默认值。然而下面是映射与哈希表不同的地方: 在映射中,实际上并不存储 key,而是存储它的 keccak256
哈希值,从而便于查询实际的值。
正因为如此,映射是没有长度的,也没有 key 的集合或 value 的集合的概念。
只有状态变量(或者在 internal 函数中的对于存储变量的引用)可以使用映射类型。。
可以将映射声明为 public
,然后来让 Solidity 创建一个 getter。 _KeyType
将成为 getter 的必须参数,并且 getter 会返回 _ValueType
。
_ValueType
也可以是一个映射。这时在使用 getter 时将将需要递归地传入每个 _KeyType
参数。
pragma solidity ^0.4.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(this); } }
注解
映射不支持迭代,但可以在此之上实现一个这样的数据结构。 例子可以参考 可迭代的映射。
如果 a
是一个 LValue(即一个变量或者其它可以被赋值的东西),以下运算符都可以使用简写:
a += e
等同于 a = a + e
。 其它运算符 -=
, *=
, /=
, %=
, |=
, &=
以及 ^=
都是如此定义的。 a++
和 a--
分别等同于 a += 1
和 a -= 1
,但表达式本身的值等于 a
在计算之前的值。 与之相反,--a
和 ++a
虽然最终 a
的结果与之前的表达式相同,但表达式的返回值是计算之后的值。
delete a
的结果是将 a
的类型在初始化时的值赋值给 a
。即对于整型变量来说,相当于 a = 0
, 但 delete 也适用于数组,对于动态数组来说,是将数组的长度设为 0,而对于静态数组来说,是将数组中的所有元素重置。 如果对象是结构体,则将结构体中的所有属性重置。
delete
对整个映射是无效的(因为映射的键可以是任意的,通常也是未知的)。 因此在你删除一个结构体时,结果将重置所有的非映射属性,这个过程是递归进行的,除非它们是映射。 然而,单个的键及其映射的值是可以被删除的。
理解 delete a
的效果就像是给 a
赋值很重要,换句话说,这相当于在 a
中存储了一个新的对象。
pragma solidity ^0.4.0; contract DeleteExample { 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 对象赋值。 } }
如果一个运算符用在两个不同类型的变量之间,那么编译器将隐式地将其中一个类型转换为另一个类型(不同类型之间的赋值也是一样)。 一般来说,只要值类型之间的转换在语义上行得通,而且转换的过程中没有信息丢失,那么隐式转换基本都是可以实现的: uint8
可以转换成 uint16
,int128
转换成 int256
,但 int8
不能转换成 uint256
(因为 uint256
不能涵盖某些值,例如,-1
)。 更进一步来说,无符号整型可以转换成跟它大小相等或更大的字节类型,但反之不能。 任何可以转换成 uint160
的类型都可以转换成 address
类型。
如果某些情况下编译器不支持隐式转换,但是你很清楚你要做什么,这种情况可以考虑显式转换。 注意这可能会发生一些无法预料的后果,因此一定要进行测试,确保结果是你想要的! 下面的示例是将一个 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
If an integer is explicitly converted to a larger type, it is padded on the left (i.e. at the higher order end). The result of the conversion will compare equal to the original integer:
uint16 a = 0x1234; uint32 b = uint32(a); // b will be 0x00001234 now assert(a == b);
Fixed-size bytes types behave differently during conversions. They can be thought of as sequences of individual bytes and converting to a smaller type will cut off the sequence:
bytes2 a = 0x1234; bytes1 b = bytes1(a); // b will be 0x12
If a fixed-size bytes type is explicitly converted to a larger type, it is padded on the right. Accessing the byte at a fixed index will result in the same value before and after the conversion (if the index is still in range):
bytes2 a = 0x1234; bytes4 b = bytes4(a); // b will be 0x12340000 assert(a[0] == b[0]); assert(a[1] == b[1]);
Since integers and fixed-size byte arrays behave differently when truncating or padding, explicit conversions between integers and fixed-size byte arrays are only allowed, if both have the same size. If you want to convert between integers and fixed-size byte arrays of different size, you have to use intermediate conversions that make the desired truncation and padding rules explicit:
bytes2 a = 0x1234; uint32 b = uint16(a); // b will be 0x00001234 uint32 c = uint32(bytes4(a)); // c will be 0x12340000 uint8 d = uint8(uint16(a)); // d will be 0x34 uint8 e = uint8(bytes1(a)); // e will be 0x12
Decimal and hexadecimal number literals can be implicitly converted to any integer type that is large enough to represent it without truncation:
uint8 a = 12; // fine uint32 b = 1234; // fine uint16 c = 0x123456; // fails, since it would have to truncate to 0x3456
Decimal number literals cannot be implicitly converted to fixed-size byte arrays. Hexadecimal number literals can be, but only if the number of hex digits exactly fits the size of the bytes type. As an exception both decimal and hexadecimal literals which have a value of zero can be converted to any fixed-size bytes type:
bytes2 a = 54321; // not allowed bytes2 b = 0x12; // not allowed bytes2 c = 0x123; // not allowed bytes2 d = 0x1234; // fine bytes2 e = 0x0012; // fine bytes4 f = 0; // fine bytes4 g = 0x0; // fine
String literals and hex string literals can be implicitly converted to fixed-size byte arrays, if their number of characters matches the size of the bytes type:
bytes2 a = hex"1234"; // fine bytes2 b = "xy"; // fine bytes2 c = hex"12"; // not allowed bytes2 d = hex"123"; // not allowed bytes2 e = "x"; // not allowed bytes2 f = "xyz"; // not allowed
As described in 地址字面常量, hex literals of the correct size that pass the checksum test are of address
type. No other literals can be implicitly converted to the address
type.
Explicit conversions from bytes20
or any integer type to address
result in address payable
.
为了方便起见,没有必要每次都精确指定一个变量的类型,编译器会根据分配该变量的第一个表达式的类型自动推断该变量的类型
uint24 x = 0x123; var y = x;
这里 y
的类型将是 uint24
。不能对函数参数或者返回参数使用 var
。
警告
类型只能从第一次赋值中推断出来,因此以下代码中的循环是无限的, 原因是``i`` 的类型是 uint8
,而这个类型变量的最大值比 2000
小。 for (var i = 0; i < 2000; i++) { ... }