Cairo存储变量类型

本文详细介绍了Cairo中合约存储相关的概念,包括starknet::Store trait、访问trait(如StoragePointerReadAccessStorageMapWriteAccess等),以及如何在storage中使用基本类型、Map、Vec、结构体和枚举等。

在 Cairo 合约存储中存储类型之前,它必须实现一个名为 starknet::Store trait 的 trait。这个 trait 定义了一种类型如何在存储中被序列化和反序列化。简单来说,它为编译器提供了读取和写入合约存储中的“类型”所需的逻辑。

对于整数、boolfelt252ByteArray 等类型,Cairo 已经提供了 trait 的实现。因此,这些类型可以直接在合约存储中使用,而无需任何额外的工作。例如,在下面的合约中,felt252u256 都是有效的存储成员,因为它们已经实现了该 trait。

#[storage]
struct Storage {
    num1: felt252,
    num2: u256,
}

然而,当在存储中处理复杂类型(如 mappings、数组或用户定义的 structs)时,我们必须要么 derive 该 trait,要么使用 Cairo 提供的特殊类型来表示存储中的类型。这些情况将在后面的章节中详细讨论。

虽然 starknet::Store trait 允许在合约存储中使用类型,但它不处理读取或写入存储中的值。这个责任落在一组额外的 traits 上,在本文中我们将称之为访问 traits。

本文将介绍可以在存储中使用的不同类型,以及每种类型需要哪些 traits 才能在存储中使用。

存储访问 Traits

访问 traits 决定了如何基于被访问的类型来从存储中读取或写入值。Cairo 在内部使用不同的访问 traits 来解析这些操作,具体取决于我们是与已经实现了 starknet::Store trait 的类型还是特殊类型进行交互。

以下是这些访问 traits 的一个快速分解,我们将在后面详细介绍:

  • StoragePointerReadAccessStoragePointerWriteAccess: 用于读取和写入简单类型或实现了 starknet::Store 的自定义 structs 的值。
  • StorageMapReadAccessStorageMapWriteAccess: 处理从存储中的 mapping (key-value) 类型读取和写入。
  • StoragePathEntry: 帮助解析对嵌套 mapping 的访问。
  • VecTraitMutableVecTrait: 提供对存储中动态数组的访问。

现在我们知道了 Cairo 的存储中使用的任何类型都必须实现 starknet::Store trait,并且读取或写入它需要导入适当的访问 trait,让我们看看哪些类型已经实现了 starknet::Store trait,然后再继续了解如何在合约级别上进行读取和写入。

实现了 starknet::Store Trait 的类型

以下 Cairo 类型实现了 starknet::Store trait:

  • felt252
  • unsigned and signed integers (无符号和有符号整数)
  • bool
  • bytes31
  • ByteArray
  • ContractAddress
  • Tuple (元组)

由于以上类型已经实现了 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 的存储中声明不同的数据类型,下一步就是了解如何使用它们,也就是说,如何实际将值写入存储并在以后读回它们。

Write Operation (写入操作)

在我们写入上面声明的任何状态变量之前,我们必须首先导入 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));
        }
    }
}

Read Operations (读取操作)

对于读取操作,我们需要导入 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();
        }
    }
}

Mapping 和 Vec

像 dictionaries 和 arrays 这样的集合类型不能直接存储在 Cairo 合约存储中。这是因为它们使用存储系统默认不支持的动态内存布局。相反,Cairo 提供了用于在存储中使用集合的专用类型:MapVec

这些特殊类型在 starknet::storage 模块中可用,用于在合约存储中声明 mappings 和 vectors。在我们可以在 Storage struct 中使用它们之前,我们需要从核心库中显式导入这些特殊类型,如下所示:

use starknet::storage::{ Map, Vec };

请注意,MapVec 不需要一起导入,你可以只导入你需要的那个,具体取决于你的用例。例如,如果你的合约只需要 mappings,那么只导入 Map 类型就可以了。

一旦我们导入了 MapVec,我们就可以在 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 类型接受两个泛型参数:KeyTypeValueType。在我们的例子中,ContractAddress 是 key,u256 是 value,这意味着这个 map 为每个地址存储一个 u256 的量。另一方面,Vec 类型接受单个类型,并表示该类型的元素的数组。在我们的例子中,它是一个 64 位无符号整数的数组。

请注意,Cairo 中没有像我们在 Solidity 和其他语言中拥有的传统的“固定数组”存储类型。

设置好这些状态变量后,让我们看看如何使用读写操作与它们进行交互。

在 Map 类型上进行读取和写入操作

在我们的例子中,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 地址存储的值并返回它。

Nested Maps Operations (嵌套 Map 操作)

在存储中读取或写入嵌套的 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 类型执行类似的操作,包括推送新元素和从特定索引读取。

在 Vec 类型上进行读取和写入操作

Cairo 中的 Vec 类型用于表示合约存储中可增长的数组,类似于 Solidity 中的动态数组,如 uint64[]。它支持常见的操作,如追加新元素,按索引访问项目,以及从数组末尾删除元素。

为了继续我们的例子,我们将与我们存储声明中的 Vec 类型进行交互:

#[storage]
struct Storage {
    // Solidity equivalent: uint64[] my_vec;
    my_vec: Vec<u64>,
}

但在此之前,我们有两个与 Vec 类型关联的 traits:VecTraitMutableVecTrait

VecTrait 提供了只读方法,用于与存储中的 vectors 进行交互。这包括:

  • .len() – 返回 vector 中当前元素的数量。
  • .get(index) – 安全地返回一个指向给定索引处元素的指针。如果索引超出范围,则返回 None
  • .at(index) – 返回一个指向给定索引处元素的指针,但是如果索引无效,则panic

MutableVecTrait 通过添加 mutating 方法来扩展 VecTrait,这些方法允许你修改存储中 vector 的内容。这些包括:

  • .push(value) – 将新元素附加到 vector 的末尾。
  • .pop() – 删除并返回最后一个元素,如果 vector 为空,则返回 None
  • .allocate() – 在 vector 的末尾为新元素保留空间,并返回一个可写的指针,对于复杂或嵌套类型很有用。

根据 vector 操作,我们可能还需要导入访问 traits,如 StoragePointerWriteAccessStoragePointerReadAccess

在接下来的小节中,我们将通过示例来介绍使用这些 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

Struct 和枚举类型在存储中的表现

与默认实现 starknet::Store trait 的类型(u8boolfelt252 等)不同,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 中,像 VecMap 这样的集合类型不能作为字段包含在 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,称为存储节点。

存储节点

存储节点仍然是结构体,但有一个关键的区别:它们可以包含动态集合类型,如 VecMap。与我们之前讨论过的,不支持集合的常规用户定义的结构体不同,存储节点专门设计用于处理它们,这使得它们非常适合管理存储中的嵌套或动态数据。

定义存储节点

要定义一个存储节点,我们使用 #[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:

  1. .write(value)
  2. .push(value)
  3. .entry(key)
  4. .write(key, value)
  5. .read()
  6. .at(index)
  7. .read(key)

结论

总结一下,以下是 Cairo 中最常用的存储访问 traits 的摘要。每个 trait 启用特定的方法,使我们能够与存储类型(如简单类型、MapVec、结构体和枚举)进行交互。根据我们想要如何读取或写入存储,我们需要导入下面列出的适当的 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
RareSkills
RareSkills
https://www.rareskills.io/