Solidity 数据存储
每种复杂类型(即数组和结构)都有一个额外的注释,即“数据位置”,它是存储在 memory 还是 storage 中。 根据上下文,总是有一个默认值,但可以通过将存储或内存添加到该类型来覆盖它。 最后一种数据位置比较特殊,一般只有外部函数的参数(不包括返回参数)被强制指定为 calldata。这种数据位置是只读的,不会持久化到区块链。一般我们可以选择指定的是 memory 和 storage。
数据位置很重要,因为它们会改变分配的行为方式:storage 和 memory 之间的分配以及状态变量(甚至来自其他状态变量)的分配始终会创建独立存储空间。 另一方面,从内存存储引用类型到另一个内存存储引用类型的分配不会创建存储空间。
状态变量、局部变量
pragma solidity ^0.4.4;
contract Person {
int public _age;
string public _name;
function Person(int age,string name) {
_age = age;
_name = name;
}
function f(string name) {
var name1 = name;
}
function modifyAge(int age) {
age = 100;
}
}
- _age,_name 就属于状态变量
- Person(int age,string name) 中的 age 和 name,还有 f(string name) 中的 name 以及 f() 方法中声明的 name1 都默认属于本地/局部变量。
因为_age 是值类型,所以在方法传参或者将值类型的变量值赋值给一个新变量,当尝试修改新变量时,原来的值类型变量值并不会发生任何变化,在本例中,当调用 modifyAge(_age) 代码时,创建了一个临时变量 age,并且将_age 的值传给了 age,因为是值传递,在 modifyAge 函数中修改新变量 age 的值时,原来的变量值_age 的值保持不变。
Memory
Memory(值传递)
当引用类型作为方法参数时,它的类型默认为 memory,写成 string memory name = name;,或者 var _name = name 效果都是一样的。
var 声明一个变量时,这个变量的类型最终由赋给它值的类型决定。
联想
还记得 mapping 么?mapping 不存储它们的 key,通过计算出 key 的 sha3 值,把它存储在 storage 中。 任何查找 mapping 必须提供原始 key 或能够计算它。
memory 的空间排布
Solidity 保留 3 个 256 位的存储插槽。
- 0 —— 64 位:用于 hash 方法的临时空间
- 64——96 位:当前空闲 memory 大小
临时空间也可用于存储状态声明。Solidity 会为 memory 存储开辟新存储空间,因为 memory 不会被释放。
注意:这里说的是临时空间。
官方解释:Solidity 中有一些操作需要大于 64 字节的 memory,因此不适合临时空间。 它们将被放置在空闲内存的位置,但由于其生命周期较短,指针不会更新。 内存可能会或可能不会被清零。 正因为如此,开发者不应该期望内存自己会被清零。
所以建议读者还是按最坏的打算,能手动清零就手动清零。
技巧 tips:
- delete 数组元素
- 变量手动置零
- 在 struct 中使用较短的类型,并对它们进行排序,以便将短类型组合在一起。在虚拟机层,存储短类型的指令是 SSTORE(gas 消耗 5000 —— 20000),多个 SSTORE 操作会合并为一个。排序比如:uint128、uint128、uint256,而不是 uint128、uint256、uint128。
- 把合约中状态变量声明为 public,这样自动生成 getter
- function 要有条件检查的话,使用 modifier。实践证明 gas 消耗少。
- 赋值初始化
struct:x = MyStruct({a: 1, b: 2});
storage
storage(指针传递)
pragma solidity ^0.4.4;
contract Person {
string public _name;
function Person() {
_name = "lisi";
}
function f() {
modifyName(_name);
}
function modifyName(string storage name) internal {
var name1 = name;
}
}
如果想要在 modifyName 函数中通过传递过来_name 来修改它的值,那么必须将方法参数的类型显示设置为 storage 类型,storage 类型拷贝的不是值,而是参数指针,当调用 modifyName(_name) 时,相当于把_name,name,name1 三个指针同时指向同一个对象,可以近似认为修改三个中任一个都可修改对应的值。
注意:
当方法参数为 storage 类型时,方法的类型必须为internal 或者 private。
状态变量的空间排布
静态变量(除 mapping 和动态数组类型之外的)都从位置 0 开始在 storage 中连续排布。尽可能的要少于 32 个字节的各个项目被打包到一个存储槽(slot) 中。
- 存储槽中的第一项是被低位对齐的。
- 基本类型只使用存储它们所需的那么多字节。
- 如果基本类型不适合存储插槽的其余部分,则将其移动到下一个存储插槽。
- 结构和数组数据总是会启动一个新的槽并占据整个槽(结构或数组内的项会根据他们的索引紧密排列)。
根据上述规则,在未填充情况下,mapping 或动态数组本身占据某个位置 p 的存储空间。 对于动态数组,一个存储槽就存储了数组中所有元素,数组数据位于 keccak256(p)。 对于长度为零的 mapping,该槽未被使用但已被占用,mapping key 的值位于 keccak256(k+p),如果该 key 对应的 value 是非基本类型,则通过添加 keccak256(k+p)的偏移量来查找位置。
bytes 和 string 都存储在同一插槽,如果他们是短类型那么也同时存储长度。注意:如果该数据已经有 31 字节长度,它存储在高位字节中(左对齐),低位字节存储 length * 2,如果更长空间不够的话,主插槽存储 length * 2 + 1,数据 value 存储在 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
注意:
在 for (var i = 0; i < arrayName.length; i++) { ... }
,i 的类型是 uint8,因为这是存放值 0 最小的类型。如果数组元素超过 255 个循环将不会终止。因为 2 的 8 次方是 256。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 以太坊交易流程
下一篇: 彻底找到 Tomcat 启动速度慢的元凶
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论