Skip to main content

Solidity 数据存储位置完全指南

📚 目录


基本概念

官方文档:https://docs.soliditylang.org/zh-cn/v0.8.24/types.html#data-location-assignment Solidity 中的数据存储位置(Data Location)指定了变量存储在哪里,主要有三种:

数据位置存储位置可修改生命周期Gas 成本
storage区块链状态永久⚠️⚠️⚠️ 极高 (写入 ~20k gas)
memory临时内存函数执行期间✅ 中等
calldata交易输入数据❌ 只读函数执行期间✅✅ 最低(无复制)

三种数据位置详解

📦 storage - 永久存储

特点:

  • 存储在区块链状态中,永久保存
  • 写入成本极高(约 20,000 gas)
  • 读取成本也较高(2,100 gas 首次读取)
  • 状态变量自动使用 storage

语法规则:

contract Example {
// ✅ 状态变量默认是 storage,不能也不需要显式声明
uint256[] public myArray;
mapping(address => uint256) public balances;

// ❌ 错误!不能给状态变量加 storage 关键字
// uint256[] storage public myArray; // 编译错误

function modify() public {
// ✅ 局部变量可以引用 storage
uint256[] storage arr = myArray; // 引用,不是复制
arr.push(100); // 直接修改原数组
}
}

使用场景:

  • 状态变量(自动)
  • 需要持久化的数据
  • 需要在函数间共享的数据

💾 memory - 临时内存

特点:

  • 存储在函数执行期间的临时内存中
  • 函数执行结束后数据被清除
  • Gas 成本适中
  • 数据可以修改

语法规则:

function process(uint256[] memory data) public returns (uint256[] memory) {
// ✅ 创建临时数组
uint256[] memory result = new uint256[](10);

// ✅ 可以修改
result[0] = 100;
data[0] = 200;

// ✅ 从 storage 复制到 memory
uint256[] memory copy = myStorageArray;

return result;
}

使用场景:

  • 函数内的临时变量
  • 函数参数(public/internal 函数)
  • 函数返回值
  • 需要修改但不保存的数据

📋 calldata - 只读外部数据

特点:

  • 存储在交易的输入数据区
  • 只读,不可修改
  • Gas 成本最低(不需要复制)
  • 只能用于 external 函数的参数

语法规则:

// ✅ 推荐:external 函数使用 calldata 省 gas
function process(uint256[] calldata data) external {
uint256 value = data[0]; // ✅ 可以读取

// ❌ 错误!calldata 是只读的
// data[0] = 100; // 编译错误

// ✅ 如果需要修改,复制到 memory
uint256[] memory mutableData = data;
mutableData[0] = 100;
}

// ❌ 错误!internal/private 函数不能使用 calldata
// function internal_func(uint256[] calldata data) internal { } // 编译错误

使用场景:

  • external 函数的参数(强烈推荐)
  • 大型数组/结构体传参时省 gas
  • 不需要修改的数据

使用规则总结

1️⃣ 状态变量

contract Rules {
// ✅ 状态变量永远是 storage,自动的
uint256 public number;
uint256[] public array;
mapping(address => uint256) public map;

// ❌ 不能指定其他位置
// uint256 memory number; // 错误
}

2️⃣ 函数参数

contract Rules {
// ✅ external 函数:推荐 calldata(省 gas)
function f1(uint256[] calldata data) external { }

// ✅ external 函数:也可以用 memory(可修改)
function f2(uint256[] memory data) external { }

// ✅ public 函数:只能用 memory
function f3(uint256[] memory data) public { }

// ✅ internal/private 函数:只能用 memory 或 storage
function f4(uint256[] memory data) internal { }
function f5(uint256[] storage data) internal { }

// ❌ 错误示例
// function f6(uint256[] storage data) external { } // 错误
// function f7(uint256[] calldata data) public { } // 错误
}

3️⃣ 函数返回值

contract Rules {
uint256[] private myArray;

// ✅ 返回值必须是 memory
function getArray() external view returns (uint256[] memory) {
return myArray; // 自动从 storage 复制到 memory
}

// ❌ 错误示例
// function getArray() external view returns (uint256[] storage) { } // 错误
// function getArray() external view returns (uint256[] calldata) { } // 错误
}

