5月28日 02:37

Solidity 中 storage、memory 和 calldata 三种数据位置的区别是什么?

在 Solidity 中,storagememorycalldata 是三种数据位置修饰符,决定数据的存储方式、生命周期和 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
solidity
contract 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 局部变量
solidity
contract 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 类型的局部变量,实际上是一个指向状态变量的指针(引用),不会产生拷贝:

solidity
contract 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 成本最低
标签:Solidity