news 2026/9/12 8:29:27

Sway StorageMap 存储映射实战:键值对持久化存储的声明、读写与底层原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Sway StorageMap 存储映射实战:键值对持久化存储的声明、读写与底层原理

Sway StorageMap 存储映射实战:键值对持久化存储的声明、读写与底层原理

【免费下载链接】sway🌴 Empowering everyone to build reliable and efficient smart contracts.项目地址: https://gitcode.com/GitHub_Trending/sw/sway

导读

StorageMap是 Sway 标准库提供的一种持久化键值对(key-value)存储结构,常被形象地称为哈希表(hash table)。在 Sway 智能合约开发中,它是实现「地址 → 余额」「用户 → 权限」这类映射关系的核心工具:数据被永久写入链上存储(storage),并可通过键在常数时间O(1)内完成定位。本文将基于本仓库 Sway 参考文档(storage-map.md)及其配套示例,系统讲解StorageMap的声明、读取、写入三大操作,并结合标准库源码剖析其底层存储槽计算、Option 语义与完整方法集,帮助你写出可复用的映射式合约存储逻辑。

StorageMap 是什么:以键定位值的持久化哈希表

StorageMap是一种将值v与键k关联起来的数据结构。键k被用来在存储(storage)中定位值v所在的位置。与传统内存哈希表不同,Sway 的StorageMap的数据是持久化的——它直接写入链上存储槽,而不是堆(heap)内存。

哈希表最核心的收益在于查找效率:无论值位于表的哪个位置,定位该值所需的计算量都是常数级别的,即时间复杂度为O(1)。在链上合约场景中,这意味着读取某个用户余额或查询某个标识对应的状态,不会因为数据量的增长而线性变慢。

StorageMap的一个显著特点是高度的灵活性——它使用泛型(generics)同时约束键k和值v的类型。Sway 语言层面的泛型机制可参考 generics 文档。唯一的前提约束是:kv都必须是单个值(single value)。这个「单个值」并不等同于单一基础类型——值v可以是结构体、元组、数组等复合类型。因此,如果你需要用一个复杂结构作为键或值,只需要把数据包装(wrap)进一个单一类型即可,StorageMap仍然能够正常工作。

从源码结构看,标准库中StorageMap定义为pub struct StorageMap<K, V> {}(storage_map.sw),是一个零尺寸(zero-sized)存储类型,可以嵌套在其他存储类型内部使用,例如StorageMap<K, StorageMap>这样的嵌套映射。

声明 StorageMap:泛型键值对与 prelude 导入

无需手动导入

StorageMap类型被包含在标准库的 prelude 中,因此声明时不需要额外use导入。如果在后续代码中需要使用msg_sender()来获取调用者身份,则需要显式导入相关路径(示例中通过use std::hash::*;引入哈希与发送者相关能力,见 main.sw)。

初始化写法

StorageMapstorage块中按初始化指南的方式声明:每个变量被命名、关联类型并给出默认值。示例代码如下(完整文件见 storage_map/src/main.sw):

