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 / calldata
Solidity 共有三种数据存储位置,其 Gas 成本从高到低依次为:
storage>memory>calldata
| 数据位置 | 存储介质 | 是否上链 | 可修改性 | 典型用途 | 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.sol):
function 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.34,Solidity 0.8.x 及以上版本在编译期即可拦截此类写入,这与文档示例的pragma solidity ^0.8.34完全一致。
数据位置与赋值规则
赋值本质上是创建引用(reference)指向本体,因此修改本体或引用,变化可以被同步;但在某些位置组合下,赋值创建的是独立副本。规则如下:
创建引用(修改新变量会影响原变量)的情形:
storage(合约的状态变量)赋值给本地storage(函数内的):创建引用,改变新变量会影响原变量。示例(源码 Languages/pt-br/05_DataStorage/DataStorage.sol):
uint[] 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.sol):
function 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.sol):
function 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.coinbase | address payable | 当前区块矿工的地址 |
block.gaslimit | uint | 当前区块的 gaslimit |
block.number | uint | 当前区块的 number |
block.timestamp | uint | 当前区块的时间戳,为 unix 纪元以来的秒 |
gasleft() | uint256 | 剩余 gas |
msg.data | bytes calldata | 完整 call data |
msg.sender | address payable | 消息发送者(当前 caller) |
msg.sig | bytes4 | calldata 的前四个字节(function identifier) |
msg.value | uint | 当前交易发送的wei值 |
此外,Cancun 升级还新增了两个与 EIP-4844 Blob 相关的全局变量,可在最新版编译器中直接使用:
block.blobbasefee:当前区块的 blob 基础费用。blobhash(uint index):返回与当前交易关联的第index个 blob 的版本化哈希;若当前交易不包含 blob,则返回空字节。
四、以太单位与时间单位
1. 以太单位(Ether Units)
Solidity不存在小数点,用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),仅供参考