Advanced Storage in Sway:嵌套存储、存储命名空间与手动存储管理
2026/9/12 9:48:49 网站建设 项目流程

Advanced Storage in Sway:嵌套存储、存储命名空间与手动存储管理

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

本指南基于 Sway 官方文档的《Advanced Storage》章节,深入讲解StorageKey机制下的嵌套存储集合(StorageMap/StorageVec/StorageString/StorageBytes相互嵌套)、如何通过 storage namespace 注解为存储槽计算加入盐值以避免冲突,以及如何绕过storage块直接调用底层read/write完成手动存储管理。读完本文,你将能够在 Fuel 智能合约中自由组合各类存储集合、安全地处理跨合约加载时的存储槽冲突,并掌握对未受storage块支持的数据类型(如数组)进行手工持久化的完整方案。

适用前提:本文示例均为 contract 类型程序,因为只有合约被允许访问持久化存储(StorageMap<K, V>等集合也仅能用于合约中,这与 Storage Maps 文档中的说明一致)。所有代码可在 examples/nested_storage_variables/src/main.sw、examples/storage_namespace/src/main.sw、examples/storage_example/src/main.sw 中查看完整实现。

一、预备知识:StorageKey 与存储槽模型

在进入嵌套存储之前,需要先理解 Sway 存储的底层模型。合约的持久化存储由 32 字节(一个 word)大小的存储槽(storage slot)组成,每个存储变量通过编译期计算得到的b256槽键(slot key)定位。标准库用StorageKey<T>封装了这一寻址信息,从源码看其结构包含三个字段(storage_key.sw):

  • slot:32 字节存储槽的键(b256);
  • offset:从slot起始的偏移量,以 word 为单位;
  • field_id:用于区分可能位于同一存储位置的多个零尺寸(zero-sized)存储条目的标识符。

StorageMap<K, V>StorageVec<T>StorageBytesStorageString本质上都是零尺寸的"存储类型"(storage types),它们本身不占用槽位,全部行为实现在各自的方法中——这一点与普通Vec<T>不同,也与 Storage Maps 文档中"StorageMap<K, V>本身是空结构体"的描述相互印证。

每个存储类型的槽位计算都经过精心设计以避免碰撞。以StorageMap为例,其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)+ 用户键 + 字段 ID」做哈希。其中STORAGE_MAP_DOMAIN是一个单字节前缀,源码注释明确指出:这是为了确保映射中元素的槽位 pre-image 永远不会与编译器为存储字段生成的 pre-image 相同(storage_map.sw)。StorageKey::get(key)返回的正是以该哈希为槽键的StorageKey<V>(storage_map.sw),这意味着嵌套时只需把"父类型算出的StorageKey"继续当作"子类型的槽位上下文"即可——这就是嵌套存储能够成立的根本原因。

StorageVec<T>push/pop/get则通过read_quads::<u64>(self.field_id(), 0)读取/更新长度字段来维护动态数组语义(storage_vec.sw),get(index)返回Option<StorageKey<V>>,当index >= len时返回None(storage_vec.sw)。

二、Nested Storage Collections:嵌套存储集合

通过StorageKey,你可以在一个存储集合中存放另一个存储集合,例如把StorageString存进StorageMap<K, V>,把StorageVec<T>存进StorageMap<K, V>,或者把StorageBytes存进StorageVec<T>

2.1 嵌套存储声明与导入

下面的storage块声明了三种常见的嵌套存储类型(见 nested_storage_variables/src/main.sw):

storage { nested_map_vec: StorageMap<u64, StorageVec<u8>> = StorageMap {}, nested_map_string: StorageMap<u64, StorageString> = StorageMap {}, nested_vec_bytes: StorageVec<StorageBytes> = StorageVec {}, }

使用前必须进行存储初始化:为每个存储集合显式赋予空实例(StorageMap {}StorageVec {})。

NOTE:导入存储类型时,请务必使用 glob 操作符,例如use std::storage::storage_vec::*。嵌套示例中对应的完整导入为(nested_storage_variables/src/main.sw):

