数据位置
本章讲解在 Solidity
中,数据的三种存储位置:storage
、memory
和 calldata
。
Solidity
中的数据的存储位置有 3 种:memory
、storage
、calldata
。
1. storage
storage
是指永久保存在区块链上的存储,通常用于存储合约的状态变量。
storage
中的变量在合约部署后会一直存在,直到合约被销毁。
由于它保存在区块链上,需要同步到所有区块链节点,而且永久保存,所以它的使用成本高,gas
消耗多。
比如:
contract StorageVar { string name = "BinSchool.app"; // 声明状态变量 }
name
是一个状态变量,存储在 storage
中,它的数据会一直保存在区块链上。
在函数中,对于“引用类型”的状态变量,我们可以通过关键字 storage
来引用它。例如:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract StorageVar { // 引用类型的状态变量 uint[] data = [1,2,3]; // 修改状态变量 function update() external { // 变量 dataRef 引用了状态变量 data uint[] storage dataRef = data; // 修改 dataRef dataRef[0] = 100; } // 打印状态变量 function print() external view returns(uint, uint, uint) { return (data[0],data[1],data[2]); } }
在这里,我们为状态变量 data
创建了一个引用 dataRef
,然后修改了 dataRef
的值。
dataRef
和 data
实际上指向了同一块数据,也可以说,dataRef
是 data
的别名。
所以,修改了 dataRef
指向的数据,也就是修改了 data
指向的数据。
我们可以把这个合约部署到 Remix
上。先调用 print
函数打印状态变量 data
的值,它的值为 1,2,3。
然后点击 update
函数,再次调用 print
函数,data
的值变成了 100,2,3。
其实,我们直接修改 data
,也可以达到同样的效果,那为什么要使用 storage
引用呢?
主要两个原因:一是节省 gas
,二是提高代码的可读性。
Solidity
编译器优化了使用引用的代码,减少了 sload
操作,所以比直接修改状态变量节省 gas
。
2. memory
memory
是函数调用期间分配的临时内存,通常用于存储引用类型的局部变量。
memory
中的变量在函数调用结束后会被销毁。它对应于其它编程语言中的 “堆”。
memory
的使用成本非常低,消耗的 gas
少。
contract MemoryVar { function name() public pure returns(string memory){ string memory s = "BinSchool.app"; // 声明局部变量 s return s; } }
函数 name
中的变量 s
,存储在 memory
中。当函数调用结束后,就会从内存中清除。
在函数中,对于“引用类型”的状态变量,我们可以通过关键字 memory
来创建它的副本。
我们依旧拿上面 storage
中例子,进行分析:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract MemoryVar { // 引用类型的状态变量 uint[] data = [1,2,3]; // 修改状态变量 function update() external { // 变量 dataCopy 创建了状态变量 data 的副本 uint[] memory dataCopy = data; // 修改 dataCopy dataCopy[0] = 100; } // 打印状态变量 function print() external view returns (uint, uint, uint) { return (data[0],data[1],data[2]); } }
在上面的合约中,我们为状态变量 data
创建了一个副本 dataCopy
,然后修改了 dataCopy
的值。
dataCopy
实际上复制了一份 data
的数据,两者各自指向一份独立的数据。所以,修改了 dataCopy
的数据,并不会改变 data
的数据值。
我们把这个合约部署到 Remix
上。先调用 print
函数打印状态变量 data
的值,它的值为 1,2,3。
然后点击 update
函数,再次调用 print
函数,data
的值依然是 1,2,3。
Solidity
中对 memory
变量的操作,几乎不消耗 gas
。所以,在函数中多次操作一个状态变量,最好为它创建了一个副本,这样比直接操作状态变量更节省 gas
。
3. calldata
calldata
是外部程序在调用合约函数时,用来保存传入参数的存储位置。
calldata
变量的行为类似于 memory
变量,它在函数调用结束后就会被销毁。两者不同之处在于,calldata
的数据是只读的,不能修改。
与 storage
和 memory
相比,calldata
存储的成本最低,gas
消耗最少。
calldata
只能用于函数参数,无法在函数内部声明。
例如:
function setName(string calldata name) external;
calldata
的使用场景并不多,在函数内部,通常会转为 memory
再去操作。但在某些场景下,出于节省 gas
的目的,也会使用 calldata
变量。
注意:在函数中,值类型的变量通常分配在栈上,不在上面的三种存储中,所以也就不存在变量声明时,指定它的存储位置。