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 maplet fizz: map<Int, Int> = emptyMap();
// Setting a couple of values under different keysfizz.set(7, 7);fizz.set(42, 42);
// Overriding one of the existing key-value pairsfizz.set(7, 68); // key 7 now points to value 68
获取值,.get()
通过调用 .get()
方法,检查是否在map中找到了键,所有map都可以访问该方法。 如果键丢失,则返回 null
;如果键找到,则返回值。
// Empty maplet fizz: map<Int, Int> = emptyMap();
// Setting a valuefizz.set(68, 0);
// Getting the value by its keylet 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 statementif (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 maplet fizz: map<Int, Int> = emptyMap();
// Setting a couple of values under different keysfizz.set(7, 70);fizz.set(42, 42);
// Overriding one of the existing key-value pairslet replaced1 = fizz.replace(7, 68); // key 7 now points to value 68replaced1; // true
// Trying to replace the value in a non-existing key-value pair will do nothinglet replaced2 = fizz.replace(8, 68); // no key 8, so nothing was alteredreplaced2; // false
如果给定值是null
并且键存在,地图中的条目将被删除。
// Empty maplet fizz: map<Int, Int> = emptyMap();
// Setting a couple of values under different keysfizz.set(7, 70);fizz.set(42, 42);
// Overriding one of the existing key-value pairslet replaced1 = fizz.replace(7, null); // the entry under key 7 is now deletedreplaced1; // true
// Trying to replace the value in a non-existing key-value pair will do nothinglet replaced2 = fizz.replace(8, null); // no key 8, so nothing was alteredreplaced2; // false
替换并获取旧值,.replaceGet()
Available since Tact 1.6 (not released yet)
类似于 .replace()
,但在成功替换时返回旧的(替换前的)值,否则返回 null
,而不是返回一个 Boolean
。
// Empty maplet fizz: map<Int, Int> = emptyMap();
// Setting a couple of values under different keysfizz.set(7, 70);fizz.set(42, 42);
// Overriding one of the existing key-value pairslet oldVal1 = fizz.replaceGet(7, 68); // key 7 now points to value 68oldVal1; // 70
// Trying to replace the value in a non-existing key-value pair will do nothinglet oldVal2 = fizz.replaceGet(8, 68); // no key 8, so nothing was alteredoldVal2; // null
如果给定值是null
,并且键存在,则条目将从映射中删除。
// Empty maplet fizz: map<Int, Int> = emptyMap();
// Setting a couple of values under different keysfizz.set(7, 70);fizz.set(42, 42);
// Overriding one of the existing key-value pairslet oldVal1 = fizz.replaceGet(7, null); // the entry under key 7 is now deletedoldVal1; // 70
// Trying to replace the value in a non-existing key-value pair will do nothinglet oldVal2 = fizz.replaceGet(8, null); // no key 8, so nothing was alteredoldVal2; // null
删除条目,.del()
要删除单个键值对(单个条目),请使用 .del()
方法。 如果删除成功,则返回 true
,否则返回 false
。
// Empty maplet fizz: map<Int, Int> = emptyMap();
// Setting a couple of values under different keysfizz.set(7, 123);fizz.set(42, 321);
// Deleting one of the keyslet deletionSuccess: Bool = fizz.del(7); // true, because map contained the entry under key 7fizz.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 maplet fizz: map<Int, Int> = emptyMap();
// Setting a couple of values under different keysfizz.set(7, 123);fizz.set(42, 321);
// Deleting all of the entries at oncefizz = 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); // truefizz == buzz; // true, and uses much less gas to compute
使用 .deepEquals()
非常重要,因为map来自第三方源,它没有提供关于序列化布局 的任何保证。 例如,考虑以下代码:
// First map, with long labelsconst m1 = beginCell() .storeUint(2, 2) // long label .storeUint(8, 4) // key length .storeUint(1, 8) // key .storeBit(true) // value .endCell();
// Second map, with short labelsconst 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 maplet fizz: map<Int, Int> = emptyMap();
// Setting a couple of values under different keysfizz.set(42, 321);fizz.set(7, 123);
// Iterating over in a sequential order: from the smallest keys to the biggest onesforeach (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的大小会大幅增加,就会产生很多问题:
-
由于智能合约状态大小的上限约为 项类型为
Cell
,因此整个合约的映射存储上限约为 键值对。 -
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的一部分。