Skip to content

映射 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 区块链的异步和基于演员模型架构你的智能合约。也就是说,使用合约分片,并实际上使整个区块链成为你的映射的一部分。