在 Solidity 中,storage、memory 和 calldata 是三种关键的数据位置,它们决定了变量的存储位置、生命周期和行为。
1. storage(存储区)
- 位置:永久存储在区块链上,是合约状态变量存储的位置
- 生命周期:与合约生命周期相同,永久存储
- 成本:读写成本高(写入消耗大量 gas,读取相对便宜)
- 特点 :
-
引用类型,传递的是引用(类似指针)
-
修改会影响原始数据
-
用于状态变量
contract Example {
uint[] public arr; // storage变量function storageExample() public { uint[] storage s = arr; // storage引用 s.push(1); // 直接修改arr }}
-
2. memory(内存区)
- 位置:临时存储在内存中,函数执行期间存在
- 生命周期:仅在函数执行期间存在
- 成本:读写成本低,但大型数组可能需要较多 gas
- 特点 :
3. calldata(调用数据区)
- 位置:只读的调用数据,存储函数参数
- 生命周期:仅在函数调用期间存在
- 成本:只读,gas 成本最低
- 特点 :
-
不可修改的只读区域
-
通常用于外部函数(external)的参数
-
避免不必要的复制,节省 gas
function calldataExample(bytes calldata data) external pure returns (bytes memory) {
// data[0] = 0x01; // 错误!calldata不可修改
bytes memory copy = data; // 可以复制到memory进行修改
return data; // 可以直接返回(需要转为memory)
}
-
对比表格
| 特性 | storage | memory | calldata |
|---|---|---|---|
| 存储位置 | 区块链 | 临时内存 | 调用数据 |
| 生命周期 | 永久 | 函数执行期间 | 函数调用期间 |
| 可修改性 | 可读写 | 可读写 | 只读 |
| 传递方式 | 引用 | 值/副本 | 引用 |
| Gas 成本 | 高 | 中等 | 低 |
| 适用场景 | 状态变量 | 函数内部变量 | 外部函数参数 |
使用规则和最佳实践
1. 参数数据位置规则
// public/internal函数:参数默认memory
function func1(uint[] memory arr) public {}
// external函数:引用类型参数默认calldata
function func2(uint[] calldata arr) external {}
// 错误:external函数不能使用memory参数(但新版本已允许)
2. 返回值数据位置
function returnsMemory() public pure returns (uint[] memory) {
uint[] memory arr = new uint[](3);
return arr; // 返回memory数据
}
function returnsStorage() public view returns (uint[] storage) {
return arr; // 返回storage引用
}
3. 赋值规则
uint[] storage s = arr; // storage → storage ✓
uint[] memory m = arr; // storage → memory ✓(复制)
uint[] memory m2 = m; // memory → memory ✓(复制)
// uint[] storage s2 = m; // memory → storage ✗(不允许)
// uint[] calldata c = arr; // storage → calldata ✗
4. Gas 优化技巧
// 使用calldata节省gas(避免复制)
function processData(bytes calldata data) external {
// 直接读取calldata,不复制到memory
require(data.length > 0);
}
// 需要修改时再复制到memory
function modifyData(bytes calldata data) external {
bytes memory modified = data; // 复制到memory
modified[0] = 0x01; // 进行修改
}
常见错误
contract CommonErrors {
uint[] public arr;
// 错误:未指定数据位置
// function error1(uint[] a) public {}
// 错误:尝试修改calldata
// function error2(uint[] calldata a) external {
// a[0] = 1; // 编译错误
// }
// 正确:明确指定数据位置
function correct(uint[] memory a) public {
uint[] memory local = a; // 复制到memory
local[0] = 1; // 可以修改
}
}
总结
- storage:用于永久存储的状态变量
- memory:用于函数内部的临时变量,可修改
- calldata:用于外部函数参数,只读,最节省 gas
选择合适的数据位置对于优化 Gas 消耗和确保合约正确性至关重要。通常:
- 外部函数参数使用
calldata - 内部函数参数和局部变量使用
memory - 需要修改状态时使用
storage引用
注:
强制数据位置类型:
• 可见性为external的函数入参的数据类型(Data Location)必须是calldata类型。
•状态变量的数据位置(Data Location)类型必须是storage类型。
默认数据类型:
• 一个函数的入参和出参的数据位置(Data Location)类型默认是memory类型。(external函数的入参除外,因为强制为calldata类型)
• 除了入参和出参之外的所有局部变量的数据位置(Data Location)类型默认为storage。