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

作者:Darington Nnam
原文:Journey through Cairo IV — A deep dive into Cairo’s Storage with Starklings
翻译:Louis Wang
校对:「StarkNet 中文社区」

欢迎来到「Cairo 之旅」第四讲,在上一讲中,我们学习了Cairo 中的 felt 数据类型和短字符串(Strings)。

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

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

Cairo 数据存储

在这个系列的第二讲中,我们学习区分 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

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

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

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

看看是否通过测试…

成功!进入下一步!

storage02.cairo

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

三个步骤:

创建一个名为 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

成功运行!

storage03.cairo

这里要求实现 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

完整代码如图:

成功通过!

最后

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

如果你在实操练习中遇到了困难,可以在我的 repo 里找到练习的答案。

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

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

Subscribe to Starknet 中文
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.