关注我,带你转型Web3开发,分享我从2018年入行区块链以来的开发经验(DeFi/GameFi/NFT/DAO/OTC/钱包/Trading Bot/MEME Launch),免费领取我私下整理的“智能合约学习”资料一份,帮你少走弯路!
在Solidity开发中,搞懂数据存储位置就像掌握了合约的“命脉”。选错了地方,gas费飙升不说,合约还可能出大问题。新手开发者稍微不注意,就可能写出“花钱如流水”还bug不断的代码。
这篇文章,我将和你聊聊 storage、memory、calldata 和 transient 这存储四兄弟的区别,帮你写出更安全、更省钱的合约。
先来个直观的对比(手机屏下可左右滚动查看):
| 存储位置 | 生命周期 | 可变性 | gas成本 | 主要用途 |
|---|---|---|---|---|
| storage | 永久 | 可修改 | 最高 | 合约状态变量 |
| memory | 函数执行期间 | 可修改 | 中等 | 函数内临时数据 |
| calldata | 函数调用期间 | 只读 | 最低 | 外部函数参数 |
| transient | 单次交易期间 | 可修改 | 低 | 跨函数临时数据 |
我个人是这样简单记的:
接下来,我带你逐个认识它们。
storage 是智能合约的“保险箱”,数据放进去就永久保存,交易之间都能访问。每个合约都有自己的 storage,除非你用 selfdestruct 把合约干掉,不然数据一直在那儿。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract StorageExample {
// 以下三行都是storage变量
uint256 public counter;
mapping(address => uint256) public balances;
string public message;
// 修改storage变量
function incCounter() external {
counter += 1; // 直接修改storage
}
// 使用storage引用
function updateBalance(address user, uint256 amount) external {
// 创建指向storage的引用
mapping(address => uint256) storage userBalances = balances;
userBalances[user] = amount; // 修改storage
}
}
storage操作昂贵,特别是写操作。如果你需要在函数中频繁操作storage数据,可以先将其加载到memory中,操作完成后再一次性写回storage:
function optimizedUpdate(uint256 index, uint256 value) external {
// 加载到memory
uint256[] memory tempArray = new uint256[](myStorageArray.length);
for(uint i = 0; i < myStorageArray.length; i++) {
tempArray[i] = myStorageArray[i];
}
// 在memory中操作
tempArray[index] = value;
// 一次性写回storage
for(uint i = 0; i < tempArray.length; i++) {
myStorageArray[i] = tempArray[i];
}
}
memory 就像函数的“草稿纸”,用完就扔,适合记些临时数据,比如计算中间结果。函数一结束,里面的东西就清空了。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract MemoryExample {
function processArray(uint256[] calldata inputs) external pure returns (uint256[] memory) {
// 在memory中创建新数组
uint256[] memory results = new uint256[](inputs.length);
// 在memory中操作数据
for (uint i = 0; i < inputs.length; i++) {
results[i] = inputs[i] * 2;
}
// 返回memory数组
return results;
}
}
memory 数组一旦建好,大小就固定了,想加元素?得重新建个大的,再把老数据抄过去,有点麻烦:
function resizeArray() public pure returns (uint[] memory) {
// 初始数组
uint[] memory array = new uint[](2);
array[0] = 10;
array[1] = 20;
// 创建更大的数组
uint[] memory newArray = new uint[](3);
for(uint i = 0; i < array.length; i++) {
newArray[i] = array[i];
}
newArray[2] = 30;
return newArray;
}
calldata是一个特殊的数据位置,它包含了调用智能合约函数时传入的所有数据。这是一个只读区域,不能被修改,但访问成本非常低。
calldata 是函数的“快递包裹”,里面装着调用者送来的数据,只能看不能改(存放在一个特殊的只读区域),但访问超便宜,gas费很低。
external 函数参数。// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract CalldataExample {
// 使用calldata处理大型数组参数
function sumArray(uint256[] calldata values) external pure returns (uint256) {
uint256 sum = 0;
for(uint i = 0; i < values.length; i++) {
sum += values[i];
// values[i] = 0; // 错误!calldata是只读的
}
return sum;
}
}
calldata 还能切片,直接用,不用拷到 memory,不消耗额外 gas,非常适合处理大型输入数据:
function processSlice(uint[] calldata data, uint start, uint end) external pure returns (uint) {
// 直接对calldata进行切片,非常高效
uint[] calldata slice = data[start:end];
uint sum = 0;
for(uint i = 0; i < slice.length; i++) {
sum += slice[i];
}
return sum;
}
transient 是 Solidity 0.8.28 后的“新宠”(EIP-1153,对应的操作码是TSTORE / TLOAD),专门为交易内临时数据设计。gas 费比 storage 便宜多了,交易一结束,数据自动消失,干净利落。
防重入锁是个经典场景,用 transient 超划算:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
contract TransientExample {
// 使用 storage 变量,首次写入约 20000 gas,更新约 5000 gas
bool transient _locked; // transient 变量,交易结束自动清零
mapping(address => uint256) public balances;
modifier nonReentrant() {
require(!_locked, "ReentrancyGuard: reentrant call");
_locked = true; // 写 transient,便宜
_;
_locked = false; // 再写一次,还是便宜
}
function withdraw() external payable nonReentrant {
uint256 amount = balances[msg.sender];
if (amount > 0) {
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
}
如何为你的数据选择正确的存储位置?以下是我的判断方法:
storagetransient storagecalldatamemoryuint[] storageArray;
function buggy() public {
uint[] storage myArray = storageArray; // 直接指向 storage
myArray[0] = 100; // 改的是 storageArray,容易搞乱
}
正确做法: 想临时改,用 memory 副本,别直接碰 storage。
uint[] memory array = new uint[](5);
array.push(10); // 错!memory 数组不能 push
解决办法: 重建新数组,手动抄数据。
只能在 Solidity 0.8.28+ 用,编译器版本不对就歇菜。
看到这里,相信你对solidity的数据存储位置有了清晰的认识:
搞懂这四兄弟,你的合约会更安全、更省钱,面试也能轻松过关!
💬 如果本文对你有所帮助,请帮忙点赞、转发、关注一下,有啥想法,欢迎留言,下篇见!
[图片占位] 加入知识星球,我可以为你提供:
本人技术栈: Solidity/Golang/Rust/NodeJS/Python/AI编程 项目合作,欢迎你来链接我! 都看到这里了,不关注一下嘛👇
基于全文检索与主题相似度
说在前面 在Solidity开发中,错误处理就像是合约的守护者,保护着你的代码免受意外和攻击,所以用好require、revert和assert这三个关键字特别重要。 这篇文章,我将和你分享它们的区别和用法,帮你在开发中少踩坑,写出更安全、更可靠的合约。 核心区别对比 先来个直观的对比(手机屏下可左
关注我,带你转型Web3开发,分享我从2018年入行区块链以来的开发经验(DeFi/GameFi/NFT/DAO/OTC/钱包/Trading Bot/MEME Launch),免费领取我私下整理的“智能合约学习”资料一份,帮你少走弯路! 说在前面 在solidity开发中,合约间的交互是常见需求,
关注我,带你转型Web3开发,分享我从2018年入行区块链以来的开发经验(DeFi/GameFi/NFT/DAO/OTC/钱包/Trading Bot/MEME Launch),免费领取我私下整理的“智能合约学习”资料一份,帮你少走弯路! 说在前面 在Solidit开发中,ETH 转账是一项基础却至