映射(Mapping)是Solidity中最常用的数据结构之一,类似于哈希表,用于存储键值对。本文详细介绍映射的定义、特性、使用方法和实际应用场景。
一、什么是映射
1. 基本概念
映射(Mapping)是Solidity中的键值对存储结构,类似于其他语言中的哈希表或字典。映射使用键来查找对应的值,提供O(1)的查找效率。
2. 映射的特点
键值对存储
自动初始化
- 所有可能的键都已初始化
- 不存在的键返回默认值
- 整数类型返回0
- 布尔类型返回false
无法遍历
- 不能直接遍历所有键
- 需要额外维护键列表
- 适合快速查找
- 不适合需要遍历的场景
二、如何定义和使用映射
1. 基本定义
简单映射
1 2 3 4 5 6 7 8 9 10 11 12 13
| pragma solidity ^0.8.0;
contract MappingExample { mapping(address => uint256) public balances; mapping(address => mapping(address => uint256)) public allowances; mapping(uint256 => string) public names; }
映射语法: - `mapping(键类型 => 值类型)`
|
- 键类型可以是值类型
- 值类型可以是任何类型
- 可以嵌套映射
2. 基本操作
1 2 3 4
| 设置值:
function setBalance(address account, uint256 amount) public { balances[account] = amount;
|
}
1 2 3
| function setAllowance(address owner, address spender, uint256 amount) public { allowances[owner][spender] = amount;
|
}
获取值:solidity
1 2 3
| function getBalance(address account) public view returns (uint256) { return balances[account];
|
}
1 2 3
| function getAllowance(address owner, address spender) public view returns (uint256) { return allowances[owner][spender];
|
}
检查存在:solidity
1 2 3
| function hasBalance(address account) public view returns (bool) { return balances[account] > 0;
|
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
二维映射:
function approve(address spender, uint256 amount) public { allowances[msg.sender][spender] = amount; }
function transferFrom( address from, address to, uint256 amount ) public { require(allowances[from][msg.sender] >= amount, "Insufficient allowance"); require(balances[from] >= amount, "Insufficient balance");
allowances[from][msg.sender] -= amount; balances[from] -= amount; balances[to] += amount; } `三维映射:`solidity
mapping(address => mapping(uint256 => mapping(string => bool))) public permissions;
function setPermission(address user, uint256 level, string memory action) public { permissions[user][level][action] = true; }
|
三、应用场景
1. ERC20代币
1 2 3 4 5 6 7 8 9 10
| 余额和授权: contract ERC20Token { java function transfer(address to, uint256 amount) public returns (bool) { require(balances[msg.sender] >= amount, "Insufficient balance"); balances[msg.sender] -= amount; return true;
|
}
1 2
| function approve(address spender, uint256 amount) public returns (bool) {
|
}
1 2
| ) public returns (bool) {
|
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| #### 2. 投票系统
投票记录: contract Voting { mapping(address => bool) public hasVoted; mapping(uint256 => uint256) public voteCounts; mapping(address => uint256) public userVotes;
function vote(uint256 proposalId) public { require(!hasVoted[msg.sender], "Already voted");
hasVoted[msg.sender] = true;
userVotes[msg.sender] = proposalId;
voteCounts[proposalId]++; }
function getVoteCount(uint256 proposalId) public view returns (uint256) { return voteCounts[proposalId]; }
|
3. 权限管理
1 2 3 4 5 6 7 8 9 10 11 12 13
| 角色权限: contract AccessControl { mapping(address => bool) public isAdmin; mapping(address => bool) public isModerator; mapping(address => mapping(string => bool)) public permissions; java function grantAdmin(address user) public { require(isAdmin[msg.sender], "Not admin"); isAdmin[user] = true;
|
}
1 2 3
| function grantPermission(address user, string memory action) public { permissions[user][action] = true;
|
}
1 2 3
| function hasPermission(address user, string memory action) public view returns (bool) { return isAdmin[user] || permissions[user][action];
|
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
用户信息: contract UserData { struct UserInfo { string name; uint256 registrationTime; bool isActive; }
mapping(address => UserInfo) public users; mapping(string => address) public nameToAddress;
function register(string memory name) public { require(users[msg.sender].registrationTime == 0, "Already registered"); require(nameToAddress[name] == address(0), "Name taken");
users[msg.sender] = UserInfo({ name: name, registrationTime: block.timestamp, isActive: true });
nameToAddress[name] = msg.sender; }
|
四、遍历映射的解决方案
1. 维护键列表
1 2 3 4 5 6 7 8 9
| 可遍历映射: contract IterableMapping { address[] public accountList;
if (balances[account] == 0 && amount != 0) { accountList.push(account);
|
}
1 2 3
| // 如果金额为0,从列表中移除 if (amount == 0) { removeAccount(account)
|
}
1 2 3 4 5 6 7 8 9
| function removeAccount(address account) internal { for (uint256 i = 0; i < accountList.length; i++) { if (accountList[i] == account) {
accountList[i] = accountList[accountList.length - 1];
accountList.pop(); break;
|
}
1 2 3
| function getAllAccounts() public view returns (address[] memory) { return accountList;
|
}
1 2 3 4
| function getTotalBalance() public view returns (uint256) { uint256 total = 0; total += balances[accountList[i]];
|
}
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| #### 2. 使用数组索引
索引映射: contract IndexedMapping { mapping(uint256 => address) public indexToAddress; mapping(address => uint256) public addressToIndex; uint256 public accountCount;
function addAccount(address account) public { require(addressToIndex[account] == 0, "Account exists"); accountCount++; indexToAddress[accountCount] = account; addressToIndex[account] = accountCount; }
function getAllBalances() public view returns (uint256[] memory) { uint256[] memory result = new uint256[](accountCount); for (uint256 i = 1; i <= accountCount; i++) { result[i - 1] = balances[indexToAddress[i]]; } return result; }
|
五、最佳实践
1. Gas优化
1 2 3 4 5 6
| 减少存储操作:
function batchUpdate(address[] memory accounts, uint256[] memory amounts) public { require(accounts.length == amounts.length, "Arrays length mismatch"); for (uint256 i = 0; i < accounts.length; i++) { balances[accounts[i]] = amounts[i];
|
}
使用packed storage:solidity
1 2 3 4
| struct PackedData { uint128 value1; uint128 value2;
|
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| mapping(address => PackedData) public packedBalances
#### 2. 安全考虑
输入验证: require(account != address(0), "Invalid address") require(amount <= type(uint128).max, "Amount too large") } `重入保护:`solidity bool private locked
// 防止重入攻击的修改器 modifier nonReentrant() { require(!locked, "Reentrant call") locked = true _ locked = false }
// 提现函数,使用重入保护 function withdraw(uint256 amount) public nonReentrant { payable(msg.sender).transfer(amount) }
|
3. 设计模式
1 2 3 4 5 6 7 8 9 10
| 工厂模式: mapping(address => bool) public isContract mapping(address => address) public contractOwner java // 部署新合约 function deployContract() public returns (address) { address newContract = address(new MyContract()) isContract[newContract] = true contractOwner[newContract] = msg.sender return newContract
|
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| ## 六、常见问题
#### 1. 无法遍历
问题: - 映射无法直接遍历 - 需要维护额外列表 - 增加Gas消耗
解决: - 维护键数组 - 使用索引映射 - 考虑使用数组替代 - 根据需求选择
#### 2. 默认值问题
- 可能误判存在性 - 需要额外检查
- 使用单独的存在性映射 - 检查特定值(如0) - 使用结构体包装 - 明确处理默认值
## 七、总结
映射是Solidity中最重要和常用的数据结构之一。关键要点:
基本特性: - 键值对存储 - 高效查找
应用场景: - ERC20代币 - 投票系统 - 权限管理 - 用户数据
最佳实践: - 合理设计结构 - 优化Gas消耗 - 注意安全考虑 - 处理遍历需求
通过深入理解映射的特性,可以高效地组织和管理数据,构建各种复杂的智能合约应用。
|
本文标题: Solidity映射类型
发布时间: 2024年11月23日 00:00
最后更新: 2026年09月16日 05:40
原始链接: https://haoxiang.eu.org/c29026e9/
版权声明: 本文著作权归作者所有,均采用CC BY-NC-SA 4.0许可协议,转载请注明出处!