4️⃣ 局部变量(引用类型)

function example() public {
// ✅ 引用类型必须指定数据位置
uint256[] memory arr1 = new uint256[](10);
uint256[] storage arr2 = myStorageArray;

// ✅ 值类型不需要指定(自动 memory)
uint256 num = 100;
address addr = msg.sender;

// ❌ 错误:引用类型没有指定位置
// uint256[] arr; // 编译错误
}

实战示例

示例 1:状态变量操作

contract StateExample {
uint256[] public vestingTimes;

// ✅ 使用 storage 引用避免多次 SLOAD
function batchAdd() public {
uint256[] storage times = vestingTimes; // 引用
for(uint i = 0; i < 10; i++) {
times.push(block.timestamp + i * 1 days);
}
// 直接修改了 storage,无需赋值回去
}

// ❌ 低效做法
function batchAddBad() public {
for(uint i = 0; i < 10; i++) {
vestingTimes.push(block.timestamp + i * 1 days);
// 每次都要访问 storage
}
}
}

示例 2:数组返回(版本演进)

contract ArrayReturn {
uint256[] public data;

// ✅ Solidity 0.6.0+ / 0.8.0+
// 编译器自动处理 storage -> memory 转换
function getData() external view returns (uint256[] memory) {
return data; // 简洁
}

// ❌ Solidity 0.5.x 及更早(现在不需要了)
function getDataOld() external view returns (uint256[] memory) {
uint256[] memory result = new uint256[](data.length);
for(uint i = 0; i < data.length; i++) {
result[i] = data[i];
}
return result;
}
}

示例 3:参数优化

contract ParamOptimization {
// ✅ 最佳实践:external + calldata
function processData(
uint256[] calldata data,
string calldata name
) external pure returns (uint256) {
uint256 sum = 0;
for(uint i = 0; i < data.length; i++) {
sum += data[i];
}
return sum;
// 省 gas:不需要复制数据到 memory
}

// ❌ 次优:external + memory(浪费 gas)
function processDataBad(
uint256[] memory data // 会复制数据,浪费 gas
) external pure returns (uint256) {
uint256 sum = 0;
for(uint i = 0; i < data.length; i++) {
sum += data[i];
}
return sum;
}
}

示例 4:数据修改

contract DataModification {
uint256[] public numbers;

// ✅ 需要修改数据时用 memory
function getDoubled() external view returns (uint256[] memory) {
uint256[] memory result = new uint256[](numbers.length);
for(uint i = 0; i < numbers.length; i++) {
result[i] = numbers[i] * 2; // 修改数据
}
return result;
}

// ✅ 直接修改 storage
function doubleInPlace() external {
uint256[] storage nums = numbers; // storage 引用
for(uint i = 0; i < nums.length; i++) {
nums[i] *= 2; // 直接修改原数组
}
}
}

示例 5:复杂结构体

contract StructExample {
struct User {
string name;
uint256[] scores;
}

User[] public users;

// ✅ 修改 storage 中的结构体
function updateUser(uint index, string memory newName) external {
User storage user = users[index]; // storage 引用
user.name = newName;
user.scores.push(100);
}

// ✅ 返回结构体(自动复制到 memory)
function getUser(uint index) external view returns (User memory) {
return users[index];
}

// ✅ external 函数接收结构体参数
function addUser(User calldata user) external {
// 从 calldata 复制到 storage
users.push(user);
}
}

Gas 优化技巧

✅ 技巧 1:external 函数优先使用 calldata

// ❌ 浪费 gas(复制数据)
function process(uint256[] memory data) external {
// ...
}

// ✅ 节省 gas(直接引用)
function process(uint256[] calldata data) external {
// 如果不需要修改,用 calldata 可以节省大量 gas
}

Gas 对比:

  • memory:10 个元素约 1,000 gas
  • calldata:10 个元素约 200 gas
  • 节省约 80% gas!

✅ 技巧 2:使用 storage 引用避免重复访问

// ❌ 多次访问 storage(昂贵)
function bad() public {
myArray.push(1); // SLOAD + SSTORE
myArray.push(2); // SLOAD + SSTORE
myArray.push(3); // SLOAD + SSTORE
}

