Skip to main content

集合 (Collections)

儲存一組值的能力是程式中最常見的需求之一。Move Basics 章節介紹的 vector 型別是實現這個需求的基礎建構區塊,而 Sui Framework 在此之上擴充了兩種集合型別,加上額外的結構:VecSet 讓元素保持唯一,而 VecMap 則將鍵與值關聯起來。在本節中,我們將以這三者最常見的角色——作為物件的欄位——進行介紹,並展示各自的操作方式與限制。

Vector 向量 (Vector)

雖然向量章節vector 型別呈現為一個獨立的值,但在實際應用中,它通常存在於物件內部。一個擁有書籍清單的商店,就是把 vector 放在欄位裡:

module book::collections_vector;

use std::string::String;

/// The Book that can be sold by a `BookStore`
public struct Book has key, store {
id: UID,
name: String
}

/// The BookStore that sells `Book`s
public struct BookStore has key, store {
id: UID,
books: vector<Book>
}

向量章節中的一切在這裡都同樣適用;下面介紹的集合型別也遵循相同的模式——它們是可以放進欄位、傳遞的純粹 struct 值,並且不同於本章稍後介紹的動態欄位,它們完全由持有它們的物件的型別所描述。

VecSet 唯一集合 (VecSet)

VecSet 是一種儲存唯一項目的集合。插入一個已存在的值會中止,因此這種集合非常適合用於不能有重複項目的集合,例如 ID 或地址的清單。

module book::collections_vec_set;

use sui::vec_set::{Self, VecSet};

public struct App has drop {
/// `VecSet` used in the struct definition
subscribers: VecSet<address>
}

#[test_only]
use std::unit_test::assert_eq;

#[test]
fun vec_set_playground() {
let mut set = vec_set::empty(); // create an empty set

set.insert(1u8); // add items to the set
set.insert(2);
set.insert(3);

assert_eq!(set.contains(&1), true); // check if an item is in the set
assert_eq!(set.length(), 3); // get the number of items in the set
assert_eq!(set.is_empty(), false); // check if the set is empty

set.remove(&2); // remove an item from the set
assert_eq!(set.contains(&2), false);

// the contents can be taken out as a plain vector, e.g. for iteration
let items = set.into_keys();
assert_eq!(items, vector[1, 3]);
}

contains 函式可以回答成員資格的問題,而集合內容可以透過參考以 keys 讀回,或是透過 into_keys 取出成一個純粹的 vector——例如,可以用向量巨集對其進行走訪。

VecSet 的元素型別必須具備 copydrop 能力。這對原生型別與簡單的資料 struct 來說是成立的,但排除了在集合中儲存資產的可能性。

VecMap 鍵值映射 (VecMap)

VecMap 是一種鍵值對的集合,其中每個鍵都是唯一的,並對應到單一一個值。讀回一個值是映射的日常操作,有兩種方式可以做到:索引語法 map[&key] 會借用一個值,若鍵不存在則中止;而 try_get 則會回傳一個 Option,且永遠不會中止。

module book::collections_vec_map;

use std::string::String;
use sui::vec_map::{Self, VecMap};

public struct Metadata has drop {
name: String,
/// `VecMap` used in the struct definition
attributes: VecMap<String, String>
}

#[test_only]
use std::unit_test::{assert_eq, assert_ref_eq};

#[test]
fun vec_map_playground() {
let mut map: VecMap<u64, String> = vec_map::empty(); // create an empty map

map.insert(2, "two"); // add a key-value pair to the map
map.insert(3, "three");

assert_eq!(map.contains(&2), true); // check if a key is in the map
assert_eq!(map.length(), 2); // get the number of entries

// index syntax borrows a value by key, aborts if the key is missing
assert_ref_eq!(&map[&2], &"two");

// `try_get` copies the value, returns `none` if the key is missing
assert_eq!(map.try_get(&2), option::some("two"));
assert_eq!(map.try_get(&4), option::none());

// an existing value can be replaced through a mutable reference
*(&mut map[&3]) = "III";

// `remove` returns the key-value pair
let (key, value) = map.remove(&2);
assert_eq!(key, 2);
assert_eq!(value, "two");
}

VecSet 一樣,VecMap 在嘗試 insert 一個已存在的鍵時會中止——它不會悄悄地覆寫舊值。要取代一個值,需要透過可變參考(如上面的範例所示),或是先移除舊的項目。VecMap 的鍵必須具備 copy 能力,而值則可以是任何型別。

限制 (Limitations)

以向量為基礎的集合是嚴格型別化的:VecSet<address> 只會存放地址,不會存放其他東西,這在大多數情況下正是你想要的,但也使得它們不適合用於異質資料。它們同時也是儲存在物件內部的純粹值,因此會計入 Building Against Limits 指南中所描述的 256KB 物件大小限制。

在實務上,另一個限制會更早發揮作用:每一個操作——insertcontainsget——都會逐一元素地掃描底層的 vector,因此每次存取的成本會隨著集合大小而增加。當元素數量很小且有限——幾十個或幾百個項目時,以向量為基礎的集合表現得很好。對於大型或無上限的集合,Sui Framework 提供了 TableBag 及其他以物件為基礎的型別,我們將在本章稍後的動態集合章節中介紹。

最後,以向量為基礎的集合並不支援一般預期的相等性比較。VecSetVecMap 會按插入順序保留其內容,而 == 運算子則是逐一元素地比較底層的 vector。因此,兩個包含相同元素、但插入順序不同的集合,兩者並相等。

這種行為會被 linter 抓到並發出警告:Comparing collections of type 'sui::vec_set::VecSet' may yield unexpected result

let mut set1 = vec_set::empty();
set1.insert(1u8);
set1.insert(2);

let mut set2 = vec_set::empty();
set2.insert(2);
set2.insert(1);

assert_eq!(set1, set2); // aborts!

在上面的範例中,兩個集合都包含相同的元素——12——但它們的插入順序不同。由於比較是對順序敏感的,set1 == set2 的結果會是 false,斷言因此會中止。除非你能保證元素是以相同的順序插入,否則不要依賴 == 來比較以向量為基礎的集合。

總結 (Summary)

  • Vector 是一種原生型別,可以儲存一串項目清單;在物件內部,它會以一般欄位的形式出現。
  • VecSet 是建構在 vector 之上,儲存唯一的項目;插入重複項目會中止。
  • VecMap 儲存具有唯一鍵的鍵值對;插入已存在的鍵會中止,而值可以透過索引語法或 try_get 讀取。
  • 以向量為基礎的集合是嚴格型別化的,每次操作都會線性掃描其內容,最適合用於小型、有限的集合與清單;更大的集合則需要使用動態集合

下一步 (Next Steps)

在下一節中,我們將介紹包裝型別模式——一種常與集合型別搭配使用、用來擴充或限制其行為的設計模式。

延伸閱讀 (Further Reading)