4、Solidity进阶:映射、结构体、数组、枚举、修饰符
好,咱们继续往前走。上一章我们把Solidity的基础语法过了一遍,现在该聊聊真正干活时天天用的东西了。
映射、结构体、数组、枚举、修饰符——这几个东西,说白了就是Solidity里组织数据和逻辑的“四件套”。我当年刚学的时候,觉得它们各自独立,后来才发现,它们经常组合在一起用。你想想看,一个真实的DApp,怎么可能只用单一类型?
4.1 结构体(struct)—— 自定义数据类型
结构体,就是让你把多个变量打包成一个“新类型”。
比如你要记录一个用户的信息:地址、名字、余额。用三个独立变量去存?太乱了。用结构体,清爽很多。
// 定义一个用户结构体
struct User {
address addr;
string name;
uint balance;
}
定义好之后,你就可以像用uint一样用它了。
User public myUser;
function setUser(address _addr, string memory _name, uint _balance) public {
myUser = User(_addr, _name, _balance);
}
嗯,这里要注意:结构体里的字符串类型,在函数参数里要加 memory 关键字。这是Solidity的存储位置规则,我一开始老忘,后来养成习惯:凡是传进来的复杂类型,先问自己一句“放哪?”
4.2 数组(array)—— 有序列表
数组分两种:固定长度和动态长度。固定长度数组在编译时就知道大小,动态数组可以push。
uint[5] public fixedArray; // 固定长度,5个元素
uint[] public dynamicArray; // 动态数组,可以push
动态数组在Solidity里特别常用,尤其是配合结构体。
User[] public users;
function addUser(address _addr, string memory _name, uint _balance) public {
users.push(User(_addr, _name, _balance));
}
我在项目中遇到过一个问题:数组太大时,遍历会非常耗gas。有一次我写了个循环遍历所有用户,结果合约调用直接out of gas。从那以后,我尽量用映射来替代数组遍历,或者维护一个“活跃用户索引”数组。
delete 操作不会真的缩小数组长度,它只是把值重置为默认值。如果你要真正移除元素,得自己写逻辑,比如把最后一个元素移到被删位置,然后pop。
4.3 映射(mapping)—— 键值对存储
映射是Solidity里最强大的数据结构之一。它就像一个字典,给你一个key,你就能找到对应的value。
mapping(address => uint) public balances;
mapping(uint => User) public userById;
映射的语法很特别:mapping(KeyType => ValueType)。KeyType不能是复杂类型(比如结构体、数组),但ValueType可以是任何类型,包括另一个映射。
我建议你养成一个习惯:能用映射就别用数组。为什么?因为映射的查找是O(1)的,而数组遍历是O(n)。在区块链上,gas就是命啊。
// 用映射存储用户信息,用地址做key
mapping(address => User) public userMap;
function getUser(address _addr) public view returns (User memory) {
return userMap[_addr];
}
不过映射有个缺点:你不能遍历它。Solidity没有提供遍历映射的内置方法。如果你需要遍历所有用户,就得同时维护一个数组来记录所有key。
4.4 枚举(enum)—— 有限状态集合
枚举,说白了就是给一组常量起个名字。比如订单状态:待支付、已支付、已发货、已完成。
enum OrderStatus { Pending, Paid, Shipped, Completed }
OrderStatus public status;
function pay() public {
require(status == OrderStatus.Pending, "订单状态不对");
status = OrderStatus.Paid;
}
枚举在Solidity里本质上是uint,从0开始编号。所以 Pending = 0,Paid = 1,以此类推。
我个人习惯把枚举用在状态机模式里。比如一个众筹合约:Created -> Funded -> Failed -> Completed。每个状态只能转移到特定的下一个状态,这样逻辑清晰,不容易出bug。
4.5 修饰符(modifier)—— 代码复用利器
修饰符是Solidity里一个很优雅的设计。它让你把重复的权限检查、条件判断抽出来,放到函数前面。
modifier onlyOwner() {
require(msg.sender == owner, "你不是合约所有者");
_;
}
function withdraw() public onlyOwner {
// 只有owner才能调用
payable(owner).transfer(address(this).balance);
}
那个 _ 是占位符,表示被修饰的函数体插入的位置。你可以把修饰符想象成一个“函数包装器”。
修饰符还可以带参数:
modifier minBalance(uint _min) {
require(balances[msg.sender] >= _min, "余额不足");
_;
}
function transfer(address _to, uint _amount) public minBalance(100) {
// 只有余额大于100才能转账
}
我在项目中遇到过一个问题:修饰符里如果调用了外部合约,可能会被重入攻击。所以我的原则是:修饰符里只做简单的状态检查,不要做外部调用。
function foo() public onlyOwner whenNotPaused,那么 onlyOwner 先执行,然后 whenNotPaused,最后才是函数体。
4.6 组合使用:一个完整的例子
好了,咱们把上面这些知识点串起来,写一个简单的“用户管理系统”。
contract UserManager {
address public owner;
enum UserLevel { Bronze, Silver, Gold }
struct User {
string name;
uint points;
UserLevel level;
bool active;
}
mapping(address => User) public users;
address[] public userList;
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
modifier userExists(address _addr) {
require(users[_addr].active, "User not exists");
_;
}
function addUser(address _addr, string memory _name) public onlyOwner {
require(!users[_addr].active, "User already exists");
users[_addr] = User(_name, 0, UserLevel.Bronze, true);
userList.push(_addr);
}
function addPoints(address _addr, uint _points) public onlyOwner userExists(_addr) {
users[_addr].points += _points;
// 升级逻辑
if (users[_addr].points >= 100) {
users[_addr].level = UserLevel.Gold;
} else if (users[_addr].points >= 50) {
users[_addr].level = UserLevel.Silver;
}
}
function getUserCount() public view returns (uint) {
return userList.length;
}
}
你看,结构体、映射、数组、枚举、修饰符,全用上了。这个合约虽然简单,但已经具备了真实项目的基本骨架。
最后说一句:这些数据结构不是孤立的。你写合约的时候,多想想它们怎么配合。比如映射存数据,数组存索引,枚举控状态,修饰符管权限。组合起来,就是一套完整的业务逻辑。
下一章,咱们聊聊事件和错误处理。这些东西在调试和前端交互时特别重要。