Diamond

Posted on Oct 26, 2022Read on Mirror.xyz

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)

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