FEATURED · 精选文章

Sway 常见集合(Common Collections)完整实战指南:堆上 Vec、持久化 StorageVec 与 StorageMap 源码级解析

发布时间 / 2026/9/11 20:23:51
来源 / 创域科博编辑部
栏目 / 资讯中心
Sway 常见集合(Common Collections)完整实战指南:堆上 Vec、持久化 StorageVec 与 StorageMap 源码级解析 Sway 常见集合Common Collections完整实战指南堆上 Vec、持久化 StorageVec 与 StorageMap 源码级解析【免费下载链接】sway Empowering everyone to build reliable and efficient smart contracts.项目地址: https://gitcode.com/GitHub_Trending/sw/sway本文是 Sway 语言官方书籍《Common Collections》一章的中文深度指南核心围绕 Sway 标准库中最常用的三类集合展开堆上动态数组VecT、持久化存储向量StorageVecT与键值映射StorageMapK, V。你将从本文掌握如何在脚本、谓词与合约中创建、更新、读取和迭代这些集合理解它们的内存模型堆 vs 合约存储差异并结合 sway-lib-std 的源码实现弄清push/get/insert等核心方法背后的容量增长、存储槽位与泛型机制最终能够为购物车价格列表钱包余额账本等真实合约场景选择恰当的集合。一、集合总览为什么需要它们大多数 Sway 数据类型只代表一个具体的值而集合collections可以容纳多个值。与内置的数组array和元组tuple不同——它们被分配在栈上且大小在编译期固定、无法增长——集合所指向的数据存储在堆heap或合约存储storage中这意味着数据量无需在编译期确定可以在程序运行期间增长。每种集合具有不同的能力与成本在 Sway 程序中最常用的三类集合如下集合类型数据存放位置核心用途能否在脚本/谓词中使用VecT堆上向量堆heap在内存中相邻存放可变数量的同类型值✅ 可以不依赖合约存储StorageVecT存储向量合约持久化存储storage带索引的持久化同类型值列表❌ 仅限合约只有合约能访问持久化存储StorageMapK, V存储映射合约持久化存储storage用任意类型键K关联值V❌ 仅限合约下面依次深入这三类集合的创建、更新、读取与迭代并穿插源码实现细节。二、VecT堆上的动态数组VecT允许在单个数据结构中存放多个值且这些值在内存中相邻排列。它只能存储同类型的值非常适合文件中的文本行购物车中商品的价格这类列表场景。VecT已包含在标准库 prelude 中无需手动导入。从源码可以确认这一点sway-lib-std/src/prelude.sw 中pub use ::vec::{Vec, VecIter};将其直接暴露给所有程序。2.1 创建新的向量调用Vec::new创建空向量let v: Vecu64 Vec::new();这里必须添加类型注解因为没有插入任何值时编译器无法推断元素类型。VecT基于泛型实现标准库提供的VecT可以容纳任意类型在尖括号中指定类型即可告诉编译器v将存放u64元素。从源码看Vec::new只是构造了一个零容量的向量sway-lib-std/src/vec.sw 中VecT由buf: RawVecT内部堆缓冲与len: u64当前长度组成RawVec::new调用alloc::T(0)分配 0 容量——向量在 push 元素之前不会真正分配堆内存。如果你提前知道要存放的元素数量可以使用Vec::with_capacity(capacity)预分配容量避免后续反复扩容。2.2 更新向量push追加元素let mut v Vec::new(); v.push(5); v.push(6); v.push(7); v.push(8);与任何变量一样想要修改向量的值必须使用mut关键字参见声明变量。这里的数字都是u64编译器从数据中自行推断类型因此无需Vecu64注解。源码级原理push的实现sway-lib-std/src/vec.sw在写入前会检查self.len self.buf.cap若容量不足则调用RawVec::grow。而growvec.sw采用翻倍策略new_cap if self.cap 0 { 1 } else { 2 * self.cap }然后通过realloc在堆上分配新缓冲并把旧数据拷贝过去。这正是向量无需编译期确定大小、可随程序运行增长的实现基础。2.3 读取元素get与OptionT通过索引读取元素使用get方法let third v.get(2); match third { Some(third) log(third), None revert(42), }注意两个细节索引从0开始索引值2取到的是第三个元素get方法传入索引参数返回的是OptionT。当get传入的索引越界时它返回None而不会 panic/revertlet does_not_exist v.get(100); // ...decide here how to handle an out-of-bounds access这在越界偶尔会在正常逻辑下发生的场景特别有用例如索引来自合约方法参数参数过大时get返回None合约方法可以据此选择 revert或返回一个包含当前向量长度的有意义错误给用户重新传入合法值的机会。get的实现vec.sw先做if self.len index { return None; }的边界检查再通过指针偏移读取是安全的越界防护路径。2.4 迭代向量中的值使用while循环配合len方法按合法索引遍历let mut i 0; while i v.len() { log(v.get(i).unwrap()); i 1; }这里有两个细节len返回向量的长度对get返回的Option调用unwrap取出元素。由于每个i都已知小于向量长度unwrap不会失败不会引发 revert。更符合惯例、更便捷的方式是for循环搭配iter方法——iter返回一个按顺序遍历所有元素的迭代器for elem in v.iter() { log(elem); }⚠️ 重要警告在迭代过程中修改向量例如添加或删除元素属于逻辑错误会导致未定义行为undefined behaviorfor elem in v.iter() { log(elem); if elem 3 { v.push(6); // Modification causes undefined behavior! } }为什么这是未定义行为看VecIter的实现vec.sw即可理解由于 Sway 的复制语义iter()返回的迭代器内部保存的是向量的一个副本next中检查的长度是创建迭代器那一刻的长度。如果在迭代中修改原向量修改不会反映到self.values.len上遍历结果将与直觉不符。因此对迭代期修改的唯一正确态度就是——不要这样做。while循环仅在需要对遍历施加更多控制时使用。例如下面的例子从尾部开始、只访问每隔一个的元素// Start from the end let mut i v.len() - 1; while 0 i { log(v.get(i).unwrap()); // Access every second element i - 2; }2.5 用枚举存储多种类型向量只能存同类型值这有时很不方便——例如需要存一组不同类型元素的列表。好在枚举的所有变体都定义在同一枚举类型下因此可以用一个枚举来代表不同类型的元素。例如我们需要从表格的一行中取值该行的列有的存整数、有的存b256、有的存布尔值enum TableCell { Int: u64, B256: b256, Boolean: bool, } let mut row Vec::new(); row.push(TableCell::Int(3)); row.push(TableCell::B256(0x0101010101010101010101010101010101010101010101010101010101010101)); row.push(TableCell::Boolean(true));这样row的类型是VecTableCell从类型系统的角度看所有元素同属TableCell但实际承载了三种不同的数据类型。2.6 更多VecT方法除push外标准库为VecT提供了丰富的常用方法完整列表见 sway-lib-std/src/vec.sw。几个典型方法的行为与边界条件如下方法行为越界/空时的行为pop移除并返回最后一个元素空向量返回Noneremove(index)移除并返回指定索引处的元素后续元素左移索引越界会assertrevertvec.swinsert(index, element)在指定索引处插入元素后续元素右移索引大于长度会 revertvec.swset(index, value)覆盖指定索引处的元素索引越界 revertlen/is_empty返回长度 / 是否为空—clear清空所有元素不释放已分配容量—swap(a, b)交换两个索引处的元素任一索引越界 revertlast返回最后一个元素空向量返回Noneresize(new_len, value)扩容时用value填充、缩小时截断—另外VecT还实现了AbiEncode/AbiDecodevec.sw因此可以直接作为 ABI 方法参数/返回值在合约间传递。三、StorageVecT持久化存储向量第二个集合是StorageVecT。与堆上的VecT一样StorageVecT允许多个同类型值存放在单一数据结构中每个值分配一个索引但与VecT不同的是StorageVec的元素存放在持久化存储persistent storage中且连续元素并不一定存放在键连续的存储槽位storage slots里。使用StorageVecT前必须手动导入use std::storage::storage_vec::*;另一个重要区别是StorageVecT只能在合约中使用因为只有合约才有权访问持久化存储。3.1 创建新的StorageVec必须在storage块中声明向量storage { v: StorageVecu64 StorageVec {}, }与任何存储变量一样声明StorageVec需要两样东西类型注解和初始化器。初始化器只是一个空的StorageVec结构体——因为StorageVecT本身就是一个空结构体它的一切有趣行为都实现在方法中。从源码可以印证sway-lib-std/src/storage/storage_vec.sw 中pub struct StorageVecV {}是一个零大小的存储类型zero-sized storage type可以嵌套在其他存储类型内部如StorageVecStorageVecu64、StorageVecStorageMapu64, b256。StorageVecT同样基于泛型实现尖括号中指定具体类型即可。与VecT一样StorageVec也是通过storage.v这样的语法访问的。3.2 更新push与 storage 注解#[storage(read, write)] fn push_to_storage_vec() { storage.v.push(5); storage.v.push(6); storage.v.push(7); storage.v.push(8); }两个细节使用push前需要先用storage关键字访问向量push需要访问存储因此调用push的ABI 函数必须带storage注解。虽然表面上#[storage(write)]似乎就够了但read注解同样必需——因为每次push都要读取然后更新StorageVec的长度而这个长度本身也存放在持久化存储中。注意合约中任何尝试向向量 push 的私有函数同样需要 storage 注解。注意声明StorageVecT时无需加mut关键字——所有存储变量默认都是可变的。源码级原理在非动态存储模式下experimental_dynamic_storage falseStorageVec的方法实现storage_vec.sw始终使用self.field_id作为存储槽位field_id位置保存向量的长度而实际内容存储在sha256(self.field_id)派生的槽位上。push方法在 storage_vec.sw 中被标注为3 次存储读取、2 次存储写入——这就是每次 push 都要读长度在 gas 成本层面的直接体现。3.3 读取元素get与OptionStorageKeyT#[storage(read)] fn read_from_storage_vec() { let third storage.v.get(2); match third { Some(third) log(third.read()), None revert(42), } }注意三个细节索引从0开始索引值2是第三个元素get返回的是OptionStorageKeyT需要再调用一次.read()才能真正读出存储中的值调用get的 ABI 函数只需#[storage(read)]注解——get不写存储正如预期。与Vec::get一致当索引越界时StorageVec::get返回None而不 panic便于合约方法处理索引来自外部参数、偶尔越界的正常情况revert 或返回带长度的错误信息。3.4 迭代存储向量迭代StorageVec与迭代VecT概念上一致唯一区别是需要额外的read()调用才能真正读出存储的值#[storage(read)] fn iterate_over_a_storage_vec() { // 用 while 循环逐个遍历不推荐的方式 let mut i 0; while i storage.v.len() { log(storage.v.get(i).unwrap().read()); i 1; } // 首选且最高效的方式for 循环 for elem in storage.v.iter() { log(elem.read()); } // 仅在需要更多遍历控制时使用 while // 例如从尾部开始、只访问每隔一个的元素 let mut i storage.v.len() - 1; while 0 i { log(storage.v.get(i).unwrap().read()); i - 2; } }⚠️ 重要警告迭代过程中修改存储向量增删元素同样是逻辑错误会导致未定义行为。3.5 用枚举存储多种类型StorageVec与Vec一样只能存同类型值。定义枚举后再声明StorageVec来间接存多种类型enum TableCell { Int: u64, B256: b256, Boolean: bool, } storage { row: StorageVecTableCell StorageVec {}, }接着可以向该StorageVec压入不同的枚举变体#[storage(read, write)] fn push_to_multiple_types_storage_vec() { storage.row.push(TableCell::Int(3)); storage .row .push(TableCell::B256(0x0101010101010101010101010101010101010101010101010101010101010101)); storage.row.push(TableCell::Boolean(true)); }3.6 嵌套存储向量StorageVec支持嵌套storage { nested_vec: StorageVecStorageVecu64 StorageVec {}, }访问嵌套向量#[storage(read, write)] fn access_nested_vec() { storage.nested_vec.push(StorageVec {}); storage.nested_vec.push(StorageVec {}); let mut inner_vec0 storage.nested_vec.get(0).unwrap(); let mut inner_vec1 storage.nested_vec.get(1).unwrap(); inner_vec0.push(0); inner_vec0.push(1); inner_vec1.push(2); inner_vec1.push(3); inner_vec1.push(4); assert(inner_vec0.len() 2); assert(inner_vec0.get(0).unwrap().read() 0); assert(inner_vec0.get(1).unwrap().read() 1); assert(inner_vec0.get(2).is_none()); assert(inner_vec1.len() 3); assert(inner_vec1.get(0).unwrap().read() 2); assert(inner_vec1.get(1).unwrap().read() 3); assert(inner_vec1.get(2).unwrap().read() 4); assert(inner_vec1.get(3).is_none()); }源码提示StorageVec的嵌套能力并非无限制。根据 storage_vec.sw 的文档注释部分方法对嵌套存储类型如StorageVecStorageString不适用例如remove会 revertpop虽能弹出最后一个元素但总是返回None且不会从存储中移除。另外在启用动态存储experimental_dynamic_storage true时元素类型V的大小必须小于等于 1024 字节否则属于未定义行为、大概率运行时 revert。四、StorageMapK, V存储映射StorageMapK, V是第三种重要集合。标准库中的StorageMapK, V通过哈希函数决定如何把键K和值V放入存储槽位这类似于 Rust 的HashMapK, V但存在若干差异最核心的差异是它持久化于合约存储而非内存。存储映射适用于不按索引、而按任意类型键查找数据的场景。例如实现一个基于账本的子货币智能合约时可以用存储映射记录每个钱包的余额——键是钱包的Address值是余额给定一个Address即可取出其余额。与StorageVecT一样StorageMapK, V只能在合约中使用。它包含在标准库 prelude 中prelude.sw 中pub use ::storage::storage_map::*;无需手动导入。4.1 创建新的存储映射在storage块中声明storage { map: StorageMapAddress, u64 StorageMap::Address, u64 {}, }与任何存储变量一样需要类型注解 初始化器初始化器是空的StorageMap结构体因为StorageMapK, V本身就是空结构体一切行为都在方法中。存储映射基于泛型实现键K和值V都可以是任意类型。上面的声明告诉编译器map将把Address键映射到u64值。4.2 更新insert与#[storage(write)]向映射插入键值对使用insert方法#[storage(write)] fn insert_into_storage_map() { let addr1 Address::from(0x0101010101010101010101010101010101010101010101010101010101010101); let addr2 Address::from(0x0202020202020202020202020202020202020202020202020202020202020202); storage.map.insert(addr1, 42); storage.map.insert(addr2, 77); }两个细节使用insert前需要先用storage关键字访问映射insert需要写入存储调用它的 ABI 函数必须带#[storage(write)]注解。注意合约中任何尝试向映射 insert 的私有函数同样需要 storage 注解。注意声明StorageMapK, V时无需加mut关键字——所有存储变量默认都是可变的。4.3 读取值get、try_read与Option通过键取出值使用get方法#[storage(read, write)] fn get_from_storage_map() { let addr1 Address::from(0x0101010101010101010101010101010101010101010101010101010101010101); let addr2 Address::from(0x0202020202020202020202020202020202020202020202020202020202020202); storage.map.insert(addr1, 42); storage.map.insert(addr2, 77); let value1 storage.map.get(addr1).try_read().unwrap_or(0); }这里value1将是与第一个地址关联的值42。get返回的是一个指向存储槽位的键封装实际可解出OptionV语义若映射中没有该键的值get链上的读取将返回None。上面的程序通过unwrap_or处理Option——如果map中没有该键的条目就把value1置为0。这也是余额账本场景的典型写法查不到余额时默认返回 0。4.4 多键映射元组作为键使用元组作键即可实现多键映射storage { map_two_keys: StorageMap(b256, bool), b256 StorageMap::(b256, bool), b256 {}, }4.5 嵌套存储映射存储映射支持嵌套storage { nested_map: StorageMapu64, StorageMapu64, u64 StorageMap::u64, StorageMapu64, u64 {}, }访问嵌套映射#[storage(read, write)] fn access_nested_map() { storage.nested_map.get(0).insert(1, 42); storage.nested_map.get(2).insert(3, 24); assert(storage.nested_map.get(0).get(1).read() 42); assert(storage.nested_map.get(0).get(0).try_read().is_none()); // Nothing inserted here assert(storage.nested_map.get(2).get(3).read() 24); assert(storage.nested_map.get(2).get(2).try_read().is_none()); // Nothing inserted here }注意嵌套访问的链式写法storage.nested_map.get(0)先定位到外层键0对应的内层映射再对其调用insert/gettry_read()则用于不确定该键是否存在的安全读取assert(...is_none())验证未插入的键确实返回空。五、场景选型与工程实践综合三节内容选择集合的核心判据是数据是否需要在交易之间持续存在纯内存、过程内计算脚本、谓词、合约内的临时计算用VecT。它零初始化成本push触发翻倍扩容适合排序、遍历、临时聚合等场景还能通过AbiEncode/AbiDecode直接跨 ABI 传递。跨交易的持久化列表如合约维护的待处理项、历史记录用StorageVecT。每个push都要读写存储约 3 读 2 写成本高于堆向量但数据在交易间永续存在。按任意键查找的账本型数据如地址 → 余额、资产 ID → 供应量用StorageMapK, V。哈希定位槽位、天然支持Address/b256/元组键配合try_read().unwrap_or(default)可优雅处理缺失键。本仓库中还有大量与集合配套的可运行示例建议动手实验堆向量完整示例见 examples/vec/src/main.sw存储向量示例见 examples/storage_vec/src/main.sw存储映射示例见 examples/storage_map/src/main.sw对应的Forc.toml都位于各自示例目录下可通过forc build直接构建验证。在编写实际合约时请同时注意 storage 注解的读写标注push/insert需read, write或write纯读取需read并严格遵守迭代期间不修改集合的约束以避免未定义行为。【免费下载链接】sway Empowering everyone to build reliable and efficient smart contracts.项目地址: https://gitcode.com/GitHub_Trending/sw/sway创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