【区块链安全 | 第十九篇】类型之映射类型

在这里插入图片描述

映射类型

映射类型使用语法 mapping(KeyType KeyName? => ValueType ValueName?),映射类型的变量声明使用语法 mapping(KeyType KeyName? => ValueType ValueName?) VariableName

KeyType 可以是任何内置值类型、bytesstring 或任何合约类型或枚举类型。其他用户定义的复杂类型,如映射、结构体或数组类型是不允许的。

ValueType 可以是任何类型,包括映射、数组和结构体。

KeyNameValueName 是可选的(因此 mapping(KeyType => ValueType) 也是有效的),它们可以是任何有效的标识符,但不能是类型。

你可以将映射看作哈希表,它在内部初始化,每个可能的键都会映射到一个值,该值的字节表示是全零,即类型的默认值。相似之处仅限于此,键数据不会存储在映射中,只有它的 keccak256 哈希值用于查找值。

由于这个原因,映射没有长度或键值是否已设置的概念,因此无法在没有额外信息的情况下删除映射。

映射只能有存储数据位置,因此只能作为状态变量、作为函数中的存储引用类型,或者作为库函数的参数。它们不能作为合约函数的公共参数或返回参数。这些限制也适用于包含映射的数组和结构体。

你可以将映射类型的状态变量标记为公共的,Solidity 会为你自动创建一个 getter。KeyType 变为 getter 的一个参数,并使用 KeyName(如果指定的话)。如果 ValueType 是值类型或结构体,getter 返回与该类型匹配的 ValueType(如果指定了 ValueName)。如果 ValueType 是数组或映射,则 getter 会有一个参数对应每个 KeyType,并递归处理。

在下面的示例中,MappingExample 合约定义了一个公共的 balances 映射,键类型为 address,值类型为 uint,将以太坊地址映射到无符号整数值。由于 uint 是值类型,getter 返回一个与该类型匹配的值,在 MappingUser 合约中,你可以看到它返回指定地址的值。

// 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 是一个映射类型,嵌套在另一个映射类型内部。我们为映射提供了可选的 KeyNameValueName。这不会影响合约的功能或字节码,它仅仅是在 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; // 将当前值累加到总和
        }
    }
}

猜你喜欢

转载自blog.csdn.net/2301_77485708/article/details/146884107
今日推荐