Q10: 对于地址白名单,使用 mapping 还是 array 更好?为什么?

欢迎来到《Solidity面试修炼之道》专栏💎。

专栏核心理念:

核心 Slogan💸💸:从面试题到实战精通,你的 Web3 开发进阶指南。

一句话介绍🔬🔬: 150+ 道面试题 × 103 篇深度解析 = 你的 Solidity 修炼秘籍。

  1. ✅ 名称有深度和系统性
  2. ✅ "修炼"体现进阶过程
  3. ✅ 适合中文技术社区
  4. ✅ 记忆度高,易于传播
  5. ✅ 全场景适用

Q10: 对于地址白名单,使用 mapping 还是 array 更好?为什么?

简答:

使用 mapping 更好,因为查找是 O(1) 时间复杂度且 gas 成本固定,而 array 需要遍历,时间复杂度 O(n),gas 成本随数组大小增长。

详细分析:

在智能合约中选择数据结构时,gas 成本是首要考虑因素。对于白名单这种需要频繁查询的场景,mapping 和 array 有显著差异:

Mapping 的优势

  • 查找效率:O(1) 常数时间,无论白名单大小
  • Gas 成本:固定约 2,100 gas(冷访问)或 100 gas(热访问)
  • 添加/删除:简单高效,只需设置 true/false
  • 适用场景:需要频繁检查地址是否在白名单中

Array 的劣势

  • 查找效率:O(n) 线性时间,需要遍历整个数组
  • Gas 成本:随数组大小线性增长,可能导致 gas 超限
  • 删除复杂:需要移动元素或留下空位
  • 唯一优势:可以遍历所有地址(但 gas 成本高)

混合方案

如果既需要快速查找,又需要遍历所有地址,可以同时使用 mapping 和 array,但要注意保持同步。

代码示例:

solidity 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/**
 * @title WhitelistComparison
 * @notice 对比 mapping 和 array 实现白名单
 */

// ❌ 不推荐:使用 array 实现白名单
contract ArrayWhitelist {
    address[] public whitelist;
    
    /**
     * @notice 添加地址到白名单
     * @dev Gas 成本:~50,000(第一次)+ 20,000(后续)
     */
    function addToWhitelist(address _addr) public {
        // 需要先检查是否已存在(O(n) 操作!)
        require(!isWhitelisted(_addr), "Already whitelisted");
        whitelist.push(_addr);
    }
    
    /**
     * @notice 检查地址是否在白名单中
     * @dev ❌ O(n) 时间复杂度,gas 成本随数组大小增长
     */
    function isWhitelisted(address _addr) public view returns (bool) {
        for (uint256 i = 0; i < whitelist.length; i++) {
            if (whitelist[i] == _addr) {
                return true;
            }
        }
        return false;
        // 如果白名单有 100 个地址,最坏情况需要 100 次比较
        // Gas 成本:~3,000 * n(n 是数组大小)
    }
    
    /**
     * @notice 从白名单移除地址
     * @dev ❌ 复杂且 gas 成本高
     */
    function removeFromWhitelist(address _addr) public {
        for (uint256 i = 0; i < whitelist.length; i++) {
            if (whitelist[i] == _addr) {
                // 将最后一个元素移到当前位置
                whitelist[i] = whitelist[whitelist.length - 1];
                whitelist.pop();
                return;
            }
        }
        revert("Not in whitelist");
    }
    
    /**
     * @notice 演示 gas 成本问题
     */
    function demonstrateGasCost() public view returns (uint256) {
        uint256 gasBefore = gasleft();
        isWhitelisted(address(0x123)); // 假设不在列表中
        uint256 gasAfter = gasleft();
        return gasBefore - gasAfter; // 返回消耗的 gas
    }
}

