文档首页> 云计算> 【从0学习Solidity】映射类型mapping

【从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) 按字段
是否可遍历 ❌ 不可 ✅ 可 ✅ 可(随容器)
是否记录键集合 ❌ 不记录 ✅ 有长度 ✅ 字段固定
典型用途 余额表、权限表、去重表 列表、批量项 组合字段
默认值陷阱 ✅ 明显(未写即零值) 较小 较小

五、常见坑与最佳实践

  1. “未写入=默认值”陷阱
    • 问题:scoreOf[user] == 0 无法区分“真的是 0 分”还是“没写过”。
    • 方案:配合 bool exists 标记,或用单独的 mapping(address => bool) has。
  2. 需要遍历怎么办?
    • mapping 无法遍历;如需枚举用户,请维护一个 keys 数组与一个 bool added 去重映射。
    • 注意 gas:仅在“首次插入”时 push,删除可标记“失效”并按需清理。
  3. 键选择建议
    • 选 address/uint256/bytes32/enum 等定长值类型。
    • 将长文本键(如域名、字符串)先哈希成 bytes32 再作为键:bytes32 key = keccak256(bytes(name));
  4. 删除与置零
    • delete m[k] 等价于写入默认值;从“非零→零”会触发相应的存储退款/成本规则(以当前 EVM 标准为准),频繁清零不是免费行为。
  5. 可见性与返回
    • 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