use std::{ bytes::Bytes, hash::{Hash, sha256}, storage::{ storage_bytes::*, storage_string::*, storage_vec::*, }, string::String, };

其中Hashtrait 需要显式导入——虽然StorageMap<K, V>已在标准库 prelude 中,但其get/insert方法要求键类型实现Hash(见 storage_map.sw 的where K: Hash约束,以及 blockchain-development/storage.md 中的相关警告)。

2.2 在StorageMap<K, V>中存放StorageVec<T>

写入(见 nested_storage_variables/src/main.sw):

#[storage(write)] fn store_map_vec() { // Setup and initialize storage for the StorageVec. storage.nested_map_vec.try_insert(10, StorageVec {}); // Method 1: Push to the vec directly storage.nested_map_vec.get(10).push(1u8); storage.nested_map_vec.get(10).push(2u8); storage.nested_map_vec.get(10).push(3u8); // Method 2: First get the storage key and then push the values. let storage_key_vec: StorageKey<StorageVec<u8>> = storage.nested_map_vec.get(10); storage_key_vec.push(4u8); storage_key_vec.push(5u8); storage_key_vec.push(6u8); }

这里有两种等价写法:

  • 方法 1(直接访问)storage.nested_map_vec.get(10).push(...)链式调用;
  • 方法 2(先取StorageKey再操作):先let key = storage.nested_map_vec.get(10)拿到类型为StorageKey<StorageVec<u8>>的键,再在键上调用push

注意第一步try_insert(10, StorageVec {}):由于存储集合在编译期不会自动初始化,若在写入前该键尚未初始化,直接get访问会 revert(详见 blockchain-development/storage.md 的说明)。因此写入前必须用try_insert为键10建立空的StorageVec

读取(见 nested_storage_variables/src/main.sw):

#[storage(read, write)] fn get_map_vec() { // Method 1: Access the StorageVec directly. let stored_val1: u8 = storage.nested_map_vec.get(10).pop().unwrap(); let stored_val2: u8 = storage.nested_map_vec.get(10).pop().unwrap(); let stored_val3: u8 = storage.nested_map_vec.get(10).pop().unwrap(); // Method 2: First get the storage key and then access the value. let storage_key: StorageKey<StorageVec<u8>> = storage.nested_map_vec.get(10); let stored_val4: u8 = storage_key.pop().unwrap(); let stored_val5: u8 = storage_key.pop().unwrap(); let stored_val6: u8 = storage_key.pop().unwrap(); }

pop()返回Option<V>,因此用unwrap()取出实际值;当向量为空时pop返回None(对应 storage_vec.sw 中len == 0的短路逻辑)。

2.3 在StorageMap<K, V>中存放StorageString

StorageStringStorageVec不同,它不支持按索引的逐元素读写,只能将整个String一次性写入或读出。

写入(见 nested_storage_variables/src/main.sw):

#[storage(write)] fn store_map_string() { // Setup and initialize storage for the StorageString. storage.nested_map_string.try_insert(10, StorageString {}); // Method 1: Store the string directly. let my_string = String::from_ascii_str("Fuel is blazingly fast"); storage.nested_map_string.get(10).write_slice(my_string); // Method 2: First get the storage key and then write the value. let my_string = String::from_ascii_str("Fuel is modular"); let storage_key: StorageKey<StorageString> = storage.nested_map_string.get(10); storage_key.write_slice(my_string); }

先通过String::from_ascii_str构造字符串,再调用write_slice写入。与上一节同理,try_insert(10, StorageString {})负责初始化该键的存储。

读取(见 nested_storage_variables/src/main.sw):

#[storage(read)] fn get_map_string() { // Method 1: Access the string directly. let stored_string: String = storage.nested_map_string.get(10).read_slice().unwrap(); // Method 2: First get the storage key and then access the value. let storage_key: StorageKey<StorageString> = storage.nested_map_string.get(10); let stored_string: String = storage_key.read_slice().unwrap(); }

