跳转到内容

Maps

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

例如,map<Int, Int> 使用 Int 类型作为其键和值:

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

允许的类型

允许的键(key)类型

允许的值(value)类型:

操作

Declare, emptyMap()

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

let fizz: map<Int, Int> = emptyMap();
let fizz: map<Int, Int> = null; // identical to the previous line, but less descriptive

作为 持久状态变量

contract Example {
fizz: map<Int, Int>; // Int keys to Int values
init() {
self.fizz = emptyMap(); // redundant and can be removed!
}
}

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

设置值,.set()

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

// Empty map
let fizz: map<Int, Int> = emptyMap();
// Setting a couple of values under different keys
fizz.set(7, 7);
fizz.set(42, 42);
// Overriding one of the existing key-value pairs
fizz.set(7, 68); // key 7 now points to value 68

获取值,.get()

通过调用 .get() 方法,检查是否在map中找到了键,所有map都可以访问该方法。 如果键丢失,则返回 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: Int = fizz.get(68)!!; // explicitly asserting that the value must not be null,
// which may crush at runtime if the value is, in fact, null
// Alternatively, we can check for the key in the if statement
if (gotButUnsure != null) {
// Hooray, let's use !! without fear now and cast Int? to Int
let definitelyGotIt: Int = fizz.get(68)!!;
} else {
// Do something else...
}

替换值 .replace()

Available since Tact 1.6 (not released yet)

要替换某个键下的值,如果存在这样的键,请使用 .replace() 方法。 它在成功替换时返回 true,否则返回 false

// Empty map
let fizz: map<Int, Int> = emptyMap();
// Setting a couple of values under different keys
fizz.set(7, 70);
fizz.set(42, 42);
// Overriding one of the existing key-value pairs
let replaced1 = fizz.replace(7, 68); // key 7 now points to value 68
replaced1; // true
// Trying to replace the value in a non-existing key-value pair will do nothing
let replaced2 = fizz.replace(8, 68); // no key 8, so nothing was altered
replaced2; // false

如果给定值是null并且键存在,地图中的条目将被删除。

// Empty map
let fizz: map<Int, Int> = emptyMap();
// Setting a couple of values under different keys
fizz.set(7, 70);
fizz.set(42, 42);
// Overriding one of the existing key-value pairs
let replaced1 = fizz.replace(7, null); // the entry under key 7 is now deleted
replaced1; // true
// Trying to replace the value in a non-existing key-value pair will do nothing
let replaced2 = fizz.replace(8, null); // no key 8, so nothing was altered
replaced2; // false

替换并获取旧值,.replaceGet()

Available since Tact 1.6 (not released yet)

类似于 .replace(),但在成功替换时返回旧的(替换前的)值,否则返回 null,而不是返回一个 Boolean

// Empty map
let fizz: map<Int, Int> = emptyMap();
// Setting a couple of values under different keys
fizz.set(7, 70);
fizz.set(42, 42);
// Overriding one of the existing key-value pairs
let oldVal1 = fizz.replaceGet(7, 68); // key 7 now points to value 68
oldVal1; // 70
// Trying to replace the value in a non-existing key-value pair will do nothing
let oldVal2 = fizz.replaceGet(8, 68); // no key 8, so nothing was altered
oldVal2; // null

如果给定值是null,并且键存在,则条目将从映射中删除。

// Empty map
let fizz: map<Int, Int> = emptyMap();
// Setting a couple of values under different keys
fizz.set(7, 70);
fizz.set(42, 42);
// Overriding one of the existing key-value pairs
let oldVal1 = fizz.replaceGet(7, null); // the entry under key 7 is now deleted
oldVal1; // 70
// Trying to replace the value in a non-existing key-value pair will do nothing
let oldVal2 = fizz.replaceGet(8, null); // no key 8, so nothing was altered
oldVal2; // null

删除条目,.del()

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

// Empty map
let fizz: map<Int, Int> = emptyMap();
// Setting a couple of values under different keys
fizz.set(7, 123);
fizz.set(42, 321);
// Deleting one of the keys
let deletionSuccess: Bool = fizz.del(7); // true, because map contained the entry under key 7
fizz.del(7); // false, because map no longer has an entry under key 7
// Note, that assigning the `null` value to the key when using the `.set()` method
// is equivalent to calling `.del()`, although such approach is much less descriptive
// and is generally discouraged:
fizz.set(42, null); // the entry under key 42 is now deleted

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

// Empty map
let fizz: map<Int, Int> = emptyMap();
// Setting a couple of values under different keys
fizz.set(7, 123);
fizz.set(42, 321);
// Deleting all of the entries at once
fizz = emptyMap();
fizz = null; // identical to the previous line, but less descriptive

