欢迎来到《Solidity面试修炼之道》专栏💎。
专栏核心理念:
核心 Slogan💸💸:从面试题到实战精通,你的 Web3 开发进阶指南。
一句话介绍🔬🔬: 150+ 道面试题 × 103 篇深度解析 = 你的 Solidity 修炼秘籍。
- ✅ 名称有深度和系统性
- ✅ "修炼"体现进阶过程
- ✅ 适合中文技术社区
- ✅ 记忆度高,易于传播
- ✅ 全场景适用
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 + 数组扩展成本
- 混合方案:两者之和
实际应用建议:
- 纯查找场景:使用 mapping(如访问控制)
- 需要遍历:使用混合方案(如空投、批量操作)
- 地址很多:考虑 Merkle Tree 验证
- 动态管理:使用 OpenZeppelin AccessControl
相关问题:
- Q27: 什么是访问控制,为什么它很重要?
- Q28: modifier 有什么作用?