read_slice()返回Option<String>,配合unwrap()使用。底层的切片读写由标准库storable_slice.sw提供,其中write_slice已标记为 deprecated,推荐使用write_slice_quads(按 quad 对齐写入)或write_slice_slot(按槽直接写入),read_slice_quads/read_slice_slot同理(storable_slice.sw、storable_slice.sw)。

2.4 在StorageVec<T>中存放StorageBytes

第三种嵌套形态是把StorageBytes放进StorageVec<T>。注意StorageBytesStorageVec的语义差异:StorageBytes将字节紧凑打包存储,更省 gas,但只能整体读写,无法像StorageVec<T>那样单独 push/pop 元素;若需要频繁修改,推荐改用StorageVec<u8>(此建议同样适用于顶层存储,见 blockchain-development/storage.md)。

写入(见 nested_storage_variables/src/main.sw):

#[storage(write)] fn store_vec() { // Setup Bytes to store let mut my_bytes = Bytes::new(); my_bytes.push(1u8); my_bytes.push(2u8); my_bytes.push(3u8); // Setup and initialize storage for the StorageBytes. storage.nested_vec_bytes.push(StorageBytes {}); // Method 1: Store the bytes by accessing StorageBytes directly. storage .nested_vec_bytes .get(0) .unwrap() .write_slice(my_bytes); // Method 2: First get the storage key and then write the bytes. let storage_key: StorageKey<StorageBytes> = storage.nested_vec_bytes.get(0).unwrap(); storage_key.write_slice(my_bytes); }

这里storage.nested_vec_bytes.push(StorageBytes {})先向向量压入一个空的StorageBytes完成初始化;由于StorageVec<T>::get返回Option<StorageKey<V>>,需要unwrap()取出StorageKey<StorageBytes>后再write_slice

读取(见 nested_storage_variables/src/main.sw):

#[storage(read, write)] fn get_vec() { // Method 1: Access the stored bytes directly. let stored_bytes: Bytes = storage.nested_vec_bytes.get(0).unwrap().read_slice().unwrap(); // Method 2: First get the storage key and then access the stored bytes. let storage_key: StorageKey<StorageBytes> = storage.nested_vec_bytes.get(0).unwrap(); let stored_bytes: Bytes = storage_key.read_slice().unwrap(); }

读取结果类型为Bytes,即 Sway 标准库的堆上字节集合类型(std::bytes::Bytes)。

三、Storage Namespace:为存储槽计算加盐

当合约代码被加载到与其他合约共享的环境时,不同来源的存储变量可能计算出相同的槽位,从而发生存储碰撞。如果你希望存储中的值被定位到不同的位置,可以使用namespace 注解为槽位计算加入一个"盐值"(salt),从根源上规避冲突。

语法是在storage块内部、变量名之前用一个命名字段包一层:

storage { example_namespace { foo: u64 = 0, }, }

完整示例见 storage_namespace/src/main.sw。声明之后,访问方式与普通存储变量完全一致——storage.foo.write(amount)写入、storage.foo.try_read().unwrap_or(0)读取:

