跳转到内容

地图

复合类型 map<K, V> 用于将 K 类型的键与 V 类型的相应值关联起来。

例如,map<Int, Int> 的键和值使用 [英特][英特] 类型:

struct IntToInt {
counters: map<Int, Int>;
}

允许的类型

允许的密钥类型

允许的值类型:

业务

声明,emptyMap()

作为局部变量,使用标准库的 emptyMap() 函数:

let fizz: map<Int, Int> = emptyMap();
let fizz: map<Int, Int> = null; // 与前一行相同,但描述性较弱

作为 持久状态变量

contract Example {
fizz: map<Int, Int>; // Int keys to Int values
init() {
self.fizz = emptyMap(); // 冗余,可以删除!
}
}

请注意,类型为 map<K, V>持久状态变量 默认为空,不需要默认值,也不需要在 init() 函数中进行初始化。

设置值,.set()

要设置或替换键下的值,请调用 .set() 方法,所有地图都可以使用该方法。

// 空 map
let fizz: map<Int, Int> = emptyMap();
// 在不同的键下设置几个值
fizz.set(7, 7);
fizz.set(42, 42);
// 覆盖现有键值对中的一个
fizz.set(7, 68); // 键 7 现在指向值 68

获取值,.get()

通过调用 .get() 方法,检查是否在地图中找到了键,所有地图都可以访问该方法。 如果键丢失,则返回 null;如果键找到,则返回值。

// Empty map
let fizz: map<Int, Int> = emptyMap();
// Setting a value
fizz.set(68, 0);
// Getting the value by its key
let gotButUnsure: Int? = fizz.get(68); // returns Int or null, therefore the type is Int?
let mustHaveGotOrErrored:让 mustHaveGotOrErrored: Int = fizz.get(68)!!; // 明确断言值不能为空,
// 如果值实际上为空,运行时可能会崩溃
// 或者,我们可以在 if 语句中检查关键字
if (gotButUnsure != null) {
// 万岁,现在让我们毫无顾忌地使用 !! 并将 Int? 转换为 Int
definitelyGotItInt = fizz.get(68)!!;
} else {
// Do something else...
}

删除条目,.del()

要删除单个键值对(单个条目),请使用 .del() 方法。 如果删除成功,则返回 true,否则返回 false

// 空 map
let fizz: map<Int, Int> = emptyMap();
// 在不同的键下设置几个值
fizz.set(7, 123);
fizz.set(42, 321);
// 删除其中一个键
let deletionSuccessBool = fizz.del(7); // true,因为 map 包含了键 7 下的条目
fizz.del(7); // false,因为 map 不再有键 7 下的条目
// 注意,在使用 `.set()` 方法
// 等同于调用 `.del()`,不过这种方法的描述性要差得多
// 而且一般不推荐使用:
fizz.set(42, null); // 键 42 下的条目现在被删除了

要删除映射表中的所有条目,请使用 emptyMap() 函数重新分配映射表:

// 空 map
let fizz: map<Int, Int> = emptyMap();
// 在不同的键下设置几个值
fizz.set(7, 123);
fizz.set(42, 321);
// 一次性删除所有条目
fizz = emptyMap();
fizz = null; // 与上一行相同,但描述性较弱

采用这种方法后,即使已将映射声明为持久状态变量,映射之前的所有条目也会从合约中完全删除。 因此,将地图分配给 emptyMap() 不会产生任何隐藏或突然的存储费用

检查条目是否存在, .exists()

Available since Tact 1.5

映射上的 .exists() 方法,如果给定键下的值在映射中存在,则返回 true,否则返回 false

let fizz: map<Int, Int> = emptyMap();
fizz.set(0, 0);
if (fizz.exists(2 + 2)) { // false
dump("Something doesn't add up!");
}
if (fizz.exists(1 / 2)) { // true
dump("I told a fraction joke once. It was half funny.");
}
if (fizz.get(1 / 2) != null) { // also true, but consumes more gas
dump("Gotta pump more!");
}

检查是否为空,.isEmpty()

地图上的 .isEmpty() 方法 如果地图为空,则返回 true,否则返回 false

let fizz: map<Int, Int> = emptyMap();
if (fizz.isEmpty()) {
dump("Empty maps are empty, duh!");
}
// 请注意,将地图与 "空 "进行比较的行为与".isEmpty() "方法相同,
// 尽管这种直接比较的描述性要差得多,一般不鼓励使用:
if (fizz == null) {
dump("Empty maps are null, which isn't obvious");
}

转换为 Cell, .asCell()

在地图上使用 .asCell() 方法,将其所有值转换为 [单元格][单元格] 类型。 请注意,[Cell][单元格] 类型最多只能存储 1023 位,因此将更大的映射转换为单元格会导致错误。

