本文详细介绍了Cairo中合约存储相关的概念,包括starknet::Store trait、访问trait(如StoragePointerReadAccess、StorageMapWriteAccess等),以及如何在storage中使用基本类型、Map、Vec、结构体和枚举等。
在 Cairo 合约存储中存储类型之前,它必须实现一个名为 starknet::Store trait 的 trait。这个 trait 定义了一种类型如何在存储中被序列化和反序列化。简单来说,它为编译器提供了读取和写入合约存储中的“类型”所需的逻辑。
对于整数、bool、felt252、ByteArray 等类型,Cairo 已经提供了 trait 的实现。因此,这些类型可以直接在合约存储中使用,而无需任何额外的工作。例如,在下面的合约中,felt252 和 u256 都是有效的存储成员,因为它们已经实现了该 trait。
#[storage]
struct Storage {
num1: felt252,
num2: u256,
}
然而,当在存储中处理复杂类型(如 mappings、数组或用户定义的 structs)时,我们必须要么 derive 该 trait,要么使用 Cairo 提供的特殊类型来表示存储中的类型。这些情况将在后面的章节中详细讨论。
虽然 starknet::Store trait 允许在合约存储中使用类型,但它不处理读取或写入存储中的值。这个责任落在一组额外的 traits 上,在本文中我们将称之为访问 traits。
本文将介绍可以在存储中使用的不同类型,以及每种类型需要哪些 traits 才能在存储中使用。
访问 traits 决定了如何基于被访问的类型来从存储中读取或写入值。Cairo 在内部使用不同的访问 traits 来解析这些操作,具体取决于我们是与已经实现了 starknet::Store trait 的类型还是特殊类型进行交互。
以下是这些访问 traits 的一个快速分解,我们将在后面详细介绍:
StoragePointerReadAccess 和 StoragePointerWriteAccess: 用于读取和写入简单类型或实现了 starknet::Store 的自定义 structs 的值。StorageMapReadAccess 和 StorageMapWriteAccess: 处理从存储中的 mapping (key-value) 类型读取和写入。StoragePathEntry: 帮助解析对嵌套 mapping 的访问。VecTrait 和 MutableVecTrait: 提供对存储中动态数组的访问。现在我们知道了 Cairo 的存储中使用的任何类型都必须实现 starknet::Store trait,并且读取或写入它需要导入适当的访问 trait,让我们看看哪些类型已经实现了 starknet::Store trait,然后再继续了解如何在合约级别上进行读取和写入。
starknet::Store Trait 的类型以下 Cairo 类型实现了 starknet::Store trait:
由于以上类型已经实现了 starknet::Store trait,因此从存储中读取和写入只需要导入必要的访问 traits,以使 read() 和 write() 方法可用。
考虑一个使用上面列出的类型声明了多个状态变量的合约。下面的代码片段展示了如何在存储中声明每种类型:
#[starknet::contract]
mod HelloStarknet {
use starknet::ContractAddress;
#[storage]
struct Storage {
// felt252: Field element (字段元素)
user_id: felt252,
// u256: 256-bit unsigned integer (256 位无符号整数)
total_supply: u256,
// bool: Boolean value for true/false conditions (真/假条件的布尔值)
is_paused: bool,
// bytes31: Fixed-size byte array (31 bytes), for storing short strings/data (固定大小的字节数组(31 字节),用于存储短字符串/数据)
contract_name: bytes31,
// ByteArray: for storing long strings (用于存储长字符串)
contract_description: ByteArray,
// ContractAddress: Starknet contract address type (Starknet 合约地址类型)
owner_address: ContractAddress,
// Tuple: Groups multiple values together (将多个值组合在一起)
version_info: (u8, i8) // (unsigned integer, signed integer) (无符号整数,有符号整数)
}
}
现在我们已经了解了如何在 Cairo 的存储中声明不同的数据类型,下一步就是了解如何使用它们,也就是说,如何实际将值写入存储并在以后读回它们。
在我们写入上面声明的任何状态变量之前,我们必须首先导入 StoragePointerWriteAccess trait。这个 trait 允许在存储变量上使用 .write(value) 方法,允许我们通过它们的存储指针直接赋值。
它在 starknet::storage 模块中可用:
// Import `StoragePointerWriteAccess` trait
use starknet::storage::StoragePointerWriteAccess;
以下是如何对不同的简单类型执行写入操作(新添加的代码用 `/ NEWLY ADDED /` 注释):
#[starknet::contract]
mod HelloStarknet {
use starknet::ContractAddress;
// Import `StoragePointerWriteAccess` trait
use starknet::storage::StoragePointerWriteAccess; /* NEWLY ADDED */
#[storage]
struct Storage {
user_id: felt252,
total_supply: u256,
is_paused: bool,
contract_name: bytes31,
contract_description: ByteArray,
owner_address: ContractAddress,
version_info: (u8, i8)
}
/* NEWLY ADDED */
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn write_vars(ref self: ContractState) {
// Writing to felt252
self.user_id.write(12345);
// Writing to u256
self.total_supply.write(1000000_u256);
// Writing to bool
self.is_paused.write(false);
// Writing to bytes31 (short string)
self.contract_name.write('HelloContract'.try_into().unwrap());
// Writing to ByteArray (long string)
self.contract_description.write("This is a very very very long textttt");
// Writing to ContractAddress
self.owner_address.write(0x1234.try_into().unwrap());
// Writing to tuple
self.version_info.write((1_u8, -2_i8));
}
}
}
对于读取操作,我们需要导入 StoragePointerReadAccess trait,它允许我们在上面声明的类型上使用 .read() 方法:
use starknet::storage::StoragePointerReadAccess;
扩展前一节的合约,下面的代码导入了 StoragePointerReadAccess trait,并从状态变量中读取值(新添加的代码用 `/ NEWLY ADDED /` 注释):
#[starknet::contract]
mod HelloStarknet {
use starknet::ContractAddress;
// Import `StoragePointerWriteAccess` trait
use starknet::storage::{
StoragePointerWriteAccess,
/* NEWLY ADDED */
StoragePointerReadAccess,
};
#[storage]
struct Storage {
user_id: felt252,
total_supply: u256,
is_paused: bool,
contract_name: bytes31,
contract_description: ByteArray,
owner_address: ContractAddress,
version_info: (u8, i8)
}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn write_vars(ref self: ContractState) {
self.user_id.write(12345);
self.total_supply.write(1000000_u256);
self.is_paused.write(false);
self.contract_name.write('HelloContract'.try_into().unwrap());
self.contract_description.write("This is a very very very long textttt");
self.owner_address.write(0x1234.try_into().unwrap());
self.version_info.write((1_u8, -2_i8));
}
/* NEWLY ADDED */
fn read_vars(ref self: ContractState) {
// felt252: Reading user ID returns a field element (0 to P-1 range) (读取用户 ID 返回一个字段元素 (0 到 P-1 范围))
let _ = self.user_id.read();
// u256: Reading large integer, useful for token balances and big numbers (读取大整数,对于 token 余额和大数字很有用)
let _ = self.total_supply.read();
// bool: Reading boolean state, returns true or false (读取布尔状态,返回 true 或 false)
let _ = self.is_paused.read();
// bytes31: Reading fixed-size byte array, used for short strings (读取固定大小的字节数组,用于短字符串)
let _ = self.contract_name.read();
// ByteArray: Reading dynamic-size byte array, used for long strings (读取动态大小的字节数组,用于长字符串)
let _ = self.contract_description.read();
// ContractAddress: Reading Starknet address, type-safe contract/user address (读取 Starknet 地址,类型安全的合约/用户地址)
let _ = self.owner_address.read();
// Tuple: Reading compound type returns both values as (u8, i8) pair (读取复合类型返回两个值作为 (u8, i8) 对)
let _ = self.version_info.read();
}
}
}
像 dictionaries 和 arrays 这样的集合类型不能直接存储在 Cairo 合约存储中。这是因为它们使用存储系统默认不支持的动态内存布局。相反,Cairo 提供了用于在存储中使用集合的专用类型:Map 和 Vec。
这些特殊类型在 starknet::storage 模块中可用,用于在合约存储中声明 mappings 和 vectors。在我们可以在 Storage struct 中使用它们之前,我们需要从核心库中显式导入这些特殊类型,如下所示:
use starknet::storage::{ Map, Vec };
请注意,
Map和Vec不需要一起导入,你可以只导入你需要的那个,具体取决于你的用例。例如,如果你的合约只需要 mappings,那么只导入Map类型就可以了。
一旦我们导入了 Map 和 Vec,我们就可以在 storage struct 中使用它们,如下所示:
use starknet::storage::{ Map, Vec };
#[storage]
struct Storage {
// mapping(address => uint256) my_map;
my_map: Map<ContractAddress, u256>,
// uint64[] my_vec;
my_vec: Vec<u64>,
}
在上面的代码中,如何在 Solidity 中声明 Map 和 Vec 类型的等价物在每个声明上方都注释了。Map 类型接受两个泛型参数:KeyType 和 ValueType。在我们的例子中,ContractAddress 是 key,u256 是 value,这意味着这个 map 为每个地址存储一个 u256 的量。另一方面,Vec 类型接受单个类型,并表示该类型的元素的数组。在我们的例子中,它是一个 64 位无符号整数的数组。
请注意,Cairo 中没有像我们在 Solidity 和其他语言中拥有的传统的“固定数组”存储类型。
设置好这些状态变量后,让我们看看如何使用读写操作与它们进行交互。
在我们的例子中,my_map 表示从地址到 u256 值的 mapping,类似于我们在 Solidity 中定义 mapping(address => uint256) 的方式。
my_map: Map<ContractAddress, u256>
在执行这些操作之前,我们首先需要导入必要的访问 traits,这些 traits 允许从存储中进行读取和写入。它们也在 starknet::storage 模块中可用。
#[starknet::contract]
mod HelloStarknet {
// IMPORT MAP TYPE AND NECESSARY ACCESS TRAITS (导入 MAP 类型和必要的访问 TRAITS)
use starknet::storage::{
Map,
StorageMapWriteAccess, // Enables .write(key, value) operations (允许 .write(key, value) 操作)
StorageMapReadAccess, // Enables .read(key) operations (允许 .read(key) 操作)
};
}
这些 traits 允许使用 .write(key, value) 和 .read(key) 这样的方法,我们将在下面的示例中使用它们。不导入它们,我们将无法对我们的存储集合执行任何这些操作。
有了这些,我们现在可以为 Map 类型实现写入和读取操作。
Write Operation (写入操作)
我们使用 StorageMapWriteAccess 提供的 .write(key, value) 方法。语法很简单:
self.my_map.write(key, value);
以下是每个部分的作用:
self.my_map 指的是在 storage struct 中声明的 Map。.write(...) 是更新 map 的方法。key 是将在其下存储 value 的标识符(在我们的例子中,是一个 ContractAddress)。value 是实际的数据(在我们的例子中,是一个 u256),它会被保存。每次调用 write() 都会插入一个新的 key-value 对,或者如果 key 已经存在,则覆盖现有的 value。
下面是如何在函数中使用 .write() 的一个例子:
#[starknet::contract]
mod HelloStarknet {
//...
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn write_to_mapping(
ref self: ContractState,
user: ContractAddress,
amount: u256
) {
self.my_map.write(user, amount); // write operation (写入操作)
}
}
}
这会为给定的 user 地址存储 amount。在底层,Cairo 基于 key 处理将其写入适当的存储槽。
Read Operation (读取操作)
我们使用 StorageMapReadAccess 提供的 .read(key) 方法。语法如下所示:
self.my_map.read(key);
以下是它所做的事情的细分:
self.my_map 指的是 storage struct 中的 map 实例。.read(...) 访问存储的值。key 是我们想要查找的标识符,在我们的例子中,是一个 ContractAddress,因为我们将其用作 map 的 key。read() 方法返回与 key 关联的值。如果 key 以前没有被写入过,它会返回 map 的 valueType 的默认值(例如,对于 u256,返回 0)。
例子:
#[starknet::contract]
mod HelloStarknet {
// ...
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn get_value(self: @ContractState, user: ContractAddress) -> u256 {
self.my_map.read(user) // read operation (读取操作)
}
}
}
该函数读取为给定的 user 地址存储的值并返回它。
在存储中读取或写入嵌套的 mapping 需要一个额外的访问 trait,名为 StoragePathEntry。此 trait 允许使用 .entry(key) 方法,该方法提供对存储在给定 key 下的内部 maps 的访问。
换句话说,当我们处理一个嵌套的 mapping,其中 value 本身就是一个 Map 时,我们不能直接使用 .read() 或 .write() 来访问它。相反,我们必须首先调用 .entry(key) 来访问内部层,然后对其执行操作。
Declare a Nested Mapping (声明一个嵌套的 Mapping)
让我们在存储中声明我们的嵌套 mapping。这将是一个两级 map,其中外部 map 使用 ContractAddress(用户的地址)作为 key,内部 map 也使用 ContractAddress(token 的地址)作为 key,并且存储的 value 是一个 u256,表示 token 的余额:
#[storage]
struct Storage {
// user_address => token_address => balance
two_level_mapping: Map<ContractAddress, Map<ContractAddress, u256>>,
}
Importing Required Trait (导入所需的 Trait)
use starknet::storage::StoragePathEntry;
StoragePathEntry 允许使用 .entry(key) 方法来获取序列中下一个 key 的存储路径。
虽然 .entry() 允许我们访问嵌套的层,但它本身不足以执行读取或写入操作。我们仍然需要导入那些启用这些特定方法的 traits。我们将需要的确切 traits 取决于我们如何执行这些操作。
我们将看看在存储中读取和写入嵌套 mapping 的两种方法。
Write and Read Operation (写入和读取操作)
方法 1: 始终链接 .entry() 用于 N 层(一直到 value)
这种方法通过每个 map 层链接多次 .entry() 调用,直到它到达存储槽。然后,它使用 .write(value) 或 .read() 直接与存储的 value 交互。
#[starknet::contract]
mod HelloStarknet {
use starknet::ContractAddress;
// IMPORT TRAITS (导入 TRAITS)
use starknet::storage::{
Map,
StoragePathEntry, // Enables .entry(key) (允许 .entry(key))
/* ADDITIONAL TRAITS */
StoragePointerWriteAccess, // Enables .write(value) (允许 .write(value))
StoragePointerReadAccess, // Enables .read() (允许 .read())
};
#[storage]
struct Storage {
// user_address => token_address => balance
two_level_mapping: Map<ContractAddress, Map<ContractAddress, u256>>,
}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn write_nested_map(
ref self: ContractState,
key1: ContractAddress,
key2: ContractAddress,
value: u256,
) {
// WRITE OPERATION (写入操作)
self.two_level_mapping.entry(key1).entry(key2).write(value);
}
fn read_nested_map(
ref self: ContractState,
key1: ContractAddress,
key2: ContractAddress,
) -> u256 {
// READ OPERATION (读取操作)
self.two_level_mapping.entry(key1).entry(key2).read()
}
}
}
对于读取和写入操作,两个函数都使用了两次 .entry() 方法:
.entry(key1) 访问外部 map.entry(key2) 深入到内部 map 以到达特定的存储槽一旦我们到达了那个确切的存储位置,我们就使用:
StoragePointerWriteAccess trait 启用的 .write(value),将 value 直接写入该槽。StoragePointerReadAccess trait 启用的 .read(),从该槽读取 value。方法 2: 链接 .entry() 用于 N-1 层(在最里面的 map 停止)
这种方法使用 .entry() 方法深入到每个 map 层,直到它到达内部 mapping,然后将其视为一个整体,并使用 .write(key, value) 和 .read(key) 直接与其交互。
#[starknet::contract]
mod HelloStarknet {
use starknet::ContractAddress;
// IMPORT TRAITS (导入 TRAITS)
use starknet::storage::{
Map,
StoragePathEntry, // Enables .entry(key) method (允许 .entry(key) 方法)
/* ADDITIONAL TRAITS */
StorageMapWriteAccess, // Enables .write(key, value) method (允许 .write(key, value) 方法)
StorageMapReadAccess, // Enables .read(key) method (允许 .read(key) 方法)
};
#[storage]
struct Storage {
// user_address => token_address => balance
two_level_mapping: Map<ContractAddress, Map<ContractAddress, u256>>,
}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn write_nested_map(
ref self: ContractState,
key1: ContractAddress,
key2: ContractAddress,
value: u256,
) {
// WRITE OPERATION (写入操作)
self.two_level_mapping.entry(key1).write(key2, value);
}
fn read_nested_map(
ref self: ContractState,
key1: ContractAddress,
key2: ContractAddress,
) -> u256 {
// READ OPERATION (读取操作)
self.two_level_mapping.entry(key1).read(key2)
}
}
}
对于读取和写入操作,上面代码中的每个函数都使用 .entry(key1) 方法一次:
.entry(key1) 允许访问内部 map。一旦我们有了对该内部 map 的引用,我们就将其视为一个常规的 Map:
StorageMapWriteAccess trait 启用的 .write(key2, value) 在 key2 下存储 value。StorageMapReadAccess trait 启用的 .read(key2) 检索存储在 key2 下的 value。这种方法将内部 value 视为一个完整的 map,而不是单个存储槽。
两种方法都是有效的,开发人员只需要根据他们计划如何与嵌套 mapping 交互来导入适当的 traits。
接下来,我们将探讨如何对 Vec 类型执行类似的操作,包括推送新元素和从特定索引读取。
Cairo 中的 Vec 类型用于表示合约存储中可增长的数组,类似于 Solidity 中的动态数组,如 uint64[]。它支持常见的操作,如追加新元素,按索引访问项目,以及从数组末尾删除元素。
为了继续我们的例子,我们将与我们存储声明中的 Vec 类型进行交互:
#[storage]
struct Storage {
// Solidity equivalent: uint64[] my_vec;
my_vec: Vec<u64>,
}
但在此之前,我们有两个与 Vec 类型关联的 traits:VecTrait 和 MutableVecTrait。
VecTrait 提供了只读方法,用于与存储中的 vectors 进行交互。这包括:
.len() – 返回 vector 中当前元素的数量。.get(index) – 安全地返回一个指向给定索引处元素的指针。如果索引超出范围,则返回 None。.at(index) – 返回一个指向给定索引处元素的指针,但是如果索引无效,则panic。MutableVecTrait 通过添加 mutating 方法来扩展 VecTrait,这些方法允许你修改存储中 vector 的内容。这些包括:
.push(value) – 将新元素附加到 vector 的末尾。.pop() – 删除并返回最后一个元素,如果 vector 为空,则返回 None。.allocate() – 在 vector 的末尾为新元素保留空间,并返回一个可写的指针,对于复杂或嵌套类型很有用。根据 vector 操作,我们可能还需要导入访问 traits,如 StoragePointerWriteAccess 或 StoragePointerReadAccess。
在接下来的小节中,我们将通过示例来介绍使用这些 traits 进行的常见操作,如追加、读取、更新和删除元素。
向 my_vec Vector 推送一个新 Value
在 push_number 函数中使用的 push 方法首先递增 vector 的长度,然后在 vector 末尾的新存储槽中写入 value。
// IMPORT TRAITS (导入 TRAITS)
use starknet::storage::{Vec, MutableVecTrait};
fn push_number(ref self: ContractState, value: u64) {
// PUSH OPERATION (推送操作)
self.my_vec.push(value);
}
从现有索引读取
如果我们想要检索现有索引处的 value,我们可以使用 .get() 或 .at() 获取指针,然后使用 .read() 读取其 value:
// IMPORT TRAITS (导入 TRAITS)
use starknet::storage::{Vec, MutableVecTrait, StoragePointerReadAccess};
fn read_my_vec(self: @ContractState, index: u64) -> u64 {
// VEC READ OPERATION (VEC 读取操作)
self.my_vec.at(index).read() // Will panic if index is out of bounds (如果索引超出范围将 panic)
}
练习:我们为什么要添加 StoragePointerReadAccess trait?
更新现有索引处的 Value
要更新现有索引处的 value,我们可以使用 .get() 或 .at() 获取指针,然后使用 .write(value) 修改其 value:
// IMPORT TRAITS (导入 TRAITS)
use starknet::storage::{Vec, MutableVecTrait, StoragePointerWriteAccess};
fn write_my_vec(self: @ContractState, index: u64, val: u64) -> u64 {
// VEC WRITE OPERATION (VEC 写入操作)
self.my_vec.at(index).write(val) // Will panic if index is out of bounds (如果索引超出范围将 panic)
}
获取 Vector 的长度
.len() 返回 vector 中当前元素的数量,类型为 u64。
// IMPORT TRAITS (导入 TRAITS)
use starknet::storage::{Vec, MutableVecTrait};
fn get_vec_len(self: @ContractState) -> u64 {
// RETURN VEC LENGTH (返回 VEC 长度)
self.my_vec.len()
}
弹出最后一个元素
use starknet::storage::{Vec, MutableVecTrait};
fn pop_last(ref self: ContractState) {
// POP OPERATION (弹出操作)
self.my_vec.pop();
}
.pop() 检索存储在 vector 中最后一个位置的 value,递减 vector 的长度,然后返回检索到的 value,如果 vector 为空,则返回 None。
与默认实现 starknet::Store trait 的类型(u8、bool、felt252 等)不同,structs 需要你显式地 derive 该 trait,否则,任何在存储中使用该 struct 的尝试都会在编译时失败。
要读取或写入一个 struct 到存储,它必须实现必要的读取和写入函数,这就是 starknet::Store trait 自动执行的操作。
为了使 struct 能够存储在 Cairo 的存储中,我们需要通过在其定义上方添加这个属性 #[derive(starknet::Store)] 来 derive 该 trait:
#[derive(starknet::Store)]
struct User {
id: u32,
name: bytes31,
is_admin: bool,
}
完成此操作后,struct 可以在与存储相关的操作中使用,包括在 #[storage] struct 中作为 mappings 和数组中的类型。
下面是一个示例合约,演示了如何在存储中声明一个自定义 struct,导入必要的 traits,以及对该 struct 执行读取和写入操作。
#[starknet::contract]
mod HelloStarknet {
// IMPORT TRAITS (导入 TRAITS)
use starknet::storage::{
StoragePointerReadAccess, // Enables .read() (允许 .read())
StoragePointerWriteAccess // Enables .write(value) (允许 .write(value))
};
// CUSTOM STRUCT DEFINITION (自定义 STRUCT 定义)
#[derive(starknet::Store)]
struct UserData {
id: u32,
name: bytes31,
is_admin: bool,
}
#[storage]
struct Storage {
// CUSTOM STRUCT DECLARATION (自定义 STRUCT 声明)
user: UserData,
}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
// WRITE OPERATION (写入操作)
fn write_struct(ref self: ContractState, _id: u32, _name: bytes31, _is_admin: bool) {
self.user.id.write(_id); // Write to field 1 (写入字段 1)
self.user.name.write(_name); // Write to field 2 (写入字段 2)
self.user.is_admin.write(_is_admin); // Write to field 3 (写入字段 3)
}
// READ OPERATION (读取操作)
fn read_struct(ref self: ContractState) -> (u32, bytes31, bool) {
let id = self.user.id.read(); // Read from field 1 (从字段 1 读取)
let name = self.user.name.read(); // Read from field 2 (从字段 2 读取)
let is_admin = self.user.is_admin.read(); // Read from field 3 (从字段 3 读取)
(id, name, is_admin)
}
}
}
观察到我们导入了以下 traits:
use starknet::storage::{
StoragePointerReadAccess, // Enables .read() on storage paths (允许在存储路径上使用 .read())
StoragePointerWriteAccess // Enables .write(value) on storage paths (允许在存储路径上使用 .write(value))
};
我们导入这些 traits 是因为 struct 字段是简单类型,如果没有它们,像 .read() 和 .write(value) 这样的调用将无法编译。
Write Operation (写入操作)
在 write_struct 函数内部:
// WRITE OPERATION (写入操作)
fn write_struct(
ref self: ContractState,
_id: u32,
_name: bytes31,
_is_admin: bool
) {
self.user.id.write(_id); // Write to field 1 (写入字段 1)
self.user.name.write(_name); // Write to field 2 (写入字段 2)
self.user.is_admin.write(_is_admin); // Write to field 3 (写入字段 3)
}
每次调用都会将一个新 value 写入存储 struct 中的特定字段。这表明即使 struct 存储为一个对象,它的字段也可以被独立地访问和更新。
Read Operation (读取操作)
这个 read_struct 函数单独读取每个字段并将它们作为元组返回。:
fn read_struct(ref self: ContractState) -> (u32, bytes31, bool) {
let id = self.user.id.read(); // Read from field 1 (从字段 1 读取)
let name = self.user.name.read(); // Read from field 2 (从字段 2 读取)
let is_admin = self.user.is_admin.read(); // Read from field 3 (从字段 3 读取)
(id, name, is_admin)
}
枚举遵循与 structs 类似的模式,我们必须显式地 derive starknet::Store 才能存储它们。每种 variant 类型也必须实现 starknet::Store trait。
以下是定义枚举并在存储中使用它的一个基本示例:
#[starknet::contract]
mod HelloStarknet {
// DEFINE ENUM (定义枚举)
#[derive(starknet::Store)]
enum UserRole {
Admin,
Mod,
#[default]
User,
}
#[storage]
struct Storage {
// DECLARE ENUM (声明枚举)
my_role: UserRole,
}
}
在我们的枚举定义中,我们包含了 #[default] 属性,这是任何将在存储中使用的枚举所必需的。这个属性将其中一个 variant 标记为默认 value(在我们的例子中,是 User variant),当存储 value 尚未设置时,会分配该默认值。
在枚举上执行写入和读取操作
以下代码导入了读取和写入存储中枚举所需的访问 traits,然后是执行这些操作的两个函数:
// IMPORT TRAITS (导入 TRAITS)
use starknet::storage::{
StoragePointerReadAccess, // Enables .read() on enum stored in storage (允许在存储中存储的枚举上使用 .read())
StoragePointerWriteAccess, // Enables .write(value) on enum stored in storage (允许在存储中存储的枚举上使用 .write(value))
};
// WRITE OPERATION (写入操作)
fn write_enum(ref self: ContractState) {
// Write the Admin variant to storage (将 Admin variant 写入存储)
self.my_role.write(UserRole::Admin);
}
// READ OPERATION (读取操作)
fn read_enum(ref self: ContractState) {
// Read the current value of the enum from storage (从存储中读取枚举的当前 value)
let _ = self.my_role.read();
}
在 Cairo 中,像 Vec 或 Map 这样的集合类型不能作为字段包含在 struct 中,也不能作为 variant 包含在枚举中,因为它们依赖于存储系统默认不支持的动态内存布局。
// STRUCT: This will NOT work ❌ - Vec has dynamic size (STRUCT:这将不起作用 ❌ - Vec 具有动态大小)
#[derive(starknet::Store)]
struct InvalidUser {
name: felt252,
balance: u256,
friends: Vec<ContractAddress>, // ERROR: Cannot store Vec in struct (ERROR:无法在 struct 中存储 Vec)
tokenBal: Map<ContractAddress, u256>, // ERROR: Cannot store Map in struct (ERROR:无法在 struct 中存储 Map)
}
// ENUM: This will also NOT work ❌ - Map has dynamic size (ENUM:这也将不起作用 ❌ - Map 具有动态大小)
#[derive(starknet::Store)]
enum InvalidUserRole {
Admin: Map<felt252, bool>, // ERROR: Cannot store Map in enum variant (ERROR:无法在枚举 variant 中存储 Map)
#[default]
User,
}
如果我们需要在 struct 中存储集合,我们必须使用一种特殊的 struct,称为存储节点。
存储节点仍然是结构体,但有一个关键的区别:它们可以包含动态集合类型,如 Vec 和 Map。与我们之前讨论过的,不支持集合的常规用户定义的结构体不同,存储节点专门设计用于处理它们,这使得它们非常适合管理存储中的嵌套或动态数据。
要定义一个存储节点,我们使用 #[starknet::storage_node] 属性,而不是常规的结构体派生:
// 存储节点 - 可以包含集合
#[starknet::storage_node]
struct UserStorageNode {
name: felt252,
balance: u256,
friends: Vec<ContractAddress>, // ✅ 现在允许了!
tokenBal: Map<ContractAddress, u256>, // ✅ 也允许了!
}
#[starknet::storage_node] 属性允许集合类型支持,并自动处理必要的存储逻辑。
一旦定义好,存储节点可以像 #[storage] 结构体中的任何其他类型一样声明。例如,我们将声明一个存储变量 user_data,其类型为上面定义的存储节点类型 (UserStorageNode),就像这样:
#[storage]
struct Storage {
user_data: UserStorageNode,
}
接下来,我们将展示如何初始化 user_data 存储变量,以及如何从中读取数据。
写入存储节点
要写入存储节点,我们直接通过存储变量(在本例中为 user_data)访问它们的字段,然后根据字段类型使用适当的存储方法,如 .write(key, value)、.push() 或 .entry(key).write(value),如下所示:
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn write_nodes(ref self: ContractState) {
// 写入简单字段 (felt252 和 u256)
self.user_data.name.write(3);
self.user_data.balance.write(1000_u256);
// 将新地址推送到 friends 向量
self.user_data.friends.push(get_caller_address());
// 使用以下两种有效方法之一写入嵌套映射
// 方法 1
self.user_data.tokenBal.entry(get_caller_address()).write(23);
// 方法 2
self.user_data.tokenBal.write(get_caller_address(), 23);
}
}
从存储节点读取
从存储节点读取遵循类似的模式:我们直接访问每个字段,并为简单值调用 .read(),为特定的向量元素调用 .at(index),或者为映射调用 .read(key)。
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn read_nodes(self: @ContractState) {
// 读取简单值
let _ = self.user_data.name.read();
let _ = self.user_data.balance.read();
// 从索引 0 处的向量中读取一个值
let _ = self.user_data.friends.at(0);
// 从嵌套映射中读取 token 余额
let _ = self.user_data.tokenBal.read(get_caller_address());
}
}
练习: 通过查看上面存储节点中的读取和写入操作,列出执行以下操作所需的 traits:
.write(value).push(value).entry(key).write(key, value).read().at(index).read(key)总结一下,以下是 Cairo 中最常用的存储访问 traits 的摘要。每个 trait 启用特定的方法,使我们能够与存储类型(如简单类型、Map、Vec、结构体和枚举)进行交互。根据我们想要如何读取或写入存储,我们需要导入下面列出的适当的 traits:
| Trait | 启用的方法 | 目的 |
|---|---|---|
StoragePointerReadAccess |
.read() |
从存储路径(简单类型或结构体字段)读取一个值。 |
StoragePointerWriteAccess |
.write(value) |
将一个值写入到存储路径(简单类型或结构体字段)。 |
StorageMapReadAccess |
.read(key) |
通过键从 Map 中读取一个值。 |
StorageMapWriteAccess |
.write(key, value) |
通过键将一个值写入到 Map 中。 |
StoragePathEntry |
.entry(key) |
更深入地导航到嵌套存储(例如,嵌套 Map 或存储节点)。 |
VecTrait |
.len()、.get(index)、.at(index) |
对 Vec 的只读访问:检查长度,有选择地或直接获取元素。 |
MutableVecTrait |
.push(value)、.pop()、.allocate() |
改变 Vec:添加、删除或为元素准备空间。 |
这篇文章是关于 Starknet 上的 Cairo 编程 系列教程的一部分。
- 原文链接: rareskills.io/post/cairo...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!