Appearance
映射 map<k, v>
复合类型 map<k, v>
用于将类型为 k
的键与对应的类型为 v
的值关联起来。
例如,map<Int, Int>
使用 Int
类型作为其键和值:
solidity
struct IntToInt {
counters: map<Int, Int>;
}
允许的类型
允许的键类型:
- Int
- Address
允许的值类型:
- Int
- Bool
- Cell
- Address
- Struct
- Message
操作
声明
作为局部变量,使用标准库的 emptyMap()
函数:
solidity
let fizz: map<Int, Int> = emptyMap();
let fizz: map<Int, Int> = null; // 与上一行相同,但描述性较差
作为持久状态变量:
solidity
contract Example {
fizz: map<Int, Int>; // Int 键对 Int 值
init() {
self.fizz = emptyMap(); // 冗余,可以删除!
}
}
注意,类型为 map<k, v>
的持久状态变量默认初始化为空,不需要默认值或在 init()
函数中初始化。
设置值,.set()
要设置或替换键下的值,请调用 .set()
方法,该方法对所有映射都可访问。
solidity
// 空映射
let fizz: map<Int, Int> = emptyMap();
// 在不同的键下设置一些值
fizz.set(7, 7);
fizz.set(42, 42);
// 覆盖其中一个现有的键值对
fizz.set(7, 68); // 键 7 现在指向值 68
获取值,.get()
要通过调用 .get()
方法检查映射中是否找到键,该方法对所有映射都可访问。如果键缺失,将返回 null,如果找到键,则返回值。
solidity
// 空映射
let fizz: map<Int, Int> = emptyMap();
// 设置一个值
fizz.set(68, 0);
// 通过其键获取值
let gotButUnsure: String? = fizz.get(68); // 返回 String 或 null,因此类型为 String?
let mustHaveGotOrErrored: String = fizz.get(68)!!; // 明确断言值必须非空,
// 如果实际为空,则可能在运行时崩溃
// 或者,我们可以在 if 语句中检查键
if (gotButUnsure != null) {
// 好极了,现在可以无惧地使用 !! 并将 String? 转换为 String
let definitelyGotIt: String = fizz.get(68)!!;
} else {
// 做些其他事情...
}
删除条目
要删除单个键值对(单个条目),只需在使用 .set()
方法时将键的值赋为 null。
solidity
// 空映射
let fizz: map<Int, Int> = emptyMap();
// 在不同的键下设置一些值
fizz.set(7, 123);
fizz.set(42, 321);
// 删除其中一个键
fizz.set(7, null); // 键 7 下的条目现已删除
要一次删除映射中的所有条目,请使用 emptyMap()
函数重新分配映射:
solidity
// 空映射
let fizz: map<Int, Int> = emptyMap();
// 在不同的键下设置一些值
fizz.set(7, 123);
fizz.set(42, 321);
// 一次删除所有条目
fizz = emptyMap();
fizz = null; // 与上一行相同,但描述性较差
使用这种方法,映射中的所有先前条目都将从合约中完全丢弃,即使映射被声明为其持久状态变量。因此,将映射赋值为 emptyMap()
不会引发任何隐藏或突然的存储费用。
转换为 Cell,.asCell()
使用 .asCell()
方法将映射的所有值转换为 Cell 类型。请注意,Cell 类型能够存储多达 1023 位,因此将较大的映射转换为 Cell 将导致错误。
例如,此方法用于在回复的正文中直接发送小型映射:
solidity
contract Example {
// 持久状态变量
fizz: map<Int, Int>; // 我们的映射
// 合约的构造(初始化)函数
init() {
// 设置一堆值
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);
}
// 内部消息接收器,响应空消息
receive() {
// 在这里我们将映射转换为 Cell 并用它做出回复
self.reply(self.fizz.asCell());
}
}
遍历条目
目前 Tact 没有专门的语法用于遍历映射。然而,如果你定义一个以 Int 类型为键的 map<Int, v>
并跟踪条目数量的单独变量,可以将映射作为简单数组使用:
solidity
contract Iteration {
// 持久状态变量
counter: Int as uint32; // 映射条目计数器,以 32 位无符号整数序列化
record: map<Int, Address>; // Int 到 Address 的映射
// 合约的构造(初始化)函数
init() {
self.counter = 0; // 将 self.counter 设置为 0
}
// 内部消息接收器,响应字符串消息 "Add"
receive("Add") {
// 获取 Context 结构体
let ctx: Context = context();
// 设置条目:counter Int 作为键,ctx.sender Address 作为值
self.record.set(self.counter, ctx.sender);
// 增加计数器
self.counter += 1;
}
// 内部消息接收器,响应字符串消息 "Send"
receive("Send") {
// 循环直到 self.counter 的值(遍历所有 self.record 条目)
let i: Int = 0; // 声明用于循环迭代的常规 i
while (i < self.counter) {
send(SendParameters{
bounce: false, // 不反弹此消息
to: self.record.get(i)!!, // 设置发件人地址,知道映射中存在键 i
value: ton("0.0000001"), // 100 纳吨币(纳吨)
mode: SendIgnoreErrors, // 忽略交易中的任何错误发送
body: "SENDING".asComment() // 将字符串 "SENDING" 转换为 Cell 作为消息正文
});
i += 1; // 不要忘记增加 i
}
}
// 获取函数,用于获取 self.record 的值
get fun map(): map<Int, Address> {
return self.record;
}
// 获取函数,用于获取 self.counter 的值
get fun counter(): Int {
return self.counter;
}
}
通常有用的是对此类映射设置上限限制,以免达到限制。
注意,手动跟踪项目数量或检查此类映射的长度非常容易出错,通常不鼓励这样做。相反,尝试将您的映射包装到结构体中并在其上定义扩展函数。参见 Cookbook 中的示例:如何使用包装在结构体中的映射模拟数组。
此示例改编自 howardpen9/while-example-tact。
在 Cookbook 中查看映射使用的其他示例:
- 如何使用包装在结构体中的映射模拟栈
- 如何使用包装在结构体中的映射模拟循环缓冲区
序列化
可以对映射的键、值或两者进行整数序列化,以节省空间并降低存储成本:
solidity
struct SerializedMapInside {
// 在此,键和值都将序列化为 8 位无符号整数,
// 从而节省空间并降低存储成本:
countersButCompact: map<Int as uint8, Int as uint8>;
}
阅读有关其他序列化选项的信息:与 FunC 的兼容性。
限制和缺点
虽然在小规模上使用映射很方便,但如果条目数量无限制且映射可能显著增长,会引起一些问题:
由于智能合约状态大小的上限约为 65,000 个类型为 Cell 的项目,这限制了整个合约的映射存储限制大约为 30,000 个键值对。
映射中的条目越多,计算费用就越大。因此,处理大型映射使计算费用难以预测和管理。
在单个合约中使用大型映射不允许分配其工作负载。因此,与使用较小的映射和一系列互动智能合约相比,它可能使整体性能变得更糟。
为了解决这些问题,你可以将映射的上限限制设置为一个常量,并在每次向映射设置新值时检查它:
solidity
contract Example {
// 为我们的映射声明一个编译时常量上限
const MaxMapSize: Int = 42;
// 持久状态变量
arr: map<Int, Int>; // 作为映射的“数组” String 值
arrLength: Int = 0; // “数组”的长度,默认为 0
// 合约的构造(初始化)函数
init() {}
// 内部函数,用于将项目推送到“数组”的末尾
fun arrPush(item: String) {
if (self.arrLength >= self.MaxMapSize) {
// 做些事情,例如停止操作
} else {
// 继续添加新项目
self.arr.set(self.arrLength, item);
self.arrLength += 1;
}
}
}
如果你仍然需要一个大型映射或一个无界(无限大)的映射,最好根据 TON 区块链的异步和基于演员模型架构你的智能合约。也就是说,使用合约分片,并实际上使整个区块链成为你的映射的一部分。