例如,这种方法适用于在回复正文中直接发送小地图:

contract Example {
// Persistent state variables
fizz: map<Int, Int>; // our map
// Constructor (initialization) function of the contract
init() {
// Setting a bunch of values
self.fizz.set(0, 3);
self.fizz.set(1, 14);
self.fizz.set(2, 15);
self.fizz.set(3, 926);
self.fizz.set(4, 5_358_979_323_846);
}
// Internal message receiver, which responds to empty messages
receive() {
// Here we're converting the map to a Cell and making a reply with it
self.reply(self.fizz.asCell());
}
}

遍历条目

要遍历地图条目,有一个 foreach循环语句:

// Empty map
let fizz: map<Int, Int> = emptyMap();
// 在不同的键下设置几个值
fizz.set(42, 321);
fizz.set(7, 123);
// 按顺序迭代:从最小的键到最大的键
foreach (key, value in fizz) {
dump(key); // 第一次迭代将转存 7,第二次迭代将转存 42
}

了解更多相关信息:foreach loop in Book→Statements.

请注意,也可以将映射作为简单数组使用,只要定义一个 map<Int, V>,键为 Int 类型,值为任何允许的 V 类型,并跟踪单独变量中的项数即可:

contract Iteration {
// 持久状态变量
counterint as uint32; // 地图条目的计数器,序列化为 32 位无符号
record: map<Int, Address>; // Int to Address map
// 合约的构造函数(初始化)
init() {
self.counter = 0; // 设置 self.counter 为 0
} // 内部消息接收器,用于响应字符串消息 "Add" receive("Add") { // 获取上下文结构体 let c.counter = 0.
// 内部消息接收器,响应字符串消息 "Add"
receive("Add") {
// 获取上下文结构
let ctxContext = context();
// 设置条目:counter Int 作为键,ctx.sender Address 作为值
self.record.set(self.counter, ctx.sender);
// 增加计数器
self.counter += 1;
}
receive("Send") {
// 循环,直到 self.counter 的值(遍历所有 self.record 条目)
let i. Int = 0; // 声明通常的 i.counter = 1; // 增加计数器的值:Int = 0; // 为循环迭代声明通常的 i
while (i < self.counter) {
send(SendParameters{
bounce: false, // do not bounce back this message
to: self.record.get(i)!!, // set the sender address, knowing that key i exists in the map
value: ton("0.0000001"), // 100 nanoToncoins (nano-tons)
modeSendIgnoreErrors, // 忽略交易中的错误(如果有的话)发送
body"SENDING".asComment() // "SENDING "字符串转换为单元格作为消息正文
});
i += 1; // 不要忘记增加 i
}
}
// 获取 self.record 值的获取函数
get fun map(): map<Int, Address> {
return self.record;
}
// 获取 self.counter 值的获取函数
get fun counter():Int {
return self.counter;
}
}

在此类地图上设置上限限制通常很有用,这样就不会[触及极限](#limits-and-drawbacks)。

序列化

可以对映射键、值或两者进行整数序列化,以保留空间并降低存储成本

struct SerializedMapInside {
// 这里的键和值都将序列化为 8 位无符号整数,
// 从而节省空间并降低存储成本:
countersButCompact: map<Int as uint8, Int as uint8>;
}

局限性和缺点

虽然地图在小范围内使用起来很方便,但如果项目数量不受限制,地图的大小会大幅增加,就会产生很多问题:

  • 由于智能合约状态大小的上限约为 [Cell][cell] 类型的 65,00065,000 项,因此整个合约的映射存储上限约为 30,00030,000 键值对。

  • 地图中的条目越多,计算费 就越高。 因此,处理大型地图使得计算费用难以预测和管理。

  • 在单个合同中使用大型地图无法分散工作量。 因此,与使用较小的地图和大量交互式智能合约相比,这可能会使整体性能大打折扣。

要解决此类问题,可以将地图上的上限限制设置为常数,并在每次为地图设置新值时对其进行检查:

contract Example {
// 为我们的 map 声明一个编译时常数上限
const MaxMapSizeInt = 42;
// 持久状态变量
arr: map<Int, Int>; // Int 值的 "数组 "作为 map
arrLengthInt = 0; // "数组 "的长度,默认为 0
// 将一个项目推到 "数组 "末尾的内部函数
fun arrPush(item: Int) {
if (self.arrLength >= self.MaxMapSize) {
// 做一些事情,例如停止操作
} else {
// 继续添加新项
self.arr.set(self.arrLength, item);
self.arrLength += 1;
}
}
}

如果您仍然需要大地图或无约束(无限大)地图,最好按照TON 区块链的异步和基于角色的模型来构建您的智能合约。 也就是说,使用合约分片,让整个区块链成为地图的一部分。