.. index:: storage, state variable, mapping
|storage| 中的状态变量储存结构
静态大小的变量(除 |mapping| 和动态数组之外的所有类型)都从位置 0
开始连续放置在 |storage| 中。如果可能的话,存储需求少于 32 字节的多个变量会被打包到一个 |storage_slot| 中,规则如下:
- |storage_slot| 的第一项会以低位对齐(即右对齐)的方式储存。
- 基本类型仅使用存储它们所需的字节。
- 如果 |storage_slot| 中的剩余空间不足以储存一个基本类型,那么它会被移入下一个 |storage_slot| 。
- 结构(struct)和数组数据总是会占用一整个新插槽(但结构或数组中的各项,都会以这些规则进行打包)。
Warning
使用小于 32 字节的元素时,你的合约的 gas 使用量可能高于使用 32 字节的元素时。这是因为 |evm| 每次会操作 32 个字节, 所以如果元素比 32 字节小,|evm| 必须使用更多的操作才能将其大小缩减到到所需的大小。
仅当你处理 |storage_slot| 中的值时候,使用缩减大小的参数才是有益的。因为编译器会将多个元素打包到一个 |storage_slot| 中, 从而将多个读或写合并到一次对存储的操作中。而在处理函数参数或 |memory| 中的值时,因为编译器不会打包这些值,所以没有什么益处。
最后,为了允许 |evm| 对此进行优化,请确保你对 |storage| 中的变量和 struct
成员的书写顺序允许它们被紧密地打包。
例如,按照 uint128,uint128,uint256
的顺序声明你的存储变量,而不是 uint128,uint256,uint128
,
因为前者只占用两个 |storage_slot|,而后者将占用三个。
结构和数组中的元素都是顺序存储的,就像它们被明确给定的那样。
由于 |mapping| 和动态数组的大小是不可预知的,所以我们使用 Keccak-256 哈希计算来找到具体数值或数组数据的起始位置。 这些起始位置本身的数值总是会占满堆栈插槽。
|mapping| 或动态数组本身会根据上述规则来在某个位置 p
处占用一个(未填充的)存储中的插槽(或递归地将该规则应用到 |mapping| 的 |mapping| 或数组的数组)。
对于动态数组,此插槽中会存储数组中元素的数量(字节数组和字符串在这里是一个例外,见下文)。对于 |mapping| ,该插槽未被使用(但它仍是需要的,
以使两个相同的 |mapping| 在彼此之后会使用不同的散列分布)。数组的数据会位于 keccak256(p)
; |mapping| 中的键 k
所对应的值会位于 keccak256(k . p)
,
其中 .
是连接符。如果该值又是一个非基本类型,则通过添加 keccak256(k . p)
作为偏移量来找到位置。
如果 bytes
和 string
的数据很短,那么它们的长度也会和数据一起存储到同一个插槽。具体地说:如果数据长度小于等于 31 字节,
则它存储在高位字节(左对齐),最低位字节存储 length * 2
。如果数据长度超出 31 字节,则在主插槽存储 length * 2 + 1
,
数据照常存储在 keccak256(slot)
中。
所以对于以下合约片段:
pragma solidity ^0.4.0; contract C { struct s { uint a; uint b; } uint x; mapping(uint => mapping(uint => s)) data; }
data[4][9].b
的位置将是 keccak256(uint256(9) . keccak256(uint256(4) . uint256(1))) + 1
。
.. index:: memory layout
|memory| 中的存储结构
Solidity 保留了 4 个 32 字节的插槽(slot):
临时空间可以在语句之间使用(即在内联汇编之中)。0 值插槽则用来对动态内存数组进行初始化,且永远不会写入数据(因而可用的初始内存指针为 0x80
)。
Solidity 总会把新对象保存在空闲 |memory| 指针的位置,所以这段内存实际上从来不会空闲(在未来可能会修改这个机制)。
Warning
Solidity 中有一些操作需要大于 64 字节的临时内存区域,因此这种数据无法保存到临时空间里。它们将被放置在空闲内存指向的位置,但由于这种数据的生命周期较短,这个指针不会即时更新。这部分内存可能会被清零也可能不会。所以我们不应该期望这些所谓的空闲内存总会被清零。
尽管使用 msize
来到达非零内存区域是个好主意,然而非临时性地使用这样的指针,而不更新可用内存指针也会产生有害的结果。
.. index:: calldata layout
当从一个账户调用已部署的 Solidity 合约时,调用数据的格式被认为会遵循 :ref:`ABI 说明<ABI>`。 根据 ABI 说明的规定,参数需要被整理为 32 字节的倍数。而内部函数调用会使用不同规则。
.. index:: variable cleanup
如果一个数值不足 256 位,那么在某些情况下,不足的位必须被清除。 Solidity 编译器设计用于在执行任何操作之前清除这些剩余位中可能会造成不利影响的潜在垃圾。 例如,因为 |memory| 中的内容可以用于计算散列或作为消息调用的数据发送,所以在向 |memory| 写入数值之前,需要清除剩余的位。 同样,在向 |storage| 中保存数据之前,剩余的位也需要清除,否则就会看到被混淆的数值。
另一方面,如果接下来的操作不会被影响,那我们就不用清除这些位的数据。例如,因为任何非零值都会被 JUMPI
指令视为 true
,
所以在布尔数据用做 JUMPI
的条件之前,我们就不用清除它们。
除了以上设计原理之外,Solidity 编译器在把输入数据加载到堆栈时会对它们进行清除剩余位的处理。
不同的数据类型有不同的清除无效值的规则:
类型 | 合法数值 | 无效值会导致 |
---|---|---|
n 个成员的 enum | 0 到 n - 1 | exception |
bool | 0 或 1 | 1 |
signed integers | 以符号开头的 字(32字节) | 目前会直接打包; 未来会抛出 exception |
unsigned integers | 高位补 0 | 目前会直接打包; 未来会抛出 exception |
.. index:: optimizer, common subexpression elimination, constant propagation
Solidity 优化器是在汇编语言级别工作的,所以它可以并且也被其他语言所使用。它通过 JUMP
和 JUMPDEST
语句将指令集序列分割为基础的代码块。在这些代码块内的指令集会被分析,并且对堆栈、内存或存储的每个修改都会被记录为表达式,这些表达式由一个指令和基本上是指向其他表达式的参数列表所组成。现在,主要的想法就是找到始终相等的表达式(在每个输入上)并将它们组合到一个表达式类中。优化器首先尝试在已知的表达式列表中查找每个新表达式。如果这不起作用,表达式会以 constant + constant = sum_of_constants
或 X * 1 = X
这样的规则进行简化。由于这是递归完成的,所以在我们知道第二个因子是一个更复杂的表达式,且此表达式总是等于 1 的情况下,也可以应用后一个规则。对存储和内存上某个具体位置的修改必须删除有关存储和内存位置的认知,这里边的区别并不为人所知:如果我们先在 x 位置写入,然后在 y 位置写入,且都是输入变量,则第二个可能会覆盖第一个,所以我们实际上并不知道在写入到 y 位置之后在 x 位置存储了什么。另一方面,如果对表达式 x - y 的简化,其结果为非零常数,那么我们知道我们可以保持关于 x 位置存储内容的认知。
在这个过程结束时,我们会知道最后哪些表达式必须在栈上,并且会得到一个修改内存和存储的列表。该信息与基本代码块一起存储并用来链接它们。此外,关于栈、存储和内存的配置信息会被转发到下一个代码块。如果我们知道所有 JUMP
和 JUMPI
指令的目标,我们就可以构建一个完整的程序流程图。 如果只有一个我们不知道的目标(原则上可能发生,跳转目标可以基于输入来计算),我们必须消除关于代码块输入状态的所有信息,因为它可能是未知的 JUMP
目标。如果一个 JUMPI
的条件等于一个常量,它将被转换为无条件跳转。
作为最后一步,每个块中的代码都会被完全重新生成。然后会从代码块的结尾处在栈上的表达式开始创建依赖关系图,且不是该图组成部分的每个操作实质上都会被丢弃。现在,生成的代码将按照原始代码中的顺序对内存和存储进行修改(舍弃不需要的修改),最终,生成需要在栈中的当前位置保存的所有值。
这些步骤适用于每个基本代码块,如果代码块较小,则新生成的代码将用作替换。如果一个基本代码块在 JUMPI
处被分割,且在分析过程中被评估为一个常数,则会根据常量的值来替换 JUMPI
,因此,类似于
var x = 7; data[7] = 9; if (data[x] != x + 2) return 2; else return 1;
的代码也就被简化地编译为
data[7] = 9; return 1;
即使原始代码中包含一个跳转。
.. index:: source mappings
作为 AST 输出的一部分,编译器提供 AST 中相应节点所代表的源代码范围。这可以用于多种用途,比如从用于报告错误的 AST 静态分析工具到可以突出显示局部变量及其用途的调试工具。
此外,编译器还可以生成从字节码到生成该指令的源代码范围的映射。对于在字节码级别上运行的静态分析工具以及在调试器中显示源代码中的当前位置或处理断点,这都是同样重要的。
这两种源映射都使用整数标识符来引用源文件。这些是通常称为 “sourceList”
的源文件列表的常规数组索引,它们是 combined-json 和 json / npm 编译器输出的一部分。
Note
在指令没有与任何特定的代码文件关联的情况下,源代码映射会将 -1
赋值给一个整数标识符。这会在字节码阶段发生,源于由编译器生成的内联汇编语句。
AST 内的源代码映射使用以下表示法:
s:l:f
其中,s
是源代码文件中范围起始处的字节偏移量,l
是源代码范围的长度(以字节为单位),f
是上述源代码索引。
针对字节码的源代码映射的编码方式更加复杂:它是由 ;
分隔的 s:l:f:j
列表。每个元素都对应一条指令,即不能使用字节偏移量,但必须使用指令偏移量(push 指令长于一个字节)。字段 s
,l
和 f
如上所述,j
可以是 i
,o
或 -
,表示一个跳转指令是否进入一个函数、是否从一个函数返回或者是否是一个常规跳转的一部分,例如一个循环。
为了压缩这些源代码映射,特别是对字节码的映射,我们将使用以下规则:
- 如果一个字段为空,则使用前一个元素中对应位置的值。
- 如果缺少
:
,则后续所有字段都被视为空。
这意味着以下的源代码映射是等价的:
1:2:1;1:9:1;2:1:2;2:1:2;2:1:2
1:2:1;:9;2:1:2;;
- 可以使用
delete
来删除数组中的所有元素。 - 对 struct 中的元素使用更短的数据类型,并对它们进行排序,以便将短数据类型组合在一起。这可以降低 gas 消耗,因为多个
SSTORE
操作可能会被合并成一个(SSTORE
消耗 5000 或 20000 的 gas,所以这应该是你想要优化的)。使用 gas 估算器(启用优化器)来检查! - 将你的状态变量设置为 public ——编译器会为你自动创建 :ref:`getters <visibility-and-getters>` 。
- 如果你最终需要在函数开始位置检查很多输入条件或者状态变量的值,你可以尝试使用 :ref:`modifiers` 。
- 如果你的合约有一个
send
函数,但你想要使用内置的 send 函数,你可以使用address(contractVariable).send(amount)
。 - 使用一个赋值语句就可以初始化 struct:
x = MyStruct({a: 1, b: 2});
Note
如果存储结构具有“紧打包(tightly packed)”,可以用分开的赋值语句来初始化:x.a = 1; x.b = 2;
。这样可以使优化器更容易地一次性更新存储,使赋值的开销更小。
.. index:: precedence
以下是按评估顺序列出的操作符优先级。
优先级 | 描述 | 操作符 |
---|---|---|
1 | 后置自增和自减 | ++ , -- |
创建类型实例 | new <typename> |
|
数组元素 | <array>[<index>] |
|
访问成员 | <object>.<member> |
|
函数调用 | <func>(<args...>) |
|
小括号 | (<statement>) |
|
2 | 前置自增和自减 | ++ , -- |
一元运算的加和减 | + , - |
|
一元操作符 | delete |
|
逻辑非 | ! |
|
按位非 | ~ |
|
3 | 乘方 | ** |
4 | 乘、除和模运算 | * , / , % |
5 | 算术加和减 | + , - |
6 | 移位操作符 | << , >> |
7 | 按位与 | & |
8 | 按位异或 | ^ |
9 | 按位或 | | |
10 | 非等操作符 | < , > , <= , >= |
11 | 等于操作符 | == , != |
12 | 逻辑与 | && |
13 | 逻辑或 | || |
14 | 三元操作符 | <conditional> ? <if-true> : <if-false> |
15 | 赋值操作符 | = , |= , ^= , &= , <<= ,
>>= , += , -= , *= , /= ,
%= |
16 | 逗号 | , |
.. index:: assert, block, coinbase, difficulty, number, block;number, timestamp, block;timestamp, msg, data, gas, sender, value, now, gas price, origin, revert, require, keccak256, ripemd160, sha256, ecrecover, addmod, mulmod, cryptography, this, super, selfdestruct, balance, send
abi.encode(...) returns (bytes)
: :ref:`ABI <ABI>` - 对给定参数进行编码abi.encodePacked(...) returns (bytes)
:对给定参数执行 :ref:`紧打包编码 <abi_packed_mode>`abi.encodeWithSelector(bytes4 selector, ...) returns (bytes)
: :ref:`ABI <ABI>` - 对给定参数进行编码,并以给定的函数选择器作为起始的 4 字节数据一起返回abi.encodeWithSignature(string signature, ...) returns (bytes)
:等价于abi.encodeWithSelector(bytes4(keccak256(signature), ...)
block.blockhash(uint blockNumber) returns (bytes32)
:指定区块的区块哈希——仅可用于最新的 256 个区块且不包括当前区块;而 blocks 从 0.4.22 版本开始已经不推荐使用,由blockhash(uint blockNumber)
代替block.coinbase
(address
):挖出当前区块的矿工的地址block.difficulty
(uint
):当前区块的难度值block.gaslimit
(uint
):当前区块的 gas 上限block.number
(uint
):当前区块的区块号block.timestamp
(uint
):当前区块的时间戳gasleft() returns (uint256)
:剩余的 gasmsg.data
(bytes
):完整的 calldatamsg.gas
(uint
):剩余的 gas - 自 0.4.21 版本开始已经不推荐使用,由gesleft()
代替msg.sender
(address
):消息发送方(当前调用)msg.value
(uint
):随消息发送的 wei 的数量now
(uint
):当前区块的时间戳(等价于block.timestamp
)tx.gasprice
(uint
):交易的 gas pricetx.origin
(address
):交易发送方(完整调用链上的原始发送方)assert(bool condition)
:如果条件值为false
则中止执行并回退所有状态变更(用做内部错误)require(bool condition)
:如果条件值为false
则中止执行并回退所有状态变更(用做异常输入或外部组件错误)require(bool condition, string message)
:如果条件值为false
则中止执行并回退所有状态变更(用做异常输入或外部组件错误),可以同时提供错误消息revert()
:中止执行并回复所有状态变更revert(string message)
:中止执行并回复所有状态变更,可以同时提供错误消息blockhash(uint blockNumber) returns (bytes32)
:指定区块的区块哈希——仅可用于最新的 256 个区块keccak256(...) returns (bytes32)
:计算 :ref:`紧打包编码 <abi_packed_mode>` 的 Ethereum-SHA-3(Keccak-256)哈希sha3(...) returns (bytes32)
:等价于keccak256
sha256(...) returns (bytes32)
:计算 :ref:`紧打包编码 <abi_packed_mode>` 的 SHA-256 哈希ripemd160(...) returns (bytes20)
:计算 :ref:`紧打包编码 <abi_packed_mode>` 的 RIPEMD-160 哈希ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address)
:基于椭圆曲线签名找回与指定公钥关联的地址,发生错误的时候返回 0addmod(uint x, uint y, uint k) returns (uint)
:计算(x + y) % k
的值,其中加法的结果即使超过2**256
也不会被截取。从 0.5.0 版本开始会加入对k != 0
的 assert(即会在此函数开头执行assert(k != 0);
作为参数检查,译者注)。mulmod(uint x, uint y, uint k) returns (uint)
:计算(x * y) % k
的值,其中乘法的结果即使超过2**256
也不会被截取。从 0.5.0 版本开始会加入对k != 0
的 assert(即会在此函数开头执行assert(k != 0);
作为参数检查,译者注)。this
(类型为当前合约的变量):当前合约实例,可以准确地转换为address
super
:当前合约的上一级继承关系的合约selfdestruct(address recipient)
:销毁当前合约,把余额发送到给定地址suicide(address recipient)
:与selfdestruct
等价,但已不推荐使用<address>.balance
(uint256
): :ref:`address` 的余额,以 Wei 为单位<address>.send(uint256 amount) returns (bool)
:向 :ref:`address` 发送给定数量的 Wei,失败时返回false
<address>.transfer(uint256 amount)
:向 :ref:`address` 发送给定数量的 Wei,失败时会把错误抛出(throw)
Note
不要用 block.timestamp
、now
或者 blockhash
作为随机种子,除非你明确知道你在做什么。
时间戳和区块哈希都可以在一定程度上被矿工所影响。如果你用哈希值作为随机种子,那么例如挖矿团体中的坏人就可以使用给定的哈希来执行一个赌场功能,如果他们没赢钱,他们可以简单地换一个哈希再试。
当前区块的时间戳必须比前一个区块的时间戳大,但唯一可以确定的就是它会是权威链(主链或者主分支)上两个连续区块时间戳之间的一个数值。
Note
出于扩展性的原因,你无法取得所有区块的哈希。只有最新的 256 个区块的哈希可以拿到,其他的都将为 0。
.. index:: visibility, public, private, external, internal
function myFunction() <visibility specifier> returns (bool) { return true; }
public
:内部、外部均可见(参考为存储/状态变量创建 :ref:`getter 函数 <getter-functions>`)private
:仅在当前合约内可见external
:仅在外部可见(仅可修饰函数)——就是说,仅可用于消息调用(即使在合约内调用,也只能通过this.func
的方式)internal
:仅在内部可见(也就是在当前 Solidity 源代码文件内均可见,不仅限于当前合约内,译者注)
.. index:: modifiers, pure, view, payable, constant, anonymous, indexed
pure
修饰函数时:不允许修改或访问状态——但目前并不是强制的。view
修饰函数时:不允许修改状态——但目前不是强制的。payable
修饰函数时:允许从调用中接收 |ether| 。constant
修饰状态变量时:不允许赋值(除初始化以外),不会占据 |storage_slot| 。constant
修饰函数时:与view
等价。anonymous
修饰事件时:不把事件签名作为 topic 存储。indexed
修饰事件时:将参数作为 topic 存储。
以下是 Solidity 的保留字,未来可能会变为语法的一部分:
abstract
, after
, alias
, apply
, auto
, case
, catch
, copyof
, default
,
define
, final
, immutable
, implements
, in
, inline
, let
, macro
, match
,
mutable
, null
, of
, override
, partial
, promise
, reference
, relocatable
,
sealed
, sizeof
, static
, supports
, switch
, try
, type
, typedef
, typeof
,
unchecked
.
.. literalinclude:: grammar.txt :language: none