// ✅ 推荐:使用 mapping 实现白名单
contract MappingWhitelist {
    mapping(address => bool) public whitelist;
    uint256 public whitelistCount;
    
    event AddedToWhitelist(address indexed addr);
    event RemovedFromWhitelist(address indexed addr);
    
    /**
     * @notice 添加地址到白名单
     * @dev Gas 成本:~45,000(第一次)或 ~25,000(更新)
     */
    function addToWhitelist(address _addr) public {
        require(!whitelist[_addr], "Already whitelisted");
        whitelist[_addr] = true;
        whitelistCount++;
        emit AddedToWhitelist(_addr);
    }
    
    /**
     * @notice 检查地址是否在白名单中
     * @dev ✅ O(1) 时间复杂度,固定 gas 成本
     */
    function isWhitelisted(address _addr) public view returns (bool) {
        return whitelist[_addr];
        // Gas 成本:~2,100(冷访问)或 ~100(热访问)
        // 无论白名单有多少地址,成本都相同!
    }
    
    /**
     * @notice 从白名单移除地址
     * @dev ✅ 简单且 gas 成本低
     */
    function removeFromWhitelist(address _addr) public {
        require(whitelist[_addr], "Not in whitelist");
        whitelist[_addr] = false;
        whitelistCount--;
        emit RemovedFromWhitelist(_addr);
    }
    
    /**
     * @notice 批量添加地址
     * @dev 高效的批量操作
     */
    function addBatch(address[] calldata _addrs) public {
        for (uint256 i = 0; i < _addrs.length; i++) {
            if (!whitelist[_addrs[i]]) {
                whitelist[_addrs[i]] = true;
                whitelistCount++;
                emit AddedToWhitelist(_addrs[i]);
            }
        }
    }
}

// ✅ 最佳方案:混合使用 mapping 和 array
contract HybridWhitelist {
    mapping(address => bool) public isWhitelisted;
    address[] public whitelistedAddresses;
    mapping(address => uint256) private indexOf; // 地址在数组中的索引
    
    event AddedToWhitelist(address indexed addr);
    event RemovedFromWhitelist(address indexed addr);
    
    /**
     * @notice 添加地址到白名单
     * @dev 同时更新 mapping 和 array
     */
    function addToWhitelist(address _addr) public {
        require(!isWhitelisted[_addr], "Already whitelisted");
        
        // 更新 mapping(快速查找)
        isWhitelisted[_addr] = true;
        
        // 更新 array(可遍历)
        whitelistedAddresses.push(_addr);
        indexOf[_addr] = whitelistedAddresses.length - 1;
        
        emit AddedToWhitelist(_addr);
    }
    
    /**
     * @notice 从白名单移除地址
     * @dev 同时更新 mapping 和 array
     */
    function removeFromWhitelist(address _addr) public {
        require(isWhitelisted[_addr], "Not in whitelist");
        
        // 更新 mapping
        isWhitelisted[_addr] = false;
        
        // 更新 array:将最后一个元素移到被删除位置
        uint256 index = indexOf[_addr];
        uint256 lastIndex = whitelistedAddresses.length - 1;
        address lastAddr = whitelistedAddresses[lastIndex];
        
        whitelistedAddresses[index] = lastAddr;
        indexOf[lastAddr] = index;
        
        whitelistedAddresses.pop();
        delete indexOf[_addr];
        
        emit RemovedFromWhitelist(_addr);
    }
    
    /**
     * @notice 快速检查(使用 mapping)
     */
    function checkWhitelist(address _addr) public view returns (bool) {
        return isWhitelisted[_addr]; // O(1)
    }
    
    /**
     * @notice 获取所有白名单地址(使用 array)
     * @dev ⚠️ 注意:如果白名单很大,可能消耗大量 gas
     */
    function getAllWhitelisted() public view returns (address[] memory) {
        return whitelistedAddresses;
    }
    
    /**
     * @notice 获取白名单大小
     */
    function getWhitelistSize() public view returns (uint256) {
        return whitelistedAddresses.length;
    }
}

/**
 * @title GasComparisonDemo
 * @notice 演示 gas 成本差异
 */
contract GasComparisonDemo {
    ArrayWhitelist public arrayWL;
    MappingWhitelist public mappingWL;
    
    constructor() {
        arrayWL = new ArrayWhitelist();
        mappingWL = new MappingWhitelist();
        
        // 添加 50 个地址到两个白名单
        for (uint160 i = 1; i <= 50; i++) {
            address addr = address(i);
            arrayWL.addToWhitelist(addr);
            mappingWL.addToWhitelist(addr);
        }
    }
    
    /**
     * @notice 对比查找 gas 成本
     */
    function compareGasCost() public view returns (
        uint256 arrayGas,
        uint256 mappingGas
    ) {
        address testAddr = address(25); // 在列表中间
        
        // Array 查找
        uint256 gasBefore1 = gasleft();
        arrayWL.isWhitelisted(testAddr);
        uint256 gasAfter1 = gasleft();
        arrayGas = gasBefore1 - gasAfter1;
        
        // Mapping 查找
        uint256 gasBefore2 = gasleft();
        mappingWL.isWhitelisted(testAddr);
        uint256 gasAfter2 = gasleft();
        mappingGas = gasBefore2 - gasAfter2;
        
        // 结果:arrayGas >> mappingGas
        // 例如:arrayGas ≈ 75,000, mappingGas ≈ 2,100
    }
}

