# Cairo 之旅 IV：通过 Starklings 学习 Cairo 的数据存储

By [Starknet 中文](https://paragraph.com/@starknet-zh) · 2022-10-27

---

> 作者：[Darington Nnam](https://twitter.com/0xdarlington) 原文：[Journey through Cairo IV — A deep dive into Cairo’s Storage with Starklings](https://medium.com/coinmonks/journey-through-cairo-iv-a-deep-dive-into-cairos-storage-with-starklings-d7e3549470ff) 翻译：[Louis Wang](https://twitter.com/lviswang) 校对：[「StarkNet 中文社区」](https://twitter.com/StarkNet_ZH)

欢迎来到「Cairo 之旅」第四讲，在[上一讲](https://mirror.xyz/starknet-zh.eth/zSGxb7N4D6a1qW6S3q7JrRSaYYIOG9KasKNIVDHOlzg)中，我们学习了Cairo 中的 felt 数据类型和短字符串（Strings）。

像往常一样，如果你是中途加入的，建议从头开始看我们的文章。

_P.S：教程中所有的语法代码都是在 Cairo v0.9.0 版本下执行_

Cairo 数据存储
----------

在这个系列的[第二讲](https://mirror.xyz/starknet-zh.eth/MkxaRxBgs5BSW0jS7Plos4jIM2ZmRhUc5OEAzppBWKI)中，我们学习区分 Cairo 程序和 Cairo 合约，程序是无状态合约，而合约则运行在 Starknet VM 上，因此可访问持续（存储）状态。

**合约存储**是可书写，浏览和修改数据的持续存储空间。根据官方文档，它有 $2^{251}$ 个插槽，其中每个插槽都是初始化为 0 的 felt。

Cairo 中一个存储变量：

    @storage_var
    func id() -> (number: felt):
    end
    

其中 **@storage\_var** 被称为修饰器，用于指定存储变量。

修饰器
---

不同于 solidity，Cairo 中所有的函数执行都由 **func** 关键字指定的，而且难以区分。为此 Cairo 使用修饰器区分这些函数。所有的修饰器都以 @ 开头。

以下是 Cairo 中常见的修饰器：

1.  @storage\_var - 指定状态变量
    
2.  @constructor - 指定构造函数
    
3.  @external - 指定书写状态变量的函数
    
4.  @view - 指定从状态变量中读取的函数
    
5.  @event - 用于指定事件
    
6.  @l1\_handler - 用于指定处理从 L1 合约信息桥所发送信息的函数
    

如何读、写合约中的存储变量
-------------

### 写入存储变量

前文提及，向状态变量写入的函数，必须用 @external 修饰器来指定。列举 Cairo 函数的例子，它更新了前面的存储变量：

_P.S：如果你不理解这里的所有内容，请不要担心，因为我们还没有完整学习过 Cairo 函数。_

    @external
    func update_id{
          syscall_ptr : felt*,
          pedersen_ptr : HashBuiltin*,
          range_check_ptr
        }(_number: felt):
        id.write(_number)
    end
    

重点关注 id.write(\_number)，我们用变量名 .write（）写入或者更新一个状态变量。

### 读取存储变量

读取一个状态变量的值并不困难。如前文所述，必须用 @view 修饰器指定状态变量中读取的函数。从 **id** 状态变量读取的函数例子：

    @view
    func read_id{
          syscall_ptr : felt*,
          pedersen_ptr : HashBuiltin*,
          range_check_ptr
        }(_number: felt):
        id.read()
    end
    

注意关键词 **id.read(\_number)**，类似写入状态的方式，只要用变量名 .read() 即可。

### 存储映射 (Mapping)

不同于 Solidity 会映射其本身的特殊关键字，Cairo 使用存储变量进行映射。

在前文关于 id 的例子中，我们的状态变量只存储了一个值，但也可以创建更复杂的状态变量，即键 -> 值对：

    @storage_var
    func balance(address: felt) -> (amount: felt):
    end
    

状态变量 **balance** 在这里是地址到所持数量的映射。

**要写到这种类型的状态变量**，需要同时提供键 (address) 和值 (balance)

    @external
    func update_balance{
          syscall_ptr : felt*,
          pedersen_ptr : HashBuiltin*,
          range_check_ptr
        }(_address: felt, _amount: felt):
        balance.write(_address, _amount)
    end
    

如你所见，我们在圆括号内提供了键和值，因为需指定被更新的值和键。

然后在状态变量中读取：

    @view
    func read_balance{
          syscall_ptr : felt*,
          pedersen_ptr : HashBuiltin*,
          range_check_ptr
        }(_address: felt, _amount: felt):
        balance.read(_address)
    end
    

在这里只使用了键 (address)，因为我们只想获得该特定键的值，而不是更新或改变它。因此，使用 balance.read(\_address)，返回的是那个特定地址的数量或余额。

在理解了上述知识点后，直接进入 Starklings 的实操测试吧！

storage01.cairo
---------------

![](https://storage.googleapis.com/papyrus_images/709fd8bcfb710db1a802db339b48a2471725aecd0539c755e24aba181062a165.png)

在这里，我们要创建名为 **bool** 的存储并存入单个 felt。

它类似第一个 id 的存储示例。因此，我们要创建 bool 状态变量的方法：

    @storage_var
    func bool () -> (value: felt):
    end
    

![](https://storage.googleapis.com/papyrus_images/5bd4345b1edd53a39f2e1e602839dba1f8baa154ede794cd4ac1a07d15172747.png)

看看是否通过测试…

![](https://storage.googleapis.com/papyrus_images/451d9c4feff143ecbfa9d22d1c1718fb764bb8e430f8da1e1a1b1e125932f6ea.png)

成功！进入下一步！

storage02.cairo
---------------

这一步我们将探索如何存储结构 (Structs)

![](https://storage.googleapis.com/papyrus_images/b588ca05fc961cc9c90a4f64854491d71f9def6060fe8cec8b8ae2ddbc3edf60.png)

三个步骤：

创建一个名为 **wallet** 的存储，将一个 felt 映射到另一个。

类似于我们在 **balance** 状态变量（键到值的映射）中所做的：

    @storage_var
    func wallet (id: felt) -> (amount: felt):
    end
    

创建一个名为 **height\_map** 的存储，将两个 felt 映射到另一个。

同样类似 **balance** 状态变量时所做的，不同的是将两个键映射到一个值。

    @storage_var
    func height_map (length: felt, width: felt)  -> (height: felt):
    end
    

最后一步有点难度，需要将一个 felt，映射到一个 Id (struct)。用户值是一个 Id 类结构。

    @storage_var
    func id (address: felt) -> (user: Id):
    end
    

![](https://storage.googleapis.com/papyrus_images/90fb184b347c1887529106316427722f995df9e1c38fcb248e19bf691a3ea635.png)

成功运行！

![](https://storage.googleapis.com/papyrus_images/a4f761b6ac3c97e6dc1598750d432a93c0a13a13f3c87ff186b6b259cd81d38f.png)

storage03.cairo
---------------

![](https://storage.googleapis.com/papyrus_images/e587a03cf9ba94cb4411fdc68fa52b512482d4fe0685b0a9ea39881963757022.png)

这里要求实现 external 和 view 函数读取和写入 **bool** 状态变量。

上文所述，当一个函数写入一个存储变量时，作为外部函数应该用 **@external** 装饰器来指定，而当从存储变量中读取，它是视图函数，应该用 **@view** 修饰器来指定。

第一个需要修改的函数是 **toggle 函数**，它应该在调用时更新 **bool** 状态变量。

问题是一个布尔值 (Boolean Value) 只能为 0 或 1，所以需要通过以下方式实现，即每次调用 **toggle** 函数时，如果布尔值是 0，我们就将其更新为 1，反之亦然。所以建议我们使用 **conditionals**：

    @external
    func toggle{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}():
      let (value) = bool.read()
      if value == 0:
        bool.write(1)
      else:
        bool.write(0)
      end
      return () 
    end
    

接下来，我们需要修改 **view\_bool** 函数在调用时返回 **bool** 状态变量的值。

    @view
    func view_bool{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}() -> (bool : felt):
      let (value) = bool.read()
      return (value)
    end
    

完整代码如图：

![](https://storage.googleapis.com/papyrus_images/d71c562bc004d8910024c46ab34ad3d0dade702c46c5b0ec59fe06e7de10bfe8.png)

成功通过！

最后
--

恭喜你已经掌握了 Cairo 的存储！

如果你在实操练习中遇到了困难，可以在我的 [repo](https://github.com/Darlington02/starklings-article-solutions) 里找到练习的答案。

在下一课中，我们将研究**隐式参数 (Implicit Arguments)**。

如果觉得本教程对你有帮助，转发分享给其他人吧~

---

*Originally published on [Starknet 中文](https://paragraph.com/@starknet-zh/cairo-iv-starklings-cairo)*