通过这种方法,即使映射被声明为其持久状态变量,映射的所有先前条目也会被从合约中完全丢弃。 因此,将映射(map)赋值为 emptyMap() 不会产生任何隐藏或意外的存储费用

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

Available since Tact 1.5

map上的 .exists() 方法,如果给定键下的值在map中存在,则返回 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()

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

let fizz: map<Int, Int> = emptyMap();
if (fizz.isEmpty()) {
dump("Empty maps are empty, duh!");
}
// Note, that comparing the map to `null` behaves the same as `.isEmpty()` method,
// although such direct comparison is much less descriptive and is generally discouraged:
if (fizz == null) {
dump("Empty maps are null, which isn't obvious");
}

.deepEquals() 进行比较

Available since Tact 1.5

.deepEquals() 方法 在映射上返回 true,如果映射的所有条目都与另一个映射的相应条目匹配,忽略在底层序列化逻辑中可能存在的差异。 返回 false

let fizz: map<Int, Int> = emptyMap();
let buzz: map<Int, Int> = emptyMap();
fizz.set(1, 2);
buzz.set(1, 2);
fizz.deepEquals(buzz); // true
fizz == buzz; // true, and uses much less gas to compute

使用 .deepEquals()非常重要,因为map来自第三方源,它没有提供关于序列化布局 的任何保证。 例如,考虑以下代码:

some-typescript-code.ts
// First map, with long labels
const m1 = beginCell()
.storeUint(2, 2) // long label
.storeUint(8, 4) // key length
.storeUint(1, 8) // key
.storeBit(true) // value
.endCell();
// Second map, with short labels
const m2 = beginCell()
.storeUint(0, 1) // short label
.storeUint(0b111111110, 9) // key length
.storeUint(1, 8) // key
.storeBit(true) // value
.endCell();

在这里,两张map都是手动形成的,两张map都包含相同的键值。 如果你要在消息中将这两张map发送给Tact合约,然后将它们与.deepEquals()equality operator ==进行比较,前者将产生true ,因为两张map都有相同的条目, 后者会产生false ,因为它只能对map哈希进行浅色比较。 由于map的序列化不同,情况也不同。

转换为 Cell, .asCell()

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

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

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());
}
}

遍历条目

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

// Empty map
let fizz: map<Int, Int> = emptyMap();
// Setting a couple of values under different keys
fizz.set(42, 321);
fizz.set(7, 123);
// Iterating over in a sequential order: from the smallest keys to the biggest ones
foreach (key, value in fizz) {
dump(key); // will dump 7 on the first iteration, then 42 on the second
}

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

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

contract Iteration {
// Persistent state variables
counter: Int as uint32; // counter of map entries, serialized as a 32-bit unsigned
record: map<Int, Address>; // Int to Address map
// Constructor (initialization) function of the contract
init() {
self.counter = 0; // Setting the self.counter to 0
}
// Internal message receiver, which responds to a String message "Add"
receive("Add") {
// Get the Context Struct
let ctx: Context = context();
// Set the entry: counter Int as a key, ctx.sender Address as a value
self.record.set(self.counter, ctx.sender);
// Increase the counter
self.counter += 1;
}
// Internal message receiver, which responds to a String message "Send"
receive("Send") {
// Loop until the value of self.counter (over all the self.record entries)
let i: Int = 0; // declare usual i for loop iterations
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)
mode: SendIgnoreErrors, // send ignoring errors in transaction, if any
body: "SENDING".asComment() // String "SENDING" converted to a Cell as a message body
});
i += 1; // don't forget to increase the i
}
}
// Getter function for obtaining the value of self.record
get fun map(): map<Int, Address> {
return self.record;
}
// Getter function for obtaining the value of self.counter
get fun counter(): Int {
return self.counter;
}
}

在此类map上设置上限限制通常很有用,这样就不会触及极限

序列化

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

struct SerializedMapInside {
// Both keys and values here would be serialized as 8-bit unsigned integers,
// thus preserving the space and reducing storage costs:
countersButCompact: map<Int as uint8, Int as uint8>;
}

局限性和缺点

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

  • 由于智能合约状态大小的上限约为 6500065\,000 项类型为 Cell,因此整个合约的映射存储上限约为 3000030\,000 键值对。

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

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

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

contract Example {
// Declare a compile-time constant upper-bound for our map
const MaxMapSize: Int = 42;
// Persistent state variables
arr: map<Int, Int>; // "array" of Int values as a map
arrLength: Int = 0; // length of the "array", defaults to 0
// Internal function for pushing an item to the end of the "array"
fun arrPush(item: Int) {
if (self.arrLength >= self.MaxMapSize) {
// Do something, stop the operation, for example
} else {
// Proceed with adding new item
self.arr.set(self.arrLength, item);
self.arrLength += 1;
}
}
}

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