跳到主要内容

映射(mapping)

映射类型是一种键值对的映射关系存储结构, 在功能上和Java的Map、Python的Dict差不多。

映射是一种使用非常广泛的类型,经常在合约中充当一个类似数据库的角色,比如在代币合约中用映射来存储账户的余额,在游戏合约里可以用映射来存储每个账号的级别,如:

mapping(address => uint) public balances;
mapping(address => uint) public userLevel;

映射的定义为mapping(KeyType => ValueType)KeyType 表示键的类型,ValueType 表示值的类型。

我们可以通过键来获取到对应的值,例如:balances[userAddr] 用来获取某个地址的余额,访问形式很类似于通过下标来获取某个数组元素的值。

类似的,给某个键赋值也是一样,下面是一段示例代码:

pragma solidity >=0.8.0;

contract testMapping {
mapping(address => uint) public balances;

function update(uint newBalance) public {
balances[msg.sender] = newBalance;
}

function get(address key) public view returns(uint) {
return balances[key];
}
}

映射特性与限制

  1. 映射变量只能保存在存储中(storage),通常作为状态变量。
pragma solidity >=0.8.0;

contract testMapping {
mapping(address => uint) balances; // 正确, 默认为 storage

function init(uint newBalance) public {
mapping(address => uint) memory balances; // 错误, 不可以为 memory
}
}
  1. 键类型有一些限制,仅支持Solidity内置值类型、bytesstring 、合约或枚举,不可以是复杂类型, 如:映射、变长数组、结构体。值的类型是没有任何限制,可以为任何类型。

    pragma solidity >=0.8.0;
    contract MappingExample {
    struct Funder {
    address addr;
    uint amount;
    }

    mapping (uint => Funder) idFunders;
    mapping (Funder => uint) funderIds; // 错误, Key 不可以是结构体
    }
  1. Solidity 里的映射是没有长度的,也没有键集合或值集合的概念,因此是没法对映射进行遍历。
pragma solidity >=0.8.0;
contract MappingExample {
mapping(address => uint) balances;

function length() public view returns(uint) {
return balances.length; // 错误
}
}
  1. 映射是可以嵌套的, 嵌套映射是指映射的 Value 是另一个映射, 例如:

    contract testMapping {
    mapping(address => mapping(address => uint)) tokenBalances;
    }

    例如,我们一个合约里存了多种 Token, 我们可能就需要使用如上 tokenBalances 来保存每个用户在每个 token 上的余额。

映射访问器

对于状态变量标记为public的映射类型,编译器生成的访问器和数组一致,参数是键类型,返回值类型。

mapping (uint => uint) public idScore;

会类似这样的访问器函数:

function idScore(uint i) external returns (uint) {
return idScore[i];
}

一个稍微复杂一些的例子,以下是一个嵌套映射 :

pragma solidity >0.8.0;
contract Complex {
struct Data {
uint a;
bytes3 b;
mapping (uint => uint) map;
}
mapping (uint => mapping(bool => Data[])) public data;
}

public 的 data 变量会生成以下访问器函数:

function data(uint arg1, bool arg2, uint arg3) external returns (uint a, bytes3 b) {
a = data[arg1][arg2][arg3].a;
b = data[arg1][arg2][arg3].b;
}

Solidity 数组 vs 映射

有时候,我们既可以使用数组存数据,有可以使用映射。

Solidity 数组更适合数据迭代(例如,使用 for 循环),而基于一个已知的键来获取值时,映射更适合(即不需要迭代获得数据)。

与从映射中获取数据相比,在 Solidity 中对数组进行迭代相对来说Gas消耗更大,而且尽量不要让数组太大。

可迭代映射

有时候,可能希望在智能合约中对映射进行迭代或者计算映射长度,这时可以可以创建一个键的数组,例如:

pragma solidity >=0.8.0;

contract IterableMapping {
mapping(address => uint) public balances;
address[] users;


function length() public view returns(uint) {
return users.length;
}

function insert(address key, uint value) public {
balances[key] = value;
users.push(key);
}

}

Solidity 中有一个更复杂的可迭代的映射的例子

不过这种实现的可迭代映射, Gas 成本较高,还有另一个方式是使用 mapping 来实现一个链表,用链表来保存下一个元素来进行迭代(我比较推荐的实现)。

pragma solidity >=0.8.0;

contract IterableMapping {
mapping(address => uint) public balances;
mapping(address => address) public nextUser;

address constant GUARD = address(1);

// 如果需要长度的话
uint public listSize;

function insert(address key, uint value) public {
balances[key] = value;

// 元素插入链表
nextUser[key] = nextUser[GUARD];
nextUser[GUARD] = key;
listSize ++;
}

}

如果有兴趣了解更多链表实现细节, 例如如何删除、迭代等,可以参考专栏编写 O(1) 复杂度的可迭代映射

小结

提炼本节的重点:映射类型是一种键值对的映射关系存储结构,映射类型变量只能保存在存储中, 且映射本身不支持迭代,不能获取长度。

我们也介绍了几种方法来实现可迭代映射。

------

DeCert.me 码一个未来,DeCert 让每一位开发者轻松构建自己的可信履历。

DeCert.me 由登链社区 @UpchainDAO 孵化,欢迎 Discord 频道 一起交流。

本教程来自贡献者 @Tiny熊