数据位置

本章讲解在 Solidity 中,数据的三种存储位置:storagememorycalldata

视频Bilibili  |  Youtube
官网binschool.app
推特@BinSchool    DiscordBinDAO   微信:bkra50 

Solidity 中的数据的存储位置有 3 种:memorystoragecalldata

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 的值。

dataRefdata 实际上指向了同一块数据,也可以说,dataRefdata 的别名。

所以,修改了 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 的数据是只读的,不能修改。

storagememory 相比,calldata 存储的成本最低,gas 消耗最少。

calldata 只能用于函数参数,无法在函数内部声明。

例如:

function setName(string calldata name) external;

calldata 的使用场景并不多,在函数内部,通常会转为 memory 再去操作。但在某些场景下,出于节省 gas 的目的,也会使用 calldata 变量。

注意:在函数中,值类型的变量通常分配在栈上,不在上面的三种存储中,所以也就不存在变量声明时,指定它的存储位置。