abi StorageNamespaceExample { #[storage(write)] fn store_something(amount: u64); #[storage(read)] fn get_something() -> u64; } impl StorageNamespaceExample for Contract { #[storage(write)] fn store_something(amount: u64) { storage.foo.write(amount); } #[storage(read)] fn get_something() -> u64 { storage.foo.try_read().unwrap_or(0) } }

推荐使用try_read()而非read(),因为前者在槽位尚未写入时返回Option<T>(此处用unwrap_or(0)兜底),能避免未初始化访问导致的 revert。namespace 的名字会参与该命名空间下所有变量的槽位派生计算,因此不同的 namespace 会产出不同的槽键集合,即使变量名相同也不会互相覆盖。这一机制与 blockchain-development/storage.md 中介绍的storage块声明模型一脉相承,只是额外引入了命名空间维度。

四、Manual Storage Management:手动存储管理

除了声明式storage块,你还可以直接调用标准库提供的底层存储 API——std::storage::storage_api::writestd::storage::storage_api::read——来操作 FuelVM 的存储原语。采用这种方式时,内部存储键必须由你手动指定,编译器不会替你计算槽位。

以下是一个完整的最小示例(storage_example/src/main.sw):

contract; use std::storage::storage_api::{read, write}; abi StorageExample { #[storage(write)] fn store_something(amount: u64); #[storage(read)] fn get_something() -> u64; } const STORAGE_KEY: b256 = 0x0000000000000000000000000000000000000000000000000000000000000000; impl StorageExample for Contract { #[storage(write)] fn store_something(amount: u64) { write(STORAGE_KEY, 0, amount); } #[storage(read)] fn get_something() -> u64 { let value: Option<u64> = read::<u64>(STORAGE_KEY, 0); value.unwrap_or(0) } }

关键点拆解:

  • 手动指定存储键:这里定义了一个常量STORAGE_KEYb256,全零),所有读写都围绕该键进行。你可以为不同的数据分配不同的b256常量作为各自的槽键。
  • write(slot, offset, value):将value写入从slot开始、偏移offset(以 word 为单位)的存储位置。从标准库源码(storage_api.sw)看,值按 32 字节槽存放,若跨槽边界则继续写入下一个槽;offset可以超出slot边界(如 offset 为 4 表示下一个槽的开头)。若T是零尺寸类型则不会发生任何存储访问。写入前若目标槽已有部分数据,会先读取被部分覆盖的旧数据再合并写回(对应 1 次读取 + 1 次写入的访问成本)。
  • read::<T>(slot, offset):按类型T从指定槽与偏移处读取,返回Option<T>;槽位为空时返回None,因此示例中用unwrap_or(0)提供默认值。
  • 存储注解不可省略:写入函数需要#[storage(write)],读取函数需要#[storage(read)],这与声明式存储的要求一致。

Note:虽然read/write可以用于任何数据类型,但它们主要应被用于数组(array)——因为数组目前还不受storage块支持(见 blockchain-development/storage.md 中受支持的集合列表:StorageMap<K, V>StorageVec<T>StorageBytesStorageString均不包含数组)。此外,所有数据类型都可以无限制地用作StorageMap<K, V>的键类型和/或值类型,因此在需要以任意类型为键做映射存储时,优先考虑StorageMap而非手动管理。

补充说明:底层槽位偏移计算的细节在storage_api.swslot_calculator<T>(slot, offset)中实现(storage_api.sw),它会根据T的大小推算出实际首槽、占用槽数以及首槽内的起始 word,write/read内部正是基于该计算定位最终槽位的。

五、小结:三种高级存储技术的适用场景

技术核心语法适用场景关键注意事项
嵌套存储集合StorageMap<K, StorageVec<V>>StorageMap<K, StorageString>StorageVec<StorageBytes>需要"映射→集合""集合→字节串"等复合结构,如按账户维度维护各自的动态数据写入前必须try_insert/push初始化;导入存储类型需用 glob 操作符;StorageString/StorageBytes只能整体读写
Storage Namespacestorage { ns { field: T = init } }加载外部合约代码、多合约共享环境时规避存储槽碰撞namespace 名为槽位计算提供盐值;访问语法与普通存储一致
手动存储管理write(slot, offset, value)/read::<T>(slot, offset)存储不受storage块支持的数组等类型;完全掌控槽位布局必须自行定义并维护b256存储键;read返回Option<T>需处理空值;函数需标注#[storage(read)]/#[storage(write)]

三种技术都建立在相同的底层模型之上:StorageKey<T>承载的「槽 + 偏移 + 字段 ID」寻址、标准库storage模块(storage.sw)提供的storage_api/storage_key/storage_map/storage_vec/storage_bytes/storage_string等子模块,以及 FuelVM 的__state_load_quad/__state_store_quad存储指令。想进一步了解基础用法,可继续阅读 blockchain-development/storage.md 与 common-collections/storage_map.md。

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

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询