
映射类型
映射类型的语法为 mapping(KeyType KeyName? => ValueType ValueName?),变量声明形式为 mapping(KeyType KeyName? => ValueType ValueName?) VariableName。
其中:
-
KeyType 可以是任意内置值类型、bytes、string,或任何合约类型或枚举类型。用户自定义的复杂类型(如映射、结构体或数组)不能作为键。
-
ValueType 可以是任意类型,包括映射、数组和结构体。
-
KeyName 和 ValueName 是可选的,只用于标识 getter 函数的输入输出名,不能是类型名称。
我们可以将映射视为哈希表:每个可能的键在内部自动初始化,并映射到该值类型的默认值(例如,uint 类型默认值为 0)。不过与典型哈希表不同,映射本身并不存储键,只使用键的 keccak256 哈希值来查找对应的值。
因此:
-
映射无法获取长度,也无法检查键是否存在。
-
不具备键的可遍历性,除非通过额外数据结构实现。
-
映射只能存在于 storage 中,不能用作 memory 变量,也不能作为函数参数或返回值(包括嵌套在数组或结构体中时)。
我们可以将映射类型的状态变量声明为 public,Solidity 会为其自动生成 getter 函数。若 ValueType 是结构体或值类型,getter 会返回完整的值;若 ValueType 是数组或映射,getter 会为每一层的 KeyType 添加一个参数,并递归生成 getter。
示例如下:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
// 定义一个包含地址和余额的映射的合约
contract MappingExample {
// 声明一个公共的映射,address => uint,记录每个地址的余额
mapping(address => uint) public balances;
// 更新函数:允许发送者更新他们的余额
function update(uint newBalance) public {
balances[msg.sender] = newBalance; // 将调用者的余额更新为 newBalance
}
}
// 定义一个合约用于与 MappingExample 合约进行交互
contract MappingUser {
// 一个函数,用来调用 MappingExample 合约的 update 方法,并返回当前合约地址的余额
function f() public returns (uint) {
// 创建 MappingExample 合约的实例
MappingExample m = new MappingExample();
// 调用 update 函数,将余额设置为 100
m.update(100);
// 返回当前合约地址的余额
return m.balances(address(this)); // 返回 MappingExample 合约中当前合约地址的余额
}
}
下面的示例是一个简化版的 ERC20 代币。_allowances 是一个嵌套映射类型。我们为映射提供了可选的 KeyName 和 ValueName,这不会影响合约功能或字节码,仅用于在 ABI 中为 getter 的输入和输出命名。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.18;
// 定义一个包含映射的智能合约
contract MappingExampleWithNames {
// 定义一个 public 映射,映射地址(address)到余额(uint)。映射的键为 `user`,值为 `balance`。
// 这个映射会自动生成一个 getter 函数,可以根据地址(address)查询对应的余额。
mapping(address user => uint balance) public balances;
// 更新余额的函数,接受一个 `newBalance` 参数。
// 这个函数会将调用者(msg.sender)的余额更新为 `newBalance`。
function update(uint newBalance) public {
// 使用调用者的地址(msg.sender)作为键,更新其对应的余额值。
balances[msg.sender] = newBalance;
}
}
在下例中,_allowances 映射用于记录某个地址被授权从你的账户中提取的金额:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
contract MappingExample {
// 定义一个私有映射 `_balances`,将每个地址映射到一个无符号整数(余额)。
mapping(address => uint256) private _balances;
// 定义一个私有映射 `_allowances`,它是一个二层映射,记录每个地址(owner)允许另一个地址(spender)提取的金额。
mapping(address => mapping(address => uint256)) private _allowances;
// 定义事件,当转账发生时触发。
event Transfer(address indexed from, address indexed to, uint256 value);
// 定义事件,当批准时触发,表明某个地址被授权从另一个地址提取一定金额。
event Approval(address indexed owner, address indexed spender, uint256 value);
// 查询某个地址被授权的提取金额
function allowance(address owner, address spender) public view returns (uint256) {
return _allowances[owner][spender];
}
// 从一个账户向另一个账户转账,同时检查授权金额是否足够
function transferFrom(address sender, address recipient, uint256 amount) public returns (bool) {
// 确保 sender 允许 msg.sender(调用者)提取足够的金额
require(_allowances[sender][msg.sender] >= amount, "ERC20: Allowance not high enough.");
// 减少授权金额
_allowances[sender][msg.sender] -= amount;
// 执行转账
_transfer(sender, recipient, amount);
return true;
}
// 批准另一个地址(spender)从调用者的账户中提取指定金额(amount)
function approve(address spender, uint256 amount) public returns (bool) {
// 确保 spender 地址不是零地址
require(spender != address(0), "ERC20: approve to the zero address");
// 设置授权金额
_allowances[msg.sender][spender] = amount;
// 触发批准事件
emit Approval(msg.sender, spender, amount);
return true;
}
// 内部转账函数,用于更新账户余额,并触发转账事件
function _transfer(address sender, address recipient, uint256 amount) internal {
// 确保 sender 和 recipient 不是零地址
require(sender != address(0), "ERC20: transfer from the zero address");
require(recipient != address(0), "ERC20: transfer to the zero address");
// 确保 sender 有足够的余额
require(_balances[sender] >= amount, "ERC20: Not enough funds.");
// 执行转账:从 sender 减去金额,给 recipient 增加金额
_balances[sender] -= amount;
_balances[recipient] += amount;
// 触发转账事件
emit Transfer(sender, recipient, amount);
}
}
可迭代映射
映射类型无法直接迭代,即无法枚举其键。但可以在其基础上构建一个数据结构,从而实现迭代功能。以下代码实现了一个 IterableMapping 库,User 合约通过该库添加数据,并使用 sum 函数对所有值进行求和。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.8;
// 定义 IndexValue 结构体,用于存储每个键对应的索引和数值
struct IndexValue {
uint keyIndex; // 键的索引位置
uint value; // 键对应的值
}
// 定义 KeyFlag 结构体,用于标记键是否被删除
struct KeyFlag {
uint key; // 键的值
bool deleted; // 是否已删除标志
}
// 定义 itmap 结构体,包含了一个映射和一个存储键的数组,以及当前大小
struct itmap {
mapping(uint => IndexValue) data; // 存储键值对的映射
KeyFlag[] keys; // 存储键的数组
uint size; // 数据大小
}
// 定义一个类型 Iterator,实质上是 uint 类型,用于遍历
type Iterator is uint;
// 定义 IterableMapping 库,提供操作 itmap 类型数据的函数
library IterableMapping {
// 插入数据到 itmap 中,如果已存在则更新数据
function insert(itmap storage self, uint key, uint value) internal returns (bool replaced) {
uint keyIndex = self.data[key].keyIndex; // 获取当前键的索引
self.data[key].value = value; // 更新该键对应的值
if (keyIndex > 0) {
return true; // 如果键已经存在,返回 true,表示数据已替换
} else {
// 如果键不存在,分配一个新的索引
keyIndex = self.keys.length;
self.keys.push(); // 在数组末尾添加一个新元素
self.data[key].keyIndex = keyIndex + 1; // 设置新键的索引
self.keys[keyIndex].key = key; // 将键添加到键数组中
self.size++; // 增加数据大小
return false; // 返回 false,表示插入了新的键值对
}
}
// 从 itmap 中删除指定的键
function remove(itmap storage self, uint key) internal returns (bool success) {
uint keyIndex = self.data[key].keyIndex; // 获取该键的索引
if (keyIndex == 0) {
return false; // 如果键不存在,返回 false
}
delete self.data[key]; // 删除数据映射中的键值对
self.keys[keyIndex - 1].deleted = true; // 将对应的 KeyFlag 标记为已删除
self.size--; // 减小数据大小
return true; // 返回 true,表示删除成功
}
// 检查 itmap 中是否包含指定的键
function contains(itmap storage self, uint key) internal view returns (bool) {
return self.data[key].keyIndex > 0; // 如果该键存在,返回 true
}
// 初始化遍历,返回一个迭代器
function iterateStart(itmap storage self) internal view returns (Iterator) {
return iteratorSkipDeleted(self, 0); // 跳过已删除的项,返回起始迭代器
}
// 检查当前迭代器是否有效
function iterateValid(itmap storage self, Iterator iterator) internal view returns (bool) {
return Iterator.unwrap(iterator) < self.keys.length; // 如果迭代器位置小于键数组长度,则有效
}
// 获取下一个迭代器,跳过已删除的项
function iterateNext(itmap storage self, Iterator iterator) internal view returns (Iterator) {
return iteratorSkipDeleted(self, Iterator.unwrap(iterator) + 1); // 跳到下一个有效项
}
// 获取当前迭代器对应的键和值
function iterateGet(itmap storage self, Iterator iterator) internal view returns (uint key, uint value) {
uint keyIndex = Iterator.unwrap(iterator); // 获取迭代器的索引
key = self.keys[keyIndex].key; // 获取键
value = self.data[key].value; // 获取值
}
// 跳过已删除的项,返回有效的迭代器位置
function iteratorSkipDeleted(itmap storage self, uint keyIndex) private view returns (Iterator) {
while (keyIndex < self.keys.length && self.keys[keyIndex].deleted) // 如果该项被标记为删除,则跳过
keyIndex++;
return Iterator.wrap(keyIndex); // 返回跳过已删除项后的迭代器
}
}
// User 合约使用 IterableMapping 库进行数据操作
contract User {
itmap data; // 声明一个 itmap 类型的变量来保存数据
// 使用 IterableMapping 库来操作 itmap 类型的数据
using IterableMapping for itmap;
// 插入数据到 itmap 中
function insert(uint k, uint v) public returns (uint size) {
// 调用 IterableMapping 库的 insert 函数插入数据
data.insert(k, v);
// 返回当前数据的大小
return data.size;
}
// 计算所有存储数据的总和
function sum() public view returns (uint s) {
// 遍历 itmap 中的所有数据并计算总和
for (
Iterator i = data.iterateStart(); // 初始化迭代器
data.iterateValid(i); // 检查迭代器是否有效
i = data.iterateNext(i) // 获取下一个有效项
) {
(, uint value) = data.iterateGet(i); // 获取当前项的值
s += value; // 将当前值累加到总和
}
}
}
2131

被折叠的 条评论
为什么被折叠?