storage { // k = Identity, v = u64 balance: StorageMap<Identity, u64> = StorageMap::<Identity, u64> {}, // k = (Identity, u64), v = bool user: StorageMap<(Identity, u64), bool> = StorageMap::<(Identity, u64), bool> {}, }

示例中声明了两个 storage 变量:

  • balance:以单个值作为键,映射类型为StorageMap<Identity, u64>,即用Identity(调用者身份)映射到u64(余额)。
  • user:将两个值包装成一个元组(Identity, u64)作为键,映射类型为StorageMap<(Identity, u64), bool>,即用「身份 + 用户编号」的组合唯一标识一个用户及其状态。

第二行正体现了上文提到的「复杂键需包装为单一类型」的设计:把两个字段打包进一个元组,就能组合成复合键。声明语法要求显式写出初始化表达式StorageMap::<K, V> {},这是空映射的字面量构造方式。

从存储中读取:.get(key)与 Option 语义

基本用法

从存储中检索数据,通过.get(key)方法完成:指定要读取的 storage 变量,在末尾追加.get(),并在括号内传入想要检索的数据的键。示例代码(main.sw):

#[storage(read)] fn reading_from_storage(id: u64) { let user = storage.user.get((msg_sender().unwrap(), id)).read(); }

在这个示例中,合约把调用者的Identity(通过msg_sender().unwrap()取得)与调用者提供的id包装成一个元组,作为复合键传入.get(),从而读取storage.user中该键对应的值。

返回Option,配合unwrap_or兜底

.get(key)返回一个Option:如果映射中不存在key对应的值,get会返回None。因此读取后必须处理可能为空的情况。参考文档明确指出,示例合约通过调用unwrap_or来处理返回的Option——当映射user中没有该键的条目时,将用户状态置为零值兜底。

需要特别说明的是:.get()返回的是StorageKey<V>,对其调用.read()才能得到最终的Option<V>;在需要默认值时可在read()结果上继续调用unwrap_or(default),这是文档所描述的「将user设为零」的完整链路。完整的读取-兜底模式可写为:

let user = storage.user.get((msg_sender().unwrap(), id)).read().unwrap_or(false);

存储纯度标注

读取函数必须标注#[storage(read)]属性,表明该函数仅读取存储、不修改存储。这与读写指南中「处理存储时必须使用 storage 注解指示函数纯度」的要求一致。Sway 编译器会依据注解执行纯度检查,防止在只读上下文中意外写入存储。

写入存储:.insert(key, value)与读改写模式

基本用法

写入存储与读取类似,区别在于使用的方法变成了.insert(key, value)。示例代码(main.sw):

#[storage(read, write)] fn writing_to_storage() { let balance = storage.balance.get(msg_sender().unwrap()).read(); storage.balance.insert(msg_sender().unwrap(), balance + 1); }

这个示例实现了一个典型的「读-改-写」(read-modify-write)流程:

  1. 先通过.get(msg_sender().unwrap()).read()读取当前调用者的余额;
  2. 将其加 1;
  3. 再通过.insert(msg_sender().unwrap(), balance + 1)把新余额写回storage.balance

因为insert会修改存储,所以函数需要标注#[storage(read, write)],同时声明了读取与写入两种纯度。

关于默认值的一个细节

示例代码第 23 行直接对.read()的结果(Option<u64>)执行+ 1,从源码可见该示例默认调用者的余额已存在(即此前已insert过)。在实际生产代码中,若映射可能尚未初始化,建议先通过unwrap_or(0)提供默认值,再执行算术运算,以避免对None的处理缺失:

#[storage(read, write)] fn safe_increment() { let current = storage.balance.get(msg_sender().unwrap()).read().unwrap_or(0); storage.balance.insert(msg_sender().unwrap(), current + 1); }

标准库源码剖析:存储槽如何由键计算而来

StorageMap的实现位于标准库 storage_map.sw,它建立在更底层的StorageKeystorage_api抽象之上(模块导出见 storage.sw)。理解其内部机制有助于写出更高效的存储代码。

键到存储槽的哈希推导

StorageMap中键k到存储槽(slot)的映射不是线性地址,而是通过哈希函数推导而来。核心逻辑在get_slot_key中(storage_map.sw):

fn get_slot_key(self, key: K) -> b256 { sha256((STORAGE_MAP_DOMAIN, key, self.field_id())) }
  • 使用sha256对「域前缀(STORAGE_MAP_DOMAIN)+ 键 + 字段标识(field_id)」的元组求哈希,得到b256类型的存储槽地址;
  • STORAGE_MAP_DOMAIN取值为1u8(storage_map.sw)。源码注释说明:为了防止用户输入(键)的哈希原像与编译器为 storage 字段生成的原像相撞,标准库为存储映射域添加一个单字节前缀,从而在密钥空间中隔离不同的存储域;
  • 由于同一个field_id的哈希输入固定,同一键在链上总是映射到同一存储槽,因此StorageMap天然具备确定性——这正是合约状态一致性的基础。

get返回StorageKey<V>

get(key)的返回类型是StorageKey<V>(storage_map.sw),它描述「无论该位置是否真的存有值,键对应的值在存储中的位置」。真正读取值需要通过StorageKey上的.read()方法完成(storage_key.sw):

pub fn read(self) -> T { read_quads::<T>(self.slot, self.offset).unwrap() }

read()内部通过read_quads读取对应存储槽,并返回Option<T>解包后的值——若槽位为空则触发回退(revert)。这就是文档中「get 返回Option」的底层来源:Option语义由底层存储读取在「槽未写入」时产生None的机制承载。而try_read()(storage_key.sw)则提供不触发回退、直接返回Option<T>的读取方式,适合需要安全兜底的场景。

写入与清除:insert/remove/try_insert

标准库为StorageMap提供了比文档示例更完整的操作集(默认experimental_dynamic_storage = false分支,storage_map.sw):

  • insert(key, value):通过write_quads::<V>(key, 0, value)将值写入由get_slot_key推导出的存储槽(storage_map.sw)。注解#[storage(read, write)]表明写入可能需要先读取部分数据以覆盖;其文档注明:当值占满整个槽时读次数为 0,否则为 1(用于读取将被部分覆盖的旧数据),写次数恒为 1。
  • remove(key):返回booltrue表示该键此前存有值,通过clear_quads清除(storage_map.sw)。
  • try_insert(key, value):仅当键尚无值时插入,返回Result<V, StorageMapError<V>>;若键已存在,返回StorageMapError::OccupiedError(旧值)不覆盖原值(storage_map.sw)。这是实现「首次创建、禁止覆盖」类业务(如一次性注册)的利器。

experimental_dynamic_storage = true时,标准库提供另一套基于write_slot/read_slot的实现(storage_map.sw),方法名略有差异(如remove_existed),语义面向动态存储实验特性。

完整方法速查

方法签名要点作用存储访问
get(key)返回StorageKey<V>定位键对应值的存储位置,配合.read()/.try_read()取值0 次(仅计算位置)
insert(key, value)无返回值写入/覆盖键值对写 1;若值未占满槽则读 1
remove(key)返回bool清除键对应值,返回是否存在旧值清除 1
try_insert(key, value)返回Result<V, StorageMapError<V>>键不存在时才写入,否则返回旧值读 1;写入时再写 1

实践要点与组合建议

  1. 复合键的包装StorageMap的键、值都必须是单一类型。需要多字段键时,用元组(如(Identity, u64))、结构体或数组包裹;同样,结构体、元组、数组等复合类型都可以直接作为值v存储。
  2. Option 必处理get(key)在键不存在时返回None。读取后使用unwrap_or(default)提供默认值,或使用match显式分支处理,避免直接解包导致合约回退。
  3. 纯度注解不可缺:只读函数标注#[storage(read)],读改写函数标注#[storage(read, write)],写入专用函数可标注#[storage(write)]。注解与storage.变量名语法是 Sway 存储安全的两道闸门。
  4. 优先try_insert实现幂等写入:对于「账户只能注册一次」等语义,用try_insert天然返回冲突旧值,避免「先 get 再 insert」的竞态窗口。
  5. 与其他存储工具搭配StorageMap属于标准库存储工具家族,其余还包括持久化向量StorageVec(storage-vec.md)以及直接操作存储槽的store()/get()(store-get.md),总览见 libraries/index.md。映射适合「键 → 值」关系,向量适合按索引存取,裸槽操作则适合底层精细控制,应按业务形态选择。
  6. 配套示例可运行:本文全部代码取自仓库示例 storage_map/src/main.sw,其Forc.toml位于 storage_map/Forc.toml,可直接在 Sway 合约工程中编译验证。

小结

StorageMap是 Sway 合约中最常用的持久化数据结构之一:它以O(1)的常数时间完成键值定位,通过泛型同时支持基础类型与复合类型(结构体、元组、数组)作为键或值,仅需将复杂数据包装为单一类型。声明上,StorageMap位于 prelude 无需导入,在storage块中按StorageMap::<K, V> {}初始化;读取使用.get(key)(返回Option,可用unwrap_or兜底),写入使用.insert(key, value),并配合#[storage(read)]/#[storage(read, write)]纯度注解。底层实现上,每个键通过sha256((STORAGE_MAP_DOMAIN, key, field_id))推导出确定性的存储槽地址,标准库还提供了removetry_insert等更丰富的原子操作。掌握这套声明、读写与原理,你就能在合约中稳健地管理一切「以键查值」的持久化状态。

【免费下载链接】sway🌴 Empowering everyone to build reliable and efficient smart contracts.项目地址: https://gitcode.com/GitHub_Trending/sw/sway

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 8:29:20

预测回滚在多物理碰撞场景下的失效排查与平滑修复

预测回滚在多物理碰撞场景下的失效排查与平滑修复在网络联机游戏的客户端预测与回滚&#xff08;Prediction & Rollback&#xff09;架构中&#xff0c;单角色的自由位移预测相对容易保持一致。然而&#xff0c;一旦场景中引入多角色近身挤压、推搡刚体箱子、物理载具冲撞或…

作者头像 李华
网站建设 2026/9/12 8:28:57

20 分钟搭好自己的游戏串流主机:Sunshine 完全上手指南

20 分钟搭好自己的游戏串流主机&#xff1a;Sunshine 完全上手指南 【免费下载链接】Sunshine Self-hosted game stream host for Moonlight. 项目地址: https://gitcode.com/GitHub_Trending/su/Sunshine 手机上的 Moonlight 打开后设备列表空空如也&#xff0c;装好的…

作者头像 李华
网站建设 2026/9/12 8:27:11

TongSearch中文分词插件analysis-ik详解与优化实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 8:24:39

YOLO版本选型与DeepSeek/千问融合:电子元件质检实战复盘

YOLO版本选型、大模型接入、电子元件质检这几个词放在一起&#xff0c;乍一看像是蹭热度的标题党&#xff0c;但真把这个系统从零搭完&#xff0c;我才发现这里面每一步都是实打实的坑。半年前朋友厂里SMT贴片线想上自动外观识别&#xff0c;老师傅肉眼盯AOI图盯到眼压高&#…

作者头像 李华
网站建设 2026/9/12 8:24:05

布斯乘法器原理与Radix-4硬件实现详解

1. 为什么布斯乘法器不是“高级技巧”&#xff0c;而是数字电路设计者的必修基本功很多人第一次听说布斯乘法器&#xff08;Booth Multiplier&#xff09;&#xff0c;下意识觉得这是“教材里一笔带过、考试不考、实际不用”的冷门知识。我刚带实习生时也这么认为——直到某次调…

作者头像 李华