4、Solidity数据类型(下):字符串、枚举、结构体、映射
好,咱们接着聊。上一节我们把Solidity的基础数据类型过了一遍,像uint、address这些,说白了就是最底层的积木块。但光有这些,写起复杂逻辑来还是有点束手束脚。今天要讲的这四种——字符串、枚举、结构体、映射,才是真正让你能搭出“城堡”的东西。
我个人习惯把这一节叫做“组合拳”。因为从这开始,你写的合约会越来越像真正的业务系统。嗯,咱们一个一个来。
4.1 字符串(string)
字符串在Solidity里,其实是个挺“别扭”的类型。为什么这么说?因为它本质上是一个动态的字节数组,但又不完全等同于bytes。我刚开始学的时候,就老搞混string和bytes的区别。
简单来说:string就是用来存UTF-8编码的文本的。你不能直接通过索引去修改某个字符,也不能直接获取它的长度(除非转成bytes)。
核心要点:Solidity中的string不支持直接拼接、修改等操作。如果你需要频繁操作字符串,建议先转成bytes,处理完再转回来。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract StringDemo {
string public greeting = "Hello, Solidity!";
// 获取字符串长度(需要转成bytes)
function getLength() public view returns (uint) {
return bytes(greeting).length;
}
// 修改字符串(通过bytes中转)
function setChar(uint index, bytes1 char) public {
bytes memory strBytes = bytes(greeting);
require(index < strBytes.length, "Index out of bounds");
strBytes[index] = char;
greeting = string(strBytes);
}
}
你看,改一个字符都得绕个弯子。我在项目中遇到过需要存储用户昵称的场景,当时图省事直接用string存,结果后来要做模糊搜索,发现根本没法高效处理。最后只能把昵称哈希后存到mapping里,再用事件去链下做搜索。嗯,这里要提醒你:链上不是存大文本的地方,string一般只用来存短文本、备注、或者URL。
我的建议:如果字符串长度不确定,或者可能超过32字节,用string。如果确定长度不超过32字节,用bytes32更省gas。
4.2 枚举(enum)
枚举这东西,说白了就是给数字起个名字。比如订单状态:0代表已创建,1代表已支付,2代表已发货。你写代码的时候总不能老记着0、1、2吧?用枚举就清晰多了。
contract EnumDemo {
enum Status { Created, Paid, Shipped, Completed, Cancelled }
Status public currentStatus;
constructor() {
currentStatus = Status.Created;
}
function nextStatus() public {
// 枚举可以比较,也可以转成uint
require(currentStatus != Status.Completed, "Already completed");
currentStatus = Status(uint(currentStatus) + 1);
}
function getStatusValue() public view returns (uint) {
return uint(currentStatus);
}
}
枚举在Solidity里底层就是uint8,从0开始递增。你想想看,用枚举写出来的代码,可读性是不是比直接写数字好太多了?我曾经接手过一个老合约,里面全是if (status == 2)这种写法,看得我头皮发麻。后来重构时全改成了枚举,维护起来舒服多了。
注意:枚举不能超过256个成员(因为底层是uint8)。不过说实话,你一般也用不了那么多。另外,枚举不能作为函数参数在外部调用中直接传递,但可以在内部使用。
4.3 结构体(struct)
结构体才是真正的“组合拳”。你可以把多个不同类型的数据打包成一个自定义类型。比如一个人,有名字、年龄、地址——用结构体就完美。
contract StructDemo {
struct Person {
string name;
uint age;
address wallet;
bool isActive;
}
// 存储多个Person
Person[] public people;
mapping(address => Person) public personByAddress;
function addPerson(string memory _name, uint _age) public {
// 方式一:直接赋值
Person memory newPerson = Person({
name: _name,
age: _age,
wallet: msg.sender,
isActive: true
});
people.push(newPerson);
// 方式二:位置参数(按顺序)
// Person memory p = Person(_name, _age, msg.sender, true);
// 存入mapping
personByAddress[msg.sender] = newPerson;
}
function updateAge(uint _newAge) public {
// 注意:mapping返回的是storage引用,可以直接修改
Person storage p = personByAddress[msg.sender];
p.age = _newAge;
}
}
这里有个坑,我刚开始踩过好几次:结构体里的数组和mapping不能作为public函数的返回值。因为Solidity不允许返回动态数组或mapping类型的结构体成员。解决办法是单独写getter函数,或者只返回部分字段。
经验之谈:结构体配合数组和mapping,基本能覆盖90%的数据存储需求。我个人习惯把结构体定义在合约最前面,方便后面引用。另外,结构体里的字段顺序会影响存储布局,把相同类型的字段放一起可以节省gas。
4.4 映射(mapping)
映射,说白了就是哈希表。在Solidity里,它是最常用的数据结构,没有之一。为什么?因为它的查询速度是O(1),而且天然支持键值对存储。
contract MappingDemo {
// 基本映射:地址 => 余额
mapping(address => uint) public balances;
// 嵌套映射:地址 => (代币ID => 余额)
mapping(address => mapping(uint => uint)) public tokenBalances;
// 映射的值可以是结构体
mapping(uint => Person) public personById;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function getBalance(address _addr) public view returns (uint) {
return balances[_addr];
}
// 嵌套映射的使用
function setTokenBalance(address _user, uint _tokenId, uint _amount) public {
tokenBalances[_user][_tokenId] = _amount;
}
}
映射有几个特点,你得记住:
- 不能遍历:映射没有length属性,也不能用for循环遍历所有键。如果你需要遍历,得配合数组手动记录所有键。
- 所有键默认都有值:访问不存在的键,返回对应值类型的零值(uint返回0,address返回0x0...)。
- 不能作为函数参数或返回值:映射只能作为状态变量或局部storage引用。
我曾经踩过的坑:有一次写一个众筹合约,用mapping记录每个地址的捐款金额。结果发现有人可以反复调用提现函数,因为mapping里不存在的键返回0,而0满足提现条件。后来加了一个bool类型的mapping来标记是否已提现,才堵住这个漏洞。
4.5 综合示例:一个简单的用户管理系统
把今天学的四种类型串起来,写个简单的用户管理合约。你看,组合起来多强大:
contract UserManager {
enum UserRole { Guest, Member, Admin, SuperAdmin }
struct User {
string name;
uint age;
UserRole role;
bool isActive;
}
mapping(address => User) public users;
address[] public userList;
function register(string memory _name, uint _age) public {
require(bytes(users[msg.sender].name).length == 0, "Already registered");
User memory newUser = User({
name: _name,
age: _age,
role: UserRole.Member,
isActive: true
});
users[msg.sender] = newUser;
userList.push(msg.sender);
}
function getUserCount() public view returns (uint) {
return userList.length;
}
function isAdmin(address _addr) public view returns (bool) {
return users[_addr].role == UserRole.Admin ||
users[_addr].role == UserRole.SuperAdmin;
}
}
你看,用枚举定义角色,用结构体打包用户信息,用映射实现地址到用户的快速查找,再用数组记录所有用户地址以便遍历。四种类型配合得天衣无缝。
最后说一句:学数据类型,别死记硬背。多写几个小合约,把string、enum、struct、mapping组合起来用,慢慢就找到感觉了。下一节我们讲函数和修饰符,到时候你会看到这些数据类型在函数里怎么“活起来”。