// ✅ 使用引用(只需一次 SLOAD)
function good() public {
uint256[] storage arr = myArray; // 一次 SLOAD
arr.push(1); // 直接操作
arr.push(2);
arr.push(3);
}

✅ 技巧 3:批量操作优化

// ✅ 批量读取:一次性读到 memory
function batchRead() external view returns (uint256) {
uint256[] memory arr = myStorageArray; // 一次性复制
uint256 sum = 0;
for(uint i = 0; i < arr.length; i++) {
sum += arr[i]; // memory 访问很便宜
}
return sum;
}

// ❌ 逐个访问 storage
function batchReadBad() external view returns (uint256) {
uint256 sum = 0;
for(uint i = 0; i < myStorageArray.length; i++) {
sum += myStorageArray[i]; // 每次都访问 storage
}
return sum;
}

常见错误与解决

❌ 错误 1:状态变量指定数据位置

// ❌ 错误
contract Bad {
uint256[] storage public myArray; // 编译错误
}

// ✅ 正确
contract Good {
uint256[] public myArray; // 状态变量自动是 storage
}

❌ 错误 2:局部变量未指定数据位置

function bad() public {
uint256[] arr; // ❌ 编译错误:未指定数据位置
}

function good() public {
uint256[] memory arr = new uint256[](10); // ✅ 正确
}

❌ 错误 3:修改 calldata 数据

function bad(uint256[] calldata data) external {
data[0] = 100; // ❌ 编译错误:calldata 是只读的
}

function good(uint256[] calldata data) external {
uint256[] memory mutableData = data; // ✅ 复制到 memory
mutableData[0] = 100; // ✅ 可以修改
}

❌ 错误 4:返回 storage 引用

uint256[] private myArray;

// ❌ 错误:不能返回 storage 引用
function bad() external view returns (uint256[] storage) {
return myArray;
}

// ✅ 正确:返回 memory
function good() external view returns (uint256[] memory) {
return myArray; // 自动复制到 memory
}

❌ 错误 5:public 函数使用 calldata

// ❌ 错误:public 函数不能用 calldata
function bad(uint256[] calldata data) public {
// 编译错误
}

// ✅ 正确:改为 external 或使用 memory
function good1(uint256[] calldata data) external { }
function good2(uint256[] memory data) public { }

快速参考表

什么时候用什么?

场景使用示例
状态变量自动 storageuint256[] public arr;
external 函数参数(只读)calldatafunction f(uint[] calldata data)
external 函数参数(需修改)memoryfunction f(uint[] memory data)
public 函数参数memoryfunction f(uint[] memory data)
internal 函数参数(引用)storagefunction f(uint[] storage data)
internal 函数参数(复制)memoryfunction f(uint[] memory data)
函数返回值memoryreturns (uint[] memory)
局部临时变量memoryuint[] memory temp;
局部引用状态变量storageuint[] storage ref = arr;

简单记忆口诀

  1. 状态变量 = 自动 storage,不能写
  2. external 参数 = 优先 calldata(省 gas)
  3. public 参数 = 只能 memory
  4. 返回值 = 必须 memory
  5. 局部变量 = 引用类型必须声明

版本说明

  • Solidity 0.5.x 及更早:返回 storage 数组需手动复制
  • Solidity 0.6.0+:编译器自动处理 storage → memory 转换
  • Solidity 0.8.0+:当前主流版本,完全支持自动转换

总结

核心原则

  1. 优化 Gas:能用 calldata 就不用 memory
  2. 🎯 明确意图:storage 引用 vs memory 复制
  3. 📝 遵循规则:状态变量自动 storage,局部变量必须声明
  4. 🔒 只读场景:calldata 是最佳选择

最佳实践

contract BestPractice {
uint256[] public data; // 状态变量:自动 storage

// ✅ external + calldata:最省 gas
function read(uint256[] calldata input) external view returns (uint256) {
return input[0];
}

// ✅ 需要修改:用 memory
function modify(uint256[] memory input) external pure returns (uint256[] memory) {
input[0] = 100;
return input;
}

// ✅ 操作状态变量:用 storage 引用
function updateData() external {
uint256[] storage arr = data;
arr.push(100);
}
}
📢 Share this article