- 这两天的学习,感觉对于solidity代码中的变量的存储问题一头雾水,头都被搞晕了。
- 暂时有一点点思路,不知道是否完全正确,先梳理一下
EVM虚拟机
- 自己觉得要清楚其中变量的问题,首先要明白EVM虚拟机的组件构成。
- 这里保存两种对它的描述,一是在《精通以太坊》这本书中的,二是在以太坊黄皮书中的描述。
《精通以太坊》:
- 1、一个不可变的程序代码存储区ROM,加载了要执行二点智能合约的字节码;
- 2、一个内容可变的内存(memory),它被严格地初始化为全0数值;
- 3、一个永久的存储,它是作为以太坊状态的一部分的存在,也会被初始化为全0;
- 4、一个数据栈。
黄皮书:
- EVM是一个简单的基于堆栈体系结构。机器的字(word)大小为256位(bit)(因此堆栈元素的大小也是如此)。这是为了方便Keccak256哈希和椭圆曲线计算而选择的。
- 内存(memory)模型是一个简单的按字寻址的字节数组
- 堆栈(stack)的最大尺寸为1024
- 虚拟机同样还有一个独立的存储模型(storage),这在概念上类似于内存,但它不是字节(byte)数组,而是一个可寻址的字(word)数组。与内存(memory)不同的是,存储是非易失性的,并作为系统状态的一部分被维护。
- 存储和内存中的所有位置都被初始化为0。
- EVM不符合冯诺依曼的标准结构。它不是将程序代码存储在通常可访问的内存或存储器中,而是单独存储在虚拟ROM中,该虚拟ROM只能通过专门的指令进行交互。
- 由于多种原因,机器可能会有异常执行,包括堆栈下溢和无效指令。机器会立即停止并将问题报告给执行代理做出相应的处理。
状态变量
- 在solidity的文档中描述:Solidity 合约类似于面向对象语言中的类。合约中有用于数据持久化的状态变量,和可以修改状态变量的函数。 而状态变量是永久地存储在合约存储(storage)中的值。
pragma solidity ^0.4.0;
contract SimpleStorage {
uint storedData; // 状态变量
// ...
}
- 以上这一点与传统编程语言的存储方式有别,感觉还挺重要的。
数据位置
- 所有的复杂类型,即 数组 和 结构 类型,都有一个额外属性,“数据位置”,说明数据是保存在 内存(memory) 中还是 存储(storage) 中。 根据上下文不同,大多数时候数据有默认的位置,但也可以通过在类型名后增加关键字 storage 或 memory 进行修改。 函数参数(包括返回的参数)的数据位置默认是 memory, 局部变量的数据位置默认是 storage,状态变量的数据位置强制是 storage (这是显而易见的)。
- 也存在第三种数据位置, calldata ,这是一块只读的,且不会永久存储的位置,用来存储函数参数。 外部函数的参数(非返回参数)的数据位置被强制指定为 calldata ,效果跟 memory 差不多。
- 总结:
- 强制指定的数据位置:
- 1、外部函数的参数(不包括返回参数): calldata
- 2、状态变量: storag
- 默认数据位置:
- 1、函数参数(包括返回参数): memory
- 2、所有其它局部变量: storage
- 关于数据位置对赋值行为的影响:
- 1、 在 存储(storage)和 内存(memory)之间两两赋值,或者 存储(storage)向状态变量(甚至是从其它状态变量)赋值都会创建一份独立的拷贝;
- 2、状态变量向局部变量赋值时仅仅传递一个引用,而且这个引用总是指向状态变量,因此后者改变的同时前者也会发生改变;
- 3、从一个 内存(memory) 存储的引用类型向另一个内存(memory) 存储的引用类型赋值并不会创建拷贝。
- 以下是solidity文档中给出实例代码:
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 {}
}
关于数组的问题
- 动态数组可以创建在memory和storage中。
- storage中的数组:
- 1、数组有 length 成员变量表示当前数组的长度。 动态数组可以在存储storage 中通过改变成员变量 .length 改变数组大小。 并不能通过访问超出当前数组长度的方式实现自动扩展数组的长度。
- 2、变长的存储storage 数组以及 bytes 类型(而不是 string 类型)都有一个叫做 push 的成员函数,它用来附加新的元素到数组末尾。 这个函数将返回新的数组长度。
- memory中的数组:
pragma solidity ^0.4.16;
contract C {
function f(uint len) public pure {
uint[] memory a = new uint[](7);
bytes memory b = new bytes(len);
// 这里我们有 a.length == 7 以及 b.length == len
a[6] = 8;
}
}
- 2、可使用 new 关键字在内存中创建变长数组。 与 存储storage 数组相反的是,不能通过修改成员变量 .length 改变 内存memory 数组的大小。
- 3、 一经创建,内存memory 数组的大小就是固定的(但却是动态的,也就是说,它依赖于运行时的参数)。
网友评论