3. Solidity基础回顾:合约结构、状态变量与局部变量、基本数据类型
好,咱们直接进入正题。这一节我带你快速过一遍Solidity最核心的基础知识。别小看这些内容,我见过太多人写合约时,连变量声明的位置都搞错,结果gas费白白浪费。
3.1 合约的基本结构
一个Solidity合约,说白了就是一个状态机。它由状态变量、函数、事件、修饰器等组成。我习惯把合约想象成一个「智能保险箱」——里面存着数据,外面开着几个小窗口让别人操作。
来看一个最简结构:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MyNFT {
// 状态变量
uint256 public totalSupply;
// 构造函数
constructor() {
totalSupply = 0;
}
// 函数
function mint() public {
totalSupply += 1;
}
}
嗯,这里要注意几点:
- SPDX许可证:每个合约文件第一行必须写。我刚开始写时经常忘,结果编译报错才想起来。
- pragma版本:指定编译器版本。建议用
^0.8.0这种宽松版本,别锁死。 - contract关键字:合约名首字母大写,这是行业惯例。
3.2 状态变量 vs 局部变量
这个区别太重要了。我见过有人把状态变量当局部变量用,结果每次调用都重新初始化,数据全丢了。
| 类型 | 存储位置 | 生命周期 | Gas消耗 |
|---|---|---|---|
| 状态变量 | 区块链上(永久存储) | 合约生命周期 | 高(写入需付费) |
| 局部变量 | 内存/栈中 | 函数执行期间 | 低(免费) |
状态变量:声明在合约内部、函数外部。每次写入都要消耗gas。我个人习惯把关键数据(如NFT的owner、tokenURI)放在状态变量里。
局部变量:声明在函数内部。用完就丢,不占链上空间。适合做临时计算。
💡 避坑指南:我曾经犯过一个错——在循环里反复读取状态变量。每次读取都要花gas,循环100次就多花100倍的钱。后来我改成先把状态变量读到局部变量里,再循环操作,gas费直接降了90%。
// ❌ 错误做法:循环里反复读状态变量
function badLoop() public {
for (uint i = 0; i < 100; i++) {
totalSupply += 1; // 每次写入都花gas
}
}
// ✅ 正确做法:先读到局部变量
function goodLoop() public {
uint256 localSupply = totalSupply;
for (uint i = 0; i < 100; i++) {
localSupply += 1; // 内存操作,免费
}
totalSupply = localSupply; // 只写一次
}
3.3 基本数据类型
Solidity的数据类型,说白了就那几种。但每个都有坑,我一个个说。
3.3.1 uint(无符号整数)
从uint8到uint256,步长8。默认是uint256。
- uint8:范围0~255,适合存小数字(如NFT的稀有度等级)
- uint256:范围0~2^256-1,适合存金额、tokenId
⚠️ 注意:Solidity 0.8.0之后默认有溢出检查。但如果你用unchecked块,溢出会静默回绕。我建议非必要别用unchecked,除非你明确知道自己在做什么。
3.3.2 address(地址类型)
存以太坊地址,20字节。分两种:
- address:普通地址
- address payable:可接收ETH的地址,有
transfer和send方法
我写NFT合约时,ownerOf函数返回的就是address类型。注意:从address转address payable要用payable(addr)。
address owner = 0x123...;
address payable payableOwner = payable(owner);
payableOwner.transfer(1 ether); // 转账
3.3.3 string(字符串)
动态长度的UTF-8字符串。注意:string不能直接比较,也不能用==。
我踩过的坑:想比较两个string是否相等,直接写if (str1 == str2),结果编译报错。正确做法是用keccak256(abi.encodePacked(str1))来比较哈希值。
function compareStrings(string memory a, string memory b) public pure returns (bool) {
return keccak256(abi.encodePacked(a)) == keccak256(abi.encodePacked(b));
}
3.3.4 bytes(字节数组)
分两种:
- bytes1 ~ bytes32:固定长度,gas低
- bytes:动态长度,类似string但更底层
我个人习惯:存短数据(如NFT的metadata哈希)用bytes32,存长数据用bytes。string和bytes可以互相转换:bytes(str)或string(myBytes)。
🔧 小技巧:如果你要存一个固定长度的标识符(比如IPFS的CID),用bytes32比用string省gas得多。我有个项目把string全改成bytes32后,部署成本降了30%。
3.4 实战:NFT合约中的数据类型应用
咱们结合NFT场景,看看这些类型怎么用:
contract SimpleNFT {
// 状态变量
address public owner; // 合约所有者
uint256 public nextTokenId; // 下一个token编号
mapping(uint256 => address) public owners; // tokenId => 拥有者
mapping(uint256 => string) public uris; // tokenId => 元数据URI
constructor() {
owner = msg.sender; // 部署者就是所有者
nextTokenId = 1; // 从1开始编号
}
function mint(string memory uri) public {
uint256 tokenId = nextTokenId;
owners[tokenId] = msg.sender;
uris[tokenId] = uri;
nextTokenId++;
}
}
你看,这里用到了:
- address:存合约所有者、NFT拥有者
- uint256:存tokenId,范围够大
- string:存元数据URI(虽然用bytes32更省gas,但URI长度不固定)
嗯,基础就这些。你想想看,这些类型是不是每个NFT合约都离不开?下一节咱们就深入mapping和数组,那才是NFT合约的骨架。
📌 总结:写合约前先想清楚——哪些数据要永久存(状态变量),哪些只是临时用(局部变量)。数据类型选对了,gas费能省一大截。我每次写新合约,都会先画个数据流图,再动手编码。