Solidity 合约分析:3. Iterable Mapping

介绍

你好,我是 Pluveto。我发现多数 Solidity 的教程都是从语法开始讲起,但是我觉得这样不够直观,而且很消耗耐心。这是一个系列文章,我会在这个系列里分析一些简单的 Solidity 合约。在例子中学习。

希望你能学到东西,让我们开始吧!

代码

来自:Iterable Mapping | Solidity by Example | 0.8.20

 1// SPDX-License-Identifier: MIT
 2pragma solidity ^0.8.20;
 3
 4library IterableMapping {
 5    // Iterable mapping from address to uint;
 6    struct Map {
 7        address[] keys;
 8        mapping(address => uint) values;
 9        mapping(address => uint) indexOf;
10        mapping(address => bool) inserted;
11    }
12
13    function get(Map storage map, address key) public view returns (uint) {
14        return map.values[key];
15    }
16
17    function getKeyAtIndex(Map storage map, uint index) public view returns (address) {
18        return map.keys[index];
19    }
20
21    function size(Map storage map) public view returns (uint) {
22        return map.keys.length;
23    }
24
25    function set(Map storage map, address key, uint val) public {
26        if (map.inserted[key]) {
27            map.values[key] = val;
28        } else {
29            map.inserted[key] = true;
30            map.values[key] = val;
31            map.indexOf[key] = map.keys.length;
32            map.keys.push(key);
33        }
34    }
35
36    function remove(Map storage map, address key) public {
37        if (!map.inserted[key]) {
38            return;
39        }
40
41        delete map.inserted[key];
42        delete map.values[key];
43
44        uint index = map.indexOf[key];
45        address lastKey = map.keys[map.keys.length - 1];
46
47        map.indexOf[lastKey] = index;
48        delete map.indexOf[key];
49
50        map.keys[index] = lastKey;
51        map.keys.pop();
52    }
53}
54
55contract TestIterableMap {
56    using IterableMapping for IterableMapping.Map;
57
58    IterableMapping.Map private map;
59
60    function testIterableMap() public {
61        map.set(address(0), 0);
62        map.set(address(1), 100);
63        map.set(address(2), 200); // insert
64        map.set(address(2), 200); // update
65        map.set(address(3), 300);
66
67        for (uint i = 0; i < map.size(); i++) {
68            address key = map.getKeyAtIndex(i);
69
70            assert(map.get(key) == i * 100);
71        }
72
73        map.remove(address(1));
74
75        // keys = [address(0), address(3), address(2)]
76        assert(map.size() == 3);
77        assert(map.getKeyAtIndex(0) == address(0));
78        assert(map.getKeyAtIndex(1) == address(3));
79        assert(map.getKeyAtIndex(2) == address(2));
80    }
81}

解释

由于原生的 Solidity 并不支持 Mapping 迭代,所以我们需要自己实现。

library 关键字

library 关键字在 Solidity 中用于定义库合约 (library contract)。

库合约与普通合约有以下不同:

  • 库合约使用 library 关键字定义,普通合约使用 contract。

  • 库合约中的函数及变量默认为内部的 (internal), 不能直接在交易或消息中直接访问。

  • 需要通过其他合约使用 library 把库合约链接进来,以使调用库中的函数。

  • 链接库合约后,库中的函数就可以像普通合约内部函数一样调用。

例如:

 1library Math {
 2  function sqrt(uint x) internal pure returns (uint y) {
 3    // ...
 4  }
 5}
 6
 7contract C {
 8  using Math for uint; 
 9  function f(uint x) public pure returns (uint) {
10    return x.sqrt(); 
11  }
12}

库合约可以让公共功能共享,提高代码可重用性。

IterableMapping 库中:

  • keys 是一个动态数组,用于存储所有的 key。

  • values 是一个 mapping,用于存储 key 对应的 value。

  • indexOf 是一个 mapping,用于存储 key 在 keys 中的索引。

  • inserted 是一个 mapping,用于存储 key 是否已经插入。

从而可以实现如下功能:

  1. get: 通过 key 获取 value。直接查询 values。

  2. getKeyAtIndex: 通过索引获取 key。直接查询 keys。

  3. size: 获取 keys 的长度。直接查询 keys 的长度。

  4. set: 设置 key 对应的 value。如果 key 已经存在,则更新 value;如果 key 不存在,则插入 key。

  5. remove: 删除 key。如果 key 不存在,则不做任何操作。

  • 对于 map,删除语法是 delete map[key]

  • 对于动态数组,删除分为下面的步骤:

    1. 先获得 lastKey,即 keys 的最后一个元素。

    2. 把 lastKey 移动到 key 的位置。

    3. 删除最后一个元素:keys.pop()