【从0学习Solidity】映射类型mapping
发布时间:2025-09-01 00:00       
【从0学习Solidity】mapping 映射类型速通指南
在 Solidity 中,mapping(KeyType => ValueType) 是最常用的状态存储结构之一,用于将“键”映射到“值”。它像哈希表,但为链上存储做了极致优化:读取/写入按键寻址、O(1) 访问、未赋值键自动为默认值。📌
一、核心特性(直击要点)
- 仅存在于 storage:
mapping不能放在memory或calldata,也不能整体返回或复制。 - 按键寻址、无长度:不记录“有哪些键”,也不能遍历;需要遍历时必须自行维护“键列表”。
- 默认值:对未写入的键读取时返回
ValueType的零值(如0、false、空结构体)。 - 可嵌套:支持
mapping的值再是mapping或struct(键不行,见下)。 - 可见性与自动 getter:将映射声明为
public,编译器会为你生成按键查询的只读 getter(不是返回整个映射)。
键类型 KeyType:推荐使用定长值类型,如
address、uint256、bytes32、enum、用户自定义值类型等。动态类型键(如string/可变长bytes)不建议,可用bytes32存放哈希作为键以保证稳定与一致性。⚠️
二、最小可用示例(带注释)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
contract ScoreBook {
// 1) 基础映射:地址 => 分数
mapping(address => uint256) public scoreOf; // public 生成按键查询的 getter
// 2) “存在性”标记:默认值陷阱的常见解决方案
struct Entry { bool exists; uint256 score; }
mapping(address => Entry) private entries;
// 3) 嵌套映射:owner => (spender => 额度)
mapping(address => mapping(address => uint256)) public allowanceOf;
// 写入:首次写入要设置 exists,避免“0 分=未注册”的歧义
function setScore(uint256 newScore) external {
scoreOf[msg.sender] = newScore;
entries[msg.sender] = Entry({exists: true, score: newScore});
}
// 读取:利用 exists 区分“未写入”和“写入了零值”
function getMyScore() external view returns (bool exists, uint256 score) {
Entry memory e = entries[msg.sender];
return (e.exists, e.score);
}
// 删除:将键对应的值重置为默认值(如 0 / false)
function removeMyScore() external {
delete scoreOf[msg.sender]; // => 0
delete entries[msg.sender]; // => {false, 0}
}
// 维护可遍历键列表的示例——需配合数组与去重(略)
}
三、工作流程(读取/写入是如何发生的)
[提供键 Key] → keccak256(Key, 槽位) → 定位 storage 槽 → 读/写 Value
- 由于是按键哈希寻址,并不存储“全集键集合”,所以无法原生遍历。需要遍历时,自己维护
address[] keys并在首次插入时记录。🚀
四、对比表:mapping vs 数组 vs 结构体
| 维度 | mapping | 数组 (dynamic) | 结构体 struct |
|---|---|---|---|
| 访问复杂度 | O(1) 按键直达 | O(1) 按索引 | O(1) 按字段 |
| 是否可遍历 | ❌ 不可 | ✅ 可 | ✅ 可(随容器) |
| 是否记录键集合 | ❌ 不记录 | ✅ 有长度 | ✅ 字段固定 |
| 典型用途 | 余额表、权限表、去重表 | 列表、批量项 | 组合字段 |
| 默认值陷阱 | ✅ 明显(未写即零值) | 较小 | 较小 |
五、常见坑与最佳实践
- “未写入=默认值”陷阱
- 问题:
scoreOf[user] == 0无法区分“真的是 0 分”还是“没写过”。 - 方案:配合
bool exists标记,或用单独的mapping(address => bool) has。
- 问题:
- 需要遍历怎么办?
mapping无法遍历;如需枚举用户,请维护一个 keys 数组与一个bool added去重映射。- 注意 gas:仅在“首次插入”时 push,删除可标记“失效”并按需清理。
- 键选择建议
- 选
address/uint256/bytes32/enum等定长值类型。 - 将长文本键(如域名、字符串)先哈希成
bytes32再作为键:bytes32 key = keccak256(bytes(name));
- 选
- 删除与置零
delete m[k]等价于写入默认值;从“非零→零”会触发相应的存储退款/成本规则(以当前 EVM 标准为准),频繁清零不是免费行为。
- 可见性与返回
public只会生成按键单值查询的 getter;不能一次返回整张表。- 函数参数/返回值不能是
mapping,改用数组或事件(event)输出。
六、进阶用法片段(可直接套用)
去重/存在性集(Set):
mapping(address => bool) public inWhitelist;
function add(address a) external { inWhitelist[a] = true; }
function remove(address a) external { delete inWhitelist[a]; }
function has(address a) external view returns(bool) { return inWhitelist[a]; }
授权(Nested mapping):
function approve(address spender, uint256 amt) external {
allowanceOf[msg.sender][spender] = amt;
}
function allowance(add