第四章:Solidity进阶(上)——映射、数组、结构体、枚举与数据位置
好,咱们继续往前走。上一章我们把Solidity基础语法过了一遍,现在该聊聊真正让智能合约“活起来”的数据结构了。
我个人觉得,映射、数组、结构体和枚举这四样东西,是写合约时最常用的“积木块”。你想想看,一个合约如果没有数据组织能力,那跟一个只会算加减乘除的计算器有什么区别?
嗯,咱们一个一个来。
4.1 映射(mapping)——键值对的“字典”
映射说白了就是一个键值对数据库。在Solidity里,它的写法是 mapping(KeyType => ValueType)。
我在项目中遇到过最典型的场景:记录每个地址的余额。比如一个代币合约,你肯定需要知道谁有多少钱对吧?
mapping(address => uint256) public balances;
这里有几个坑,我得提醒你:
- 键类型有限制:键可以是基本类型(address、uint、bytes32等),但不能是结构体或映射本身。值类型则几乎什么都可以。
- 不能遍历:映射没有长度概念,你不能像数组那样用for循环遍历所有键值对。这是设计上的取舍——为了gas效率。
- 默认值:访问不存在的键,不会报错,而是返回该值类型的默认值(比如uint256返回0)。
4.2 数组(array)——有序的数据集合
数组分两种:固定长度和动态长度。固定长度数组在编译时就确定了大小,动态数组则可以push和pop。
uint256[5] public fixedArray; // 固定长度,5个元素
uint256[] public dynamicArray; // 动态长度,可以增长
我个人习惯:能用固定长度就用固定长度,因为gas更省。但现实业务中,很多时候你根本不知道会有多少数据进来,比如一个投票合约的候选人列表。
数组操作要注意:
push()在末尾添加元素,返回新长度pop()删除最后一个元素,长度减1delete array[index]只是把该位置重置为默认值,不会缩短数组长度
4.3 结构体(struct)——自定义数据类型
结构体让你能把多个相关数据打包成一个整体。比如一个用户信息:
struct User {
string name;
uint256 age;
address wallet;
bool isActive;
}
然后你可以这样用:
mapping(address => User) public users;
function addUser(string memory _name, uint256 _age) public {
users[msg.sender] = User(_name, _age, msg.sender, true);
}
嗯,这里要注意:结构体里的字段顺序会影响存储布局。Solidity会按声明顺序紧凑排列,但每个字段仍然遵循32字节对齐规则。所以把相同类型的字段放在一起,能省点gas。
4.4 枚举(enum)——有限状态集合
枚举就是给一组常量起个名字。比如订单状态:
enum OrderStatus { Created, Paid, Shipped, Completed, Cancelled }
OrderStatus public status;
function pay() public {
require(status == OrderStatus.Created, "订单状态错误");
status = OrderStatus.Paid;
}
枚举在Solidity里本质上是uint8,从0开始编号。你不能直接传字符串进来,得传对应的数字。不过前端可以做个映射,让用户选“已支付”而不是“1”。
4.5 数据位置:storage / memory / calldata
这是Solidity里最容易让人懵圈的地方。说白了,就是数据存在哪、怎么传。
| 位置 | 特点 | 生命周期 | 修改影响 |
|---|---|---|---|
storage |
永久存储,写入区块链 | 合约生命周期 | 修改会永久保存 |
memory |
临时存储,函数执行期间存在 | 函数调用期间 | 修改不影响链上数据 |
calldata |
只读,存储函数参数 | 函数调用期间 | 不可修改 |
我刚开始学的时候,总搞不清什么时候用memory。后来总结了一个规律:
- 状态变量(合约里直接声明的)默认就是storage,不用写
- 函数参数如果是复杂类型(数组、结构体、字符串),用memory或calldata
- 函数内部临时变量如果是复杂类型,用memory
举个例子:
function processUser(User memory _user) public {
// _user 是 memory 副本,修改它不影响链上数据
// 如果想修改链上数据,得用 storage 引用
User storage storedUser = users[msg.sender];
storedUser.age = _user.age; // 这会真正修改链上数据
}
4.6 综合实战:一个简单的众筹合约
咱们把今天学的知识串起来,写一个迷你众筹合约:
contract Crowdfunding {
enum CampaignState { Active, Successful, Failed }
struct Campaign {
address creator;
string title;
uint256 goal;
uint256 raised;
CampaignState state;
mapping(address => uint256) contributions;
}
Campaign[] public campaigns;
function createCampaign(string memory _title, uint256 _goal) public {
Campaign storage newCampaign = campaigns.push();
newCampaign.creator = msg.sender;
newCampaign.title = _title;
newCampaign.goal = _goal;
newCampaign.raised = 0;
newCampaign.state = CampaignState.Active;
}
function contribute(uint256 _campaignId) public payable {
Campaign storage campaign = campaigns[_campaignId];
require(campaign.state == CampaignState.Active, "众筹已结束");
campaign.contributions[msg.sender] += msg.value;
campaign.raised += msg.value;
if (campaign.raised >= campaign.goal) {
campaign.state = CampaignState.Successful;
}
}
}
你看,这里用到了:
- 枚举管理状态
- 结构体打包数据
- 数组存储多个活动
- 映射记录每个人的捐款
- storage引用直接修改链上数据
嗯,这就是Solidity进阶的第一部分。说白了,这些数据结构就是你的工具箱。用得好了,合约逻辑清晰、gas省、bug少。用不好……嗯,我当年重构过好几次代码,都是因为一开始数据结构没设计对。
下一章咱们聊继承、接口和库,那才是真正让代码复用的利器。先消化这些,别贪多。