第 5 讲:变量数据存储与作用域 storage/memory/calldata 实战详解)
WTF Solidity 极简教程葡萄牙语版第 5 讲变量数据存储与作用域 storage/memory/calldata 实战详解【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity本文基于 Languages/pt-br/05_DataStorage/readme.md 编写配套源码见 Languages/pt-br/05_DataStorage/DataStorage.sol与 05_DataStorage/DataStorage.sol 逻辑一致。本讲属于 WTF Solidity 极简入门教程的“基础语法”部分是理解合约存储模型与 Gas 优化的第一道门槛。读完本文你将掌握 Solidity 三大数据位置storage/memory/calldata的语义差异与赋值规则理清状态变量、局部变量、全局变量三种作用域并会用以太单位与时间单位在合约中做精确的数值与时间计算。一、引言为什么 Solidity 要显式声明数据位置在 Solidity 中引用类型Reference Type——包括数组array和结构体struct——与值类型如uint、address不同它们内部可能包含任意数量的元素结构复杂、占用存储空间大。因此在声明和使用这类变量时必须显式指定其数据存储位置data location编译器才能确定该变量应存放于链上持久存储还是临时内存从而决定对应的 Gas 开销与可修改性。这一设计直接服务于两条核心诉求节省链上有限的存储空间链上storage空间昂贵且不可随意挥霍降低 Gas 消耗合理利用内存型数据位置memory/calldata可显著减少每次交易的花费。本讲的实战重点就是理解storage、memory、calldata三个关键字的语义、赋值行为差异以及它们与变量作用域的配合方式。二、数据位置storage / memory / calldataSolidity 共有三种数据存储位置其 Gas 成本从高到低依次为storagememorycalldata数据位置存储介质是否上链可修改性典型用途Gas 成本storage链上持久存储是可读写合约状态变量默认位置高memory内存临时否可读写函数参数、函数内临时变量中calldata内存临时否只读immutable函数参数低1.storage链上持久存储合约里的状态变量默认就是storage数据直接写入链上存储如同计算机的“硬盘”读取与写入都消耗较多 Gas。它是唯一可以在声明时省略数据位置关键字的位置见 Languages/pt-br/05_DataStorage/DataStorage.sol 中uint[] public x [1,2,3];的写法注释明确说明 “The data location of x is storage. This is the only place where the data location can be omitted.”。2.memory内存中的临时数据函数里的参数和临时变量一般用memory数据临时存储在内存中、不上链Gas 消耗远低于storage。尤其当返回类型是变长类型时必须加memory修饰例如string、bytes、array以及自定义结构体。这是因为变长数据无法直接塞进 EVM 的返回值槽位必须指明其在内存中的位置。3.calldata只读的函数参数数据calldata与memory类似数据同样存储在内存中、不上链但区别在于calldata变量不能修改不可变/immutable一般专用于函数参数。由于它直接引用调用方传入的原始 calldata无需额外拷贝因此 Gas 成本最低。示例来自源码 Languages/pt-br/05_DataStorage/DataStorage.solfunction fCalldata(uint[] calldata _x) public pure returns(uint[] calldata){ // 参数为 calldata 数组不能被修改 // _x[0] 0 // 这样修改会报错 return(_x); }从源码注释可以确认一旦尝试写入_x[0] 0编译器会直接抛出错误——即上图中验证的Calldata arrays are read-only编译期错误。需要特别说明的是在 Remix 等开发环境中“编译报错”验证的是 calldata 的只读约束而当前仓库的foundry.toml将solc固定为0.8.34Solidity 0.8.x 及以上版本在编译期即可拦截此类写入这与文档示例的pragma solidity ^0.8.34完全一致。数据位置与赋值规则赋值本质上是创建引用reference指向本体因此修改本体或引用变化可以被同步但在某些位置组合下赋值创建的是独立副本。规则如下创建引用修改新变量会影响原变量的情形storage合约的状态变量赋值给本地storage函数内的创建引用改变新变量会影响原变量。示例源码 Languages/pt-br/05_DataStorage/DataStorage.soluint[] x [1,2,3]; // 状态变量数组 x function fStorage() public{ // 声明一个 storage 变量 xStorage指向 x修改 xStorage 也会影响 x uint[] storage xStorage x; xStorage[0] 100; }调用fStorage()后状态变量x[0]被改为100——上图调试面板中Solidity State的x[0]值变化即为佐证。memory赋值给memory同样创建引用改变新变量会影响原变量。创建副本修改互不影响的情形除上述两种情况外其余位置组合的赋值都会创建本体的副本对二者之一的修改不会同步到另一方。这在实际开发中容易踩坑例如从storage读取数据赋值给memory变量后修改了memory数据但如果没有把memory数据写回storage那么链上storage的数据并不会改变。源码中的fMemory()函数Languages/pt-br/05_DataStorage/DataStorage.sol正是该场景的完整演示function fMemory() public view{ // 声明 memory 变量 xMemory复制 x修改 xMemory 不会影响 x uint[] memory xMemory x; xMemory[0] 100; xMemory[1] 200; uint[] memory xMemory2 x; xMemory2[0] 300; }执行后内存中的xMemory被改写为[100, 200, 3]而链上状态变量x依然保持[1, 2, 3]。三、变量的作用域状态变量 / 局部变量 / 全局变量Solidity 中变量按作用域划分共有三种状态变量state variable、局部变量local variable和全局变量global variable。1. 状态变量State Variable状态变量是数据存储在链上的变量合约内所有函数都可以访问Gas 消耗高。状态变量在合约内、函数外声明contract Variables { uint public x 1; uint public y; string public z; }可以在函数里更改状态变量的值对应源码 Languages/pt-br/05_DataStorage/DataStorage.solfunction foo() external{ // 可以在函数里更改状态变量的值 x 5; y 2; z 0xAA; }2. 局部变量Local Variable局部变量仅在函数执行过程中有效函数退出后变量即失效。局部变量的数据存储在内存中、不上链Gas 低。局部变量在函数内声明function bar() external pure returns(uint){ uint xx 1; uint yy 3; uint zz xx yy; return(zz); }注意这里使用了pure修饰符函数既不读取也不修改状态变量仅依赖入参与局部变量完成计算。3. 全局变量Global Variable全局变量是全局范围有效的变量均为 Solidity 预留关键字可以在函数内不声明直接使用。示例源码 Languages/pt-br/05_DataStorage/DataStorage.solfunction global() external view returns(address, uint, bytes memory){ address sender msg.sender; uint blockNum block.number; bytes memory data msg.data; return(sender, blockNum, data); }上例使用了 3 个常用全局变量msg.sender请求发起地址、block.number当前区块高度和msg.data请求数据。由于函数体读取了区块链与消息上下文因此使用view修饰符。下面列出常用全局变量完整列表以官方 Solidity 文档为准全局变量类型含义blockhash(uint blockNumber)bytes32给定区块的哈希值只适用于最近的 256 个区块不包含当前区块block.coinbaseaddress payable当前区块矿工的地址block.gaslimituint当前区块的 gaslimitblock.numberuint当前区块的 numberblock.timestampuint当前区块的时间戳为 unix 纪元以来的秒gasleft()uint256剩余 gasmsg.databytes calldata完整 call datamsg.senderaddress payable消息发送者当前 callermsg.sigbytes4calldata 的前四个字节function identifiermsg.valueuint当前交易发送的wei值此外Cancun 升级还新增了两个与 EIP-4844 Blob 相关的全局变量可在最新版编译器中直接使用block.blobbasefee当前区块的 blob 基础费用。blobhash(uint index)返回与当前交易关联的第index个 blob 的版本化哈希若当前交易不包含 blob则返回空字节。四、以太单位与时间单位1. 以太单位Ether UnitsSolidity不存在小数点用0表示小数位以确保交易的精确度并防止精度损失。利用以太单位可以避免误算问题方便在合约中处理货币交易。常用以太单位换算关系wei: 1gwei: 1e9 1000000000ether: 1e18 1000000000000000000源码中的验证函数Languages/pt-br/05_DataStorage/DataStorage.sol通过assert断言保证换算正确任何单位换算偏差都会导致交易回滚function weiUnit() external pure returns(uint) { assert(1 wei 1e0); assert(1 wei 1); return 1 wei; } function gweiUnit() external pure returns(uint) { assert(1 gwei 1e9); assert(1 gwei 1000000000); return 1 gwei; } function etherUnit() external pure returns(uint) { assert(1 ether 1e18); assert(1 ether 1000000000000000000); return 1 ether; }2. 时间单位Time Units可以在合约中规定“一个操作必须在一周内完成”或“某个事件在一个月后发生”这让合约的执行更加精确不因技术上的误差影响合约结果。时间单位是 Solidity 的重要概念有助于提高合约的可读性和可维护性。换算关系seconds: 1minutes: 60 seconds 60hours: 60 minutes 3600days: 24 hours 86400weeks: 7 days 604800源码中的验证函数Languages/pt-br/05_DataStorage/DataStorage.sol同样使用assert层层校验单位间的等价关系function secondsUnit() external pure returns(uint) { assert(1 seconds 1); return 1 seconds; } function minutesUnit() external pure returns(uint) { assert(1 minutes 60); assert(1 minutes 60 seconds); return 1 minutes; } function hoursUnit() external pure returns(uint) { assert(1 hours 3600); assert(1 hours 60 minutes); return 1 hours; } function daysUnit() external pure returns(uint) { assert(1 days 86400); assert(1 days 24 hours); return 1 days; } function weeksUnit() external pure returns(uint) { assert(1 weeks 604800); assert(1 weeks 7 days); return 1 weeks; }五、源码级验证与实操建议在仓库中验证本讲代码本讲完整可编译源码位于 Languages/pt-br/05_DataStorage/DataStorage.sol中文版同逻辑代码位于 05_DataStorage/DataStorage.sol包含DataStorage与Variables两个合约覆盖fStorage、fMemory、fCalldata以及以太/时间单位断言函数。仓库根目录的 foundry.toml 将默认编译器固定为solc 0.8.34与本讲源码的pragma solidity ^0.8.34匹配如需用 Foundry 编译验证可执行forge build配合根目录 scripts/run-forge-tests.sh 可运行全仓测试。开发中的三条经验法则函数参数优先用calldata只读入参声明为calldata可省去内存拷贝Gas 最省若需在函数内修改入参副本再改用memory。从storage读、到memory改、写回要显式赋值memory副本不会自动同步回链上任何需要持久化的修改都必须显式写回storage。用单位常量代替魔法数字在锁仓、拍卖、时间限制等场景中直接使用days、weeks、ether等单位可显著提升合约可读性与可维护性本仓库后续的 35_DutchAuction荷兰拍卖、43_TokenVesting代币归属、44_TokenLocker代币锁仓等实例中时间与金额单位的运用正是本讲知识的直接落地。总结本讲介绍了 Solidity 中的引用类型、数据位置storage/memory/calldata和变量作用域核心是三个关键字的用法storage链上持久存储、Gas 高memory内存临时存储、Gas 适中calldata内存只读参数、Gas 最低。它们出现的原因正是为了节省链上有限的存储空间并降低 Gas 消耗。下一讲将进入引用类型中的数组array进一步实践本讲的数据位置规则。【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考