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 gascalldata: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 { }
快速参考表
什么时候用什么?
| 场景 | 使用 | 示例 |
|---|---|---|
| 状态变量 | 自动 storage | uint256[] public arr; |
| external 函数参数(只读) | calldata | function f(uint[] calldata data) |
| external 函数参数(需修改) | memory | function f(uint[] memory data) |
| public 函数参数 | memory | function f(uint[] memory data) |
| internal 函数参数(引用) | storage | function f(uint[] storage data) |
| internal 函数参数(复制) | memory | function f(uint[] memory data) |
| 函数返回值 | memory | returns (uint[] memory) |
| 局部临时变量 | memory | uint[] memory temp; |
| 局部引用状态变量 | storage | uint[] storage ref = arr; |
简单记忆口诀
- 状态变量 = 自动 storage,不能写
- external 参数 = 优先 calldata(省 gas)
- public 参数 = 只能 memory
- 返回值 = 必须 memory
- 局部变量 = 引用类型必须声明
版本说明
- Solidity 0.5.x 及更早:返回 storage 数组需手动复制
- Solidity 0.6.0+:编译器自动处理 storage → memory 转换
- Solidity 0.8.0+:当前主流版本,完全支持自动转换
总结
核心原则
- ⚡ 优化 Gas:能用 calldata 就不用 memory
- 🎯 明确意图:storage 引用 vs memory 复制
- 📝 遵循规则:状态变量自动 storage,局部变量必须声明
- 🔒 只读场景: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);
}
}