/**
 * @title WhitelistBestPractices
 * @notice 白名单最佳实践
 */
contract WhitelistBestPractices {
    mapping(address => bool) public whitelist;
    
    /**
     * @notice 使用 modifier 简化白名单检查
     */
    modifier onlyWhitelisted() {
        require(whitelist[msg.sender], "Not whitelisted");
        _;
    }
    
    /**
     * @notice 受保护的函数
     */
    function protectedFunction() public onlyWhitelisted {
        // 只有白名单地址可以调用
    }
    
    /**
     * @notice 使用角色管理(更灵活)
     */
    mapping(address => mapping(bytes32 => bool)) public roles;
    bytes32 public constant ADMIN_ROLE = keccak256("ADMIN");
    bytes32 public constant USER_ROLE = keccak256("USER");
    
    function grantRole(address _addr, bytes32 _role) public {
        roles[_addr][_role] = true;
    }
    
    function hasRole(address _addr, bytes32 _role) public view returns (bool) {
        return roles[_addr][_role];
    }
}

理论补充:

数据结构选择的决策树:

复制代码
需要白名单功能?
├─ 只需要检查地址是否在列表中?
│  └─ ✅ 使用 mapping
├─ 需要遍历所有地址?
│  ├─ 地址数量少(< 100)?
│  │  └─ ✅ 使用混合方案(mapping + array)
│  └─ 地址数量多(> 100)?
│     └─ ⚠️ 考虑链下索引或分页
└─ 需要按顺序访问?
   └─ ✅ 使用混合方案

Gas 成本对比(假设白名单有 100 个地址):

操作 Array Mapping 混合方案
添加 ~50k ~45k ~70k
查找 ~300k ~2.1k ~2.1k
删除 ~200k ~25k ~50k
遍历 ~100k 不支持 ~100k

存储成本:

  • Mapping:每个地址 ~20,000 gas(首次写入)
  • Array:每个地址 ~20,000 gas + 数组扩展成本
  • 混合方案:两者之和

实际应用建议:

  1. 纯查找场景:使用 mapping(如访问控制)
  2. 需要遍历:使用混合方案(如空投、批量操作)
  3. 地址很多:考虑 Merkle Tree 验证
  4. 动态管理:使用 OpenZeppelin AccessControl

相关问题:

  • Q27: 什么是访问控制,为什么它很重要?
  • Q28: modifier 有什么作用?
相关推荐
王中阳Go2 小时前
面试被挂的第3次,面试官说:你懂的LLM框架,只够骗骗自己
面试·职场和发展
terminal0073 小时前
浅谈useRef的使用和渲染机制
前端·react.js·面试
川西胖墩墩4 小时前
流程图在算法设计中的实战应用
数据库·论文阅读·人工智能·职场和发展·流程图
Dream it possible!6 小时前
LeetCode 面试经典 150_二叉树层次遍历_二叉树的层序遍历(83_102_C++_中等)
c++·leetcode·面试·二叉树
不会写DN7 小时前
[特殊字符]开班会时由于太无聊,我开发了一个小游戏……
程序人生·信息可视化·职场和发展·交互·图形渲染·学习方法·高考
Kuo-Teng7 小时前
LeetCode 23: Merge k Sorted Lists
算法·leetcode·职场和发展
南山小乌贼7 小时前
集成电路综合总结面试宝典十五
面试·职场和发展·硬件工程师·pcb·硬件测试·硬件面试·集成电路面试
云泽8088 小时前
攻克算法面试:C++ Vector 核心问题精讲
c++·算法·面试
吃饺子不吃馅9 小时前
react-grid-layout 原理拆解:布局引擎、拖拽系统与响应式设计
前端·面试·架构