5月28日 02:37
Solidity 中 storage、memory 和 calldata 三种数据位置的区别是什么?
在 Solidity 中,storage、memory 和 calldata 是三种数据位置修饰符,决定数据的存储方式、生命周期和 Gas 开销。核心区别:storage 永久存链上,memory 是临时可变内存,calldata 是临时只读调用数据。
直接回答
| 数据位置 | 持久性 | 可修改 | Gas 成本 | 默认适用 |
|---|---|---|---|---|
| storage | 永久(链上) | 可读写 | 最高 | 状态变量 |
| memory | 临时(函数内) | 可读写 | 中等 | 函数参数、局部引用类型 |
| calldata | 临时(函数内) | 只读 | 最低 | external 函数的引用类型参数 |
面试一句话总结:storage 是链上持久存储,读写最贵;memory 是临时内存,函数结束即释放;calldata 是只读的调用输入,external 函数参数强制使用,Gas 最省。
追问一:赋值时是拷贝还是引用?
这是面试最容易踩坑的点:
- storage → memory:深拷贝,修改 memory 变量不影响原 storage
- memory → memory:引用传递(引用类型如数组、结构体),修改会互相影响
- storage → storage:引用传递,指向同一块链上存储
- memory → storage:深拷贝,写入独立的 storage slot
soliditycontract AssignDemo { uint256[] public arr = [1, 2, 3]; function storageToMemory() external view returns (uint256) { uint256[] memory mArr = arr; // 深拷贝 mArr[0] = 99; // 不影响 arr return arr[0]; // 返回 1 } function memoryToMemory() external pure returns (uint256) { uint256[] memory a = new uint256[](3); a[0] = 10; uint256[] memory b = a; // 引用,非拷贝 b[0] = 20; return a[0]; // 返回 20,a 和 b 指向同一内存 } }
追问二:默认数据位置规则
Solidity 对数据位置有强制约束,不是随便选的:
- 状态变量:强制
storage - 函数参数(external):强制
calldata(返回参数除外) - 函数参数(public/internal):默认
memory,可显式指定calldata - 局部变量:值类型在栈上,引用类型默认
storage指针指向状态变量 - mapping 和动态数组:只能存在于
storage,不能声明为memory局部变量
soliditycontract LocationRules { mapping(address => uint256) public balances; // 强制 storage // external 参数强制 calldata function externalFn(uint256[] calldata data) external pure returns (uint256) { return data[0]; } // public 参数默认 memory,也可显式用 calldata 省 Gas function publicFn(uint256[] calldata data) public pure returns (uint256) { return data[0]; } function badLocalMapping() internal pure { // mapping(address => uint256) localMap; // 编译错误!mapping 不能在 memory } }
追问三:为什么 calldata 比 memory 省 Gas?
calldata 直接读取交易输入的原始 calldata 编码,不需要将数据拷贝到内存。memory 参数则需要 EVM 执行一次从 calldata 到内存的复制操作,对于大型数组或结构体,这个拷贝开销显著。所以当函数参数不需要修改时,用 calldata 替代 memory 是最常见的 Gas 优化手段之一。
追问四:storage 指针是什么?
在函数内声明一个 storage 类型的局部变量,实际上是一个指向状态变量的指针(引用),不会产生拷贝:
soliditycontract StoragePointer { struct User { uint256 balance; bool active; } mapping(address => User) public users; function deactivate(address addr) external { User storage u = users[addr]; // storage 指针,不拷贝 u.active = false; // 直接修改链上状态 } }
如果误写成 User memory u = users[addr],修改只会影响内存副本,不会写入链上,这是一个常见的 bug 来源。
追问五:EVM 视角下三种位置的本质
- storage:对应 EVM 的
SLOAD/SSTORE操作码,读写永久存储(key-value 永久数据库),每次操作 2100+ Gas - memory:对应
MLOAD/MSTORE,线性可扩展内存,按字访问,Gas 随使用量线性增长 - calldata:对应
CALLDATALOAD/CALLDATASIZE/CALLDATACOPY,只读访问交易输入数据,Gas 成本最低