数据位置
所有的复杂类型,即 数组 和 结构 类型,都有一个额外属性,“数据位置”,说明数据是保存在 内存memory 中还是 存储storage 中。 根据上下文不同,大多数时候数据有默认的位置,但也可以通过在类型名后增加关键字 storage
或 memory
进行修改。 函数参数(包括返回的参数)的数据位置默认是 memory
, 局部变量的数据位置默认是 storage
,状态变量的数据位置强制是 storage
(这是显而易见的)。
也存在第三种数据位置, calldata
,这是一块只读的,且不会永久存储的位置,用来存储函数参数。 外部函数的参数(非返回参数)的数据位置被强制指定为 calldata
,效果跟 memory
差不多。
数据位置的指定非常重要,因为它们影响着赋值行为: 在 存储storage 和 内存memory 之间两两赋值,或者 存储storage 向状态变量(甚至是从其它状态变量)赋值都会创建一份独立的拷贝。 然而状态变量向局部变量赋值时仅仅传递一个引用,而且这个引用总是指向状态变量,因此后者改变的同时前者也会发生改变。 另一方面,从一个 内存memory 存储的引用类型向另一个 内存memory 存储的引用类型赋值并不会创建拷贝。
pragma solidity ^0.4.0; contract C { uint[] x; // x 的数据存储位置是 storage // memoryArray 的数据存储位置是 memory function f(uint[] memoryArray) public { x = memoryArray; // 将整个数组拷贝到 storage 中,可行 var 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 storageArray) internal {} function h(uint[] memoryArray) public {} }
总结
- 强制指定的数据位置:
-
- 外部函数的参数(不包括返回参数): calldata
- 状态变量: storage
- 默认数据位置:
-
- 函数参数(包括返回参数): memory
- 所有其它局部变量: storage
深入分析
storage 存储结构是在合约创建的时候就确定好了的,它取决于合约所声明状态变量。但是内容可以被(交易)调用改变。
Solidity 称这个为状态改变,这也是合约级变量称为状态变量的原因。也可以更好的理解为什么状态变量都是storage存储。
memory 只能用于函数内部,memory 声明用来告知EVM在运行时创建一块(固定大小)内存区域给变量使用。
storage 在区块链中是用key/value的形式存储,而memory则表现为字节数组
关于栈(stack)
EVM是一个基于栈的语言,栈实际是在内存(memory)的一个数据结构,每个栈元素占为256位,栈最大长度为1024。 值类型的局部变量是存储在栈上。
不同存储的消耗(gas消耗)
-
storage 会永久保存合约状态变量,开销最大
-
memory 仅保存临时变量,函数调用之后释放,开销很小
-
stack 保存很小的局部变量,几乎免费使用,但有数量限制。