The Cairo Programming Language

由 Cairo 社区和它的 贡献者 编写。特别感谢 Starkware 通过 OnlyDust 对本书的支持,也感谢 Voyager 对本书创作的支持。

本文假定您使用的是 Cairo v1.0.0-alpha.7 (released 2023-04-13) 版本的 Cairo 工具链。您可以参阅第一章的 安装 部分来更新或安装 Cairo 工具链。

Cairo-Book_Chinese

CC BY 4.0 GitHub stars GitHub watchers

Cairo-Book中文翻译

Cairo是一个图灵完备的用于通用可证明计算的编程语言,也是编写Starknet智能合约的语言。

Cairo最新版本为 1.0,基于Rust,并且在快速迭代。

cairo-book.github.io 是一个项目页面,旨在为Cairo这个智能合约开发语言提供文档、教程和示例代码等资源。

本项目为该项目的中文翻译项目

目录

导论

前言

第一章

❤️ 项目贡献者

永远感谢他们为本项目所作出的贡献!

contrib graph

💐 赞助我们

🪙 0xBBc1fE874422F61fB135e72C3229Fffc3Cb266Fb

您的姓名或昵称将出现在赞助榜界面上。

感谢您对我们社区未来健康发展所作出的支持

👏🏻 特别感谢

特别感谢我们的Premium Sponser

abetterweb3

😃 关于我们

这里是中国Web3知识协会,成立于2023年2月7日

(也许是,也希望未来是)中国最早的Web3社群

我们建立的初衷是因为创始人烟波,苦于中文互联网中的 Web3 相关知识分享内容相对较少,而为了改变这一现状,希望未来能整合和拓宽这类方向相关的 中文知识内容,从而建立了该组织。

社群宗旨

永远关注知识和技术的进步,而不是价格

在此,我们希望为所有的对Web3未来感兴趣和欲为其“添砖加瓦”的朋友们一起,创造出更美好的Web3未来前景!

(详见关于我们

⚠️ 免责声明

This web page "Cairo-Book_Chinese" and any other contents published on this website shall not constitute investment advice, financial advice, trading advice, or any other kind of advice, and you should not treat any of the website's content as such.

You alone assume the sole responsibility of evaluating the merits and risks associated with using any information or other content on this website before making any decisions based on such information.

The organization that developed this page, "Web3Club", is currently a non-profit open source community, not a company or corporationand.

All translations of the project were developed by members and contributors to the project, and any content in the project is protected by an open source licence.

We always open source the original open source project in accordance with the license of the original open source project.

When we use the knowledge content of other projects, we will follow the open source LICENCE of other projects.

And in accordance with the requirements of the licence,the information of the original English project or the original author will be indicated in the following sections.

If you have any questions about licence or copyright, please read the LICENCE section below or contact us at web3clubCN@outlook.com.

📖 LICENCE

Attribution-ShareAlike 4.0 International

Built by China Web3-Club contributors with heart.
Copyright © cairo-book.github.io
Chinese Translation copyright © 2023   China Web3-Club|中国Web3社区
ALL RIGHT RESERVED

前言

在 2020 年,StarkWare 发布了 Cairo 0,这是一种支持可验证计算的图灵完备的编程语言。最初,Cairo 是一门汇编语言,随着语法元素的增加逐渐变得更具表现力。但 Cario 0.x 属于低级语言,其没有完全抽象出为程序执行建立证明所需要的密码学原语,这导致 Cairo 0 的学习曲线非常陡峭。

随着 Cairo 1 的发布,开发者的体验获得了相当高的提升。这是因为 Cairo 1 尽可能抽象了 Cairo 架构中的不可变内存模型。受到 Rust 语言极大的启发,Cairo 1 的目标是帮助开发者在不了解底层架构的背景下构造可证明的程序,这使得开发者更加专注于程序本身,从而尽可能提高 Cairo 的整体安全性。得益于 Rust VM ,Cairo 程序的执行速度快得惊人,允许开发者在不影响性能的情况下构造大量的测试。

导论

什么是Cairo?

Cairo是为同名虚拟CPU设计的编程语言。该处理器的独特之处在于,它不是针对我们世界的物理约束而创建的,而是针对加密需求而设计的,因此能够有效地证明其上运行的任何程序的执行情况。这意味着您可以在一个不信任的机器上执行耗时的操作,并在更便宜的机器上快速检查结果。尽管Cairo 0曾经直接编译为Cairo CPU汇编语言(CASM),但Cairo 1是一种更高级的语言。它首先编译为Sierra,Cairo的中间表示形式,稍后将编译为CASM的安全子集。Sierra的作用是确保您的CASM始终可证明,即使计算失败。

你可以用它做什么?

Cairo允许您在不受信任的机器上计算可信的值。其一个主要用例是Starknet,一种以太坊扩容的解决方案。以太坊是一种去中心化的区块链平台,使得可以创建所有参与者都验证用户和d-app之间的每次交互的去中心化应用程序。Starknet是建立在以太坊之上的第二层。它不需要网络的所有参与者来验证所有用户交互,只有一个节点(称为证明者)执行程序并生成证据证明计算是正确的。然后智能合约验证这些证据,相比于执行交互本身,需要的计算能力大大降低。这种方法允许增加吞吐量和减少交易成本,同时保持以太坊的安全性。

与其他编程语言相比有什么区别?

Cairo与传统编程语言有很大不同,尤其是在开销和其主要优点方面。您的程序可以通过两种不同的方式执行:

当由证明者执行时,它类似于任何其他语言。因为Cairo是虚拟化的,并且操作并非专门设计用于最大效率,这可能会导致一些性能开销,但这并不是最重要的部分需要进行优化。

当生成的证据由验证者验证时,则稍微有些不同。这必须尽可能便宜,因为它可能在许多非常小的机器上进行验证。幸运的是,验证比计算快,而Cairo具有一些独特的优势,可以进一步改善。一个值得注意的例子是非确定性。这是本书中将在更多细节中介绍的主题,但基本意思是理论上可以使用不同的算法进行验证和计算。目前,为开发人员编写自定义的非确定性代码不受支持,但标准库利用非确定性来提高性能。例如,在Cairo中对数组排序的成本与复制相同。由于验证者不会对数组进行排序,它只检查它是否排序,这就更便宜了。

另一个使该语言独特的方面是其内存模型。在Cairo中,内存访问是不可变的,意味着一旦将值写入内存,它将无法更改。Cairo 1提供了帮助开发人员处理这些约束的抽象,但它并没有完全模拟可变性。因此,开发人员必须仔细考虑如何管理他们程序中的内存和数据结构以优化性能。

此外,Cairo还有一些其他特点:

  • 静态类型:与许多现代编程语言一样,Cairo是一种静态类型的语言。这意味着变量必须在使用之前声明,并且它们只能被分配给其声明类型的值。
  • 无限制的精度算术:Cairo中的数学运算不受硬件规定的限制,因此可以进行以任意精度执行。
  • 内存安全:由于内存访问是不可变的,Cairo几乎可以完全避免许多常见的内存错误,例如缓冲区溢出和悬空指针。

总体而言,Cairo是一种专门设计用于加密需求的编程语言,并且具有独特的优势,使其成为构建安全和高效计算的理想选择。

参考文献

  1. Cairo CPU Architecture: https://eprint.iacr.org/2021/1063
  2. Cairo、Sierra 和 Casm:https://medium.com/nethermind-eth/under-the-hood-of-cairo-1-0-exploring-sierra-7f32808421f5
  3. 非确定性的状态:https://twitter.com/PapiniShahar/status/1638203716535713798

请注意,第三个参考文献似乎是一条推文的链接,因此我提供了一个指向该推文的链接以供参考。如果需要翻译,请复制并使用您选择的在线翻译工具对其进行翻译。

入门

安装

安装Cairo的第一步是从cairo存储库手动下载或使用安装脚本进行下载。您需要互联网连接以进行下载。

先决条件

首先,您需要安装Rust和Git。如果您没有安装它们,可以通过运行以下命令来安装它们:

# 安装稳定版Rust
rustup override set stable && rustup update

然后,安装Git。

使用脚本(由Fran编写的安装程序)安装Cairo

要使用Installer by Fran脚本安装Cairo,请运行以下命令:

curl -L https://github.com/franalgaba/cairo-installer/raw/main/bin/cairo-installer | bash

如果你想要安装特定版本的Cairo而不是最新版本,则设置 CAIRO_GIT_TAG 环境变量 (例如:export CAIRO_GIT_TAG=v1.0.0-alpha.6)。

安装Cairo后,请按照以下说明设置您的shell环境。

更新

要更新Cairo,请运行以下命令:

rm -fr ~/.cairo
curl -L https://github.com/franalgaba/cairo-installer/raw/main/bin/cairo-installer | bash

卸载

要卸载Cairo,请删除 $CAIRO_ROOT 目录,该目录通常为 ~/.cairo

从您的 .bashrc 文件中删除以下行:

export PATH="$HOME/.cairo/target/release:$PATH"

然后重启您的shell:

exec $SHELL

为Cairo设置您的Shell环境

将环境变量 CAIRO_ROOT 定义为指向Cairo存储其数据的路径。默认值为 $HOME/.cairo,但如果您通过Git checkout安装了Cairo,则建议将其设置为与克隆它的位置相同的位置。

如果尚未添加 cairo-* 可执行文件,请将它们添加到您的 PATH 中。按照下面的说明为您的shell进行设置。

对于Bash

echo 'export CAIRO_ROOT="$HOME/.cairo"' >> ~/.bashrc
echo 'command -v cairo-compile >/dev/null || export PATH="$CAIRO_ROOT/target/release:$PATH"' >> ~/.bashrc

如果您有 .profile.bash_profile.bash_login 文件,请在其中添加上述命令。如果没有这些文件之一,请将它们添加到您的.profile 中。

echo 'export CAIRO_ROOT="$HOME/.cairo"' >> ~/.profile
echo 'command -v cairo-compile >/dev/null || export PATH="$CAIRO_ROOT/target/release:$PATH"' >> ~/.profile

对于Zsh

echo 'export CAIRO_ROOT="$HOME/.cairo"' >> ~/.zshrc
echo 'command -v cairo-compile >/dev/null || export PATH="$CAIRO_ROOT/target/release:$PATH"' >> ~/.zshrc

如果您希望非交互式登录shell中使用Cairo,请还将这些命令添加到您的 .zprofile.zlogin 中。

对于Fish

对于 Fish 3.2.0 或更新版本:

set -Ux CAIRO_ROOT $HOME/.cairo
fish_add_path $CAIRO_ROOT/target/release

否则:

set -Ux CAIRO_ROOT $HOME/.cairo
set -U fish_user_paths $CAIRO_ROOT/target/release $fish_user_paths

在MacOS上,您可能还想安装一个名为 Fig 的工具,它为许多命令行工具提供替代的shell补全功能,并在终端窗口中提供类似于IDE的弹出式界面。

手动安装Cairo(由Abdel编写的指南)

步骤1:安装Cairo 1.0

如果您使用的是x86 Linux系统并且可以使用发布二进制文件,请从此处下载Cairo:https://github.com/starkware-libs/cairo/releases。

以下是安装Cairo的指南:

步骤1:从源代码编译Cairo

首先,您需要定义环境变量CAIRO_ROOT,可以通过以下命令进行设置:

export CAIRO_ROOT="${HOME}/.cairo"

如果该文件夹不存在,则需创建.cairo文件夹:

mkdir $CAIRO_ROOT

然后将Cairo编译器克隆到$CAIRO_ROOT目录下(默认根目录):

cd $CAIRO_ROOT && git clone git@github.com:starkware-libs/cairo.git .

如果您想要安装特定版本的编译器,可以使用以下命令查看可用的版本:

git fetch --all --tags
git describe --tags `git rev-list --tags`

然后,使用git checkout命令检出所需版本。例如,对于v1.0.0-alpha.6版本,可以执行以下命令:

git checkout tags/v1.0.0-alpha.6

最后,生成发布二进制文件:

cargo build --all --release

注意事项:保持Cairo更新

由于您已经在本地克隆了Cairo编译器的存储库,所以只需使用以下命令即可拉取最新更改并重新构建:

cd $CAIRO_ROOT && git fetch && git pull && cargo build --all --release

步骤2:将Cairo 1.0可执行文件添加到PATH

使用以下命令将Cairo 1.0的可执行文件添加到$PATH环境变量中:

export PATH="$CAIRO_ROOT/target/release:$PATH"

注意:如果您使用Linux二进制文件进行安装,则需相应地调整目标路径。

步骤3:设置语言服务器

VS Code扩展程序

在进行以下步骤之前,请禁用先前的Cairo 0.x扩展程序。然后按照以下步骤安装Cairo 1扩展程序以获得正确的语法高亮和代码导航:

  1. 打开VS Code并转到菜单“View”,然后选择“Extensions”;
  2. 在搜索栏中输入“Cairo”;
  3. 选择“Cairo Language Support”;
  4. 点击“Install”按钮。

Cairo语言服务器

在完成步骤1后,您将已经构建了cairo-language-server二进制文件。执行以下命令将其路径复制到剪贴板中(注意:以下命令适用于macOS系统):

which cairo-language-server | pbcopy

然后,将该路径粘贴到Cairo 1.0扩展程序的languageServerPath设置中。

Hello, World!

现在你已经安装了Cairo,是时候编写你的第一个Cairo程序了。在学习一门新语言时,往往会写一个简单的程序,在屏幕上输出“Hello, world!”这句话,因此我们也将在这里完成相同的任务!

注意:本书假定读者熟悉基本的命令行操作。Cairo对代码编辑或工具使用没有特别的要求,你可以使用自己喜欢的集成开发环境(IDE)代替命令行。Cairo团队已经为Cairo语言开发了一个VSCode插件,您可以使用它来获得来自语言服务器和代码高亮的功能。有关更多详细信息,请参见附录A。

创建项目目录

首先,需要创建一个用于存储Cairo代码的目录。Cairo并不关心代码存放的位置,但是对于本书中的练习和项目,建议在用户主目录下创建一个cairo_projects目录,并在其中保存所有项目。

打开一个终端窗口,输入以下命令,以创建cairo_projects目录和cairo_projects目录中的“Hello, world!”项目目录:

  • 对于Linux、macOS和PowerShell on Windows,请输入以下命令:

    mkdir ~/cairo_projects
    cd ~/cairo_projects
    mkdir hello_world
    cd hello_world
    
  • 对于Windows CMD,请输入以下命令:

    > mkdir "%USERPROFILE%\projects"
    > cd /d "%USERPROFILE%\projects"
    > mkdir hello_world
    > cd hello_world
    

编写和运行Cairo程序

接下来,创建一个名为main.cairo的新源文件。Cairo文件总是以.cairo扩展名结尾。如果文件名中包含多个单词,则惯例是使用下划线来分隔它们。例如,使用hello_world.cairo而不是helloworld.cairo

现在打开刚刚创建的main.cairo文件,并输入清单1-1中的代码。

文件名:main.cairo

use debug::PrintTrait;
fn main() {
    'Hello, world!'.print();
}

清单1-1:一个打印“Hello, world!”的程序。

保存文件并回到~/cairo_projects/hello_world目录的终端窗口。输入以下命令来编译和运行文件:

$ cairo-run main.cairo
Hello, world!

无论您使用的是哪种操作系统,都应该在终端上输出字符串“Hello, world!”。

如果“Hello, world!”已经被打印出来,恭喜你!你已经正式编写了一个Cairo程序。这意味着你成为了一个Cairo程序员——欢迎加入!

Cairo程序的解剖

让我们详细查看这个“Hello, world!”程序。这是谜题的第一部分:

fn main() {

}

这些行定义了一个名为main的函数。main函数很特殊:它是每个可执行Cairo程序中第一个运行的代码。在这里,第一行声明了一个名为main的函数,它没有参数并且不返回任何值。如果有参数,它们将放在括号() 中。

函数体包含在{}中。Cairo需要在所有函数体周围使用花括号。好的习惯是将左花括号与函数声明放在同一行,在它们之间添加一个空格。

**注意:如果您希望在不同的Cairo项目中使用标准样式,可以使用自动格式化工具cairo-format以特定的风格格式化代码(有关cairo-format的更多信息请参见附录A)。Cairo团队已经将这个工具与标准Cairo发行版一起提供了,就像cairo-run一样,所以它应该已经安装在您的计算机上!

main函数声明之前,行use debug::PrintTrait;负责导入PrintTrait trait。trait定义了一组方法或函数,可以应用于特定类型的值。这里,我们使用PrintTrait trait来打印字符串。

main函数的函数体中,我们使用单引号将“Hello, world!”字符包裹在内部。单引号表示一个字符,双引号表示一个字符串。然后我们调用print()方法来打印出这个字符串。

这就是整个程序的全部内容。虽然看起来很简单,但它已经涵盖了许多基本概念,例如如何声明一个函数、如何使用trait和如何使用字符串和字符。这些都是编程中必不可少的基本概念,它们会在Cairo中反复出现。

继续学习Cairo

恭喜!您已经成功地编写并运行了第一个Cairo程序。现在你已经有了一些关于Cairo的基本知识,建议你继续阅读本书,学习更多高级的Cairo编程概念。祝你好运!

Hello, Scarb!

Scarb 包管理器

Scarb 是Cairo的包管理器,受到 Rust 构建系统和包管理器 Cargo 的启发。它可以处理诸如构建代码、下载所需库以及构建这些库等多个任务。

安装

要求

在安装之前,需要确保 Git 可执行文件已经被添加到 PATH 环境变量中。

安装步骤

目前,Scarb 需要手动安装,以下是安装步骤:

  1. Scarb releases on GitHub 下载与您的操作系统和 CPU 架构相匹配的发布存档。
  2. 将其提取到您希望安装 Scarb 的位置,例如 ~/scarb
  3. 将 Scarb 的二进制可执行文件路径添加到 PATH 环境变量中。具体方法根据您使用的 shell 不同而不同。以 zsh 为例,假设您将 Scarb 提取到了 ~/scarb,则在 ~/.zshrc 文件末尾添加以下行:export PATH="$PATH:~/scarb/bin"
  4. 在新终端会话中运行以下命令来验证安装:scarb --version,它应该会打印出 Scarb 和 Cairo 语言版本信息。

使用 Scarb 创建项目

让我们使用 Scarb 创建一个新项目,并查看它与最初的 "Hello, world!" 项目有何不同。

首先进入您的项目目录(或者您想存储代码的任何位置),然后执行以下命令:

scarb new hello_scarb

这将在当前目录下创建一个名为 hello_scarb 的新项目。运行此命令后,会生成两个文件和一个目录:一个 Scarb.toml 文件、包含 lib.cairo 文件的 src 目录,以及一个 .gitignore 文件。同时,它还会初始化一个新的 Git 存储库。

打开 Scarb.toml 文件,您将看到 TOML 格式的配置信息,用于设置项目的名称和版本号,并列出任何依赖项。对于我们的 "Hello, world!" 项目,我们不需要任何其他依赖项,因此可以保留默认值。

在 src 目录中,存在一个空的 lib.cairo 文件。让我们清空其内容,并添加以下一行代码:

mod hello_scarb;

稍后,我们将解释此代码的含义和用途。

我们可以创建一个名为src/hello_scarb.cairo的新文件,并将以下代码放入其中:

文件名:src/hello_scarb.cairo

use debug::PrintTrait;
fn main() {
    'Hello, Scarb!'.print();
}

我们刚刚创建了一个名为lib.cairo的文件,其中包含一个模块声明引用了另一个名为“hello_scarb”的模块,以及包含“hello_scarb”模块的实现细节的文件hello_scarb.cairo。

Scarb要求你的源代码文件位于src目录中。

顶层项目目录保留用于README文件、许可信息、配置文件和任何其他非代码相关内容。Scarb确保为所有项目组件指定位置,保持结构化组织。

如果你启动的项目不使用Scarb,就像我们使用“Hello, world!”项目一样,你可以将其转换为使用Scarb的项目。将项目代码移动到src目录中并创建一个合适的Scarb.toml文件即可。

构建Scarb项目

从你的hello_scarb目录中,输入以下命令构建你的项目:

$ scarb build
   Compiling hello_scarb v0.1.0 (file:///projects/Scarb.toml)
    Finished release target(s) in 0 seconds

此命令在target/release中创建一个sierra文件,现在我们先忽略sierra文件。

如果你正确安装了Cairo,你应该能够运行并看到以下输出:

$ cairo-run src/lib.cairo
[DEBUG] Hello, Scarb!                   (raw: 5735816763073854913753904210465)

运行成功,返回[]。 注意:你会注意到这里我们没有使用Scarb命令,而是直接使用了Cairo二进制文件中的命令。由于Scarb尚未具有执行Cairo代码的命令,因此我们必须直接使用cairo-run命令。在本教程的其余部分中,我们将使用该命令,但我们也将使用Scarb命令来初始化项目。

定义自定义脚本

我们可以在Scarb.toml文件中定义Scarb脚本,用于执行自定义shell脚本。将以下行添加到Scarb.toml文件中:

[scripts]
run-lib = "cairo-run src/lib.cairo"

现在你可以运行以下命令来运行该项目:

$ scarb run run-lib
[DEBUG] Hello, Scarb!                   (raw: 5735816763073854913753904210465)

运行成功,返回[]。

使用scarb run是一种方便的方式来运行自定义shell脚本,它可以帮助你运行文件并测试你的项目。

让我们总结一下我们已经学到的Scarab知识:

  • 我们可以使用scarb new创建一个项目。
  • 我们可以使用scarb build构建项目以生成编译后的Sierra代码。
  • 我们可以在Scarb.toml中定义自定义脚本,并使用scarb run命令调用它们。
  • 使用Scarb的另一个优点是,无论你在哪个操作系统上工作,命令都是相同的。因此,此时我们将不再为Linux和macOS与Windows提供特定的说明。

总结

你已经开始了Cairo之旅!在本章中,你学会了:

  • 安装最新稳定版的Cairo。
  • 使用cairo-run直接编写和运行“Hello, world!”程序。
  • 使用Scarb惯例创建和运行一个新项目。

现在是一个建立更加实质性的程序来逐渐熟悉阅读和编写Cairo代码的好时机。

Common Programming Concepts

变量和可变性

Cairo使用不可变的内存模型,这意味着一旦写入内存单元,就无法覆盖而只能从中读取。为了反映这种不可变的内存模型,Cairo中的变量默认是不可变的。但是,该语言抽象了这种模型,并为您提供了使变量可变的选项。让我们探讨一下Cairo如何强制实行不可变性,以及如何使变量可变。

当变量是不可变的时,一旦将一个值绑定到一个名称,就无法更改该值。为了说明这一点,在cairo_projects目录中生成一个名为variables的新项目,使用命令scarb new variables。

然后,在您的新variables目录中,打开src/lib.cairo文件并将其代码替换为以下代码,该代码目前无法编译:

    use debug::PrintTrait;
    fn main() {
        let x = 5;
        x.print();
        x = 6;
        x.print();
    }

保存并使用cairo-run src/lib.cairo运行程序。您应该会收到有关不可变性错误的错误消息,如以下输出所示:

    error: Cannot assign to an immutable variable.
     --> lib.cairo:5:5
        x = 6;
        ^***^

    Error: failed to compile: src/lib.cairo

该示例演示了编译器如何帮助您查找程序中的错误。编译器错误可能让人沮丧,但它们只意味着您的程序尚未安全地执行所需的操作;它们不意味着您不是一个好的程序员!经验丰富的Caironautes仍然会出现编译器错误。

您收到了错误消息 Cannot assign to an immutable variable. ,因为您尝试为不可变x变量分配第二个值。

当我们试图更改被指定为不可变的值时,获得编译时错误非常重要,因为这种情况可能导致错误。如果我们的代码的一部分在假设一个值永远不会改变,而我们的代码的另一部分更改了该值,那么这种错误的原因可能在事后很难追踪,特别是当第二段代码只有有时更改该值时。 Cairo编译器保证了当您声明一个值不会改变时,它确实不会改变,因此您不必自己跟踪它。因此,您的代码更容易推理。

但是,可变性非常有用,可以使代码更方便。尽管变量默认情况下是不可变的,但是可以在变量名称前添加mut来使它们可变。添加mut还通过指示代码的未来读者将更改该变量的值来传达意图。

然而,此时您可能会想知道当一个变量被声明为mut时到底发生了什么,因为我们之前提到Cairo的内存是不可变的。答案是,Cairo的内存是不可变的,但是变量指向的内存地址可以更改。在检查低级Cairo汇编代码时,变量突变被实现为语法糖,它将变异操作转换为等效于变量遮蔽的一系列步骤。唯一的区别是在Cairo级别,变量没有被重新声明,因此它的类型不能改变。

例如,让我们将src/lib.cairo更改为以下内容:

    use debug::PrintTrait;
    fn main() {
        let mut x = 5;
        x.print();
        x = 6;
        x.print();
    }

现在运行该程序,我们会得到以下输出: [DEBUG] (raw: 5)

    [DEBUG]                                (raw: 6)
    
    Run completed successfully, returning []

当使用mut时,我们被允许将x绑定的值从5更改为6。最终,决定是否使用可变性取决于您,这取决于您认为在特定情况下最清晰的内容。

常量

与不可变变量一样,常量是将值绑定到名称并且不允许更改的值,但常量和变量之间有一些区别。

首先,您不允许使用mut来定义常量。常量默认情况下是不可变的 - 它们始终是不可变的。您可以使用const关键字而不是let关键字来声明常量,值的类型必须用注释标注。我们将在下一节“数据类型”中介绍类型和类型注释,所以现在不必担心细节。只需知道您必须始终注释类型。 常量只能在全局范围内声明,这使它们对于许多代码部分需要了解的值非常有用。 最后一个区别是常量只能设置为常量表达式,而不能设置为仅在运行时才能计算的值的结果。当前仅支持字面量常量。 以下是常量声明的示例:

const ONE_HOUR_IN_SECONDS: u32 = 3600;

Cairo对常量的命名惯例是使用所有大写字母,并在单词之间使用下划线。 常量对于在程序运行的整个时间内都有效,在声明它们的作用域内是非常有用的。这个属性使得常量对于应用程序域中的值对于程序的多个部分可能需要了解的情况非常有用,比如一种游戏中任何玩家被允许获得的最大分数,或者光速的速度等等。 将硬编码值用作常量在传达该值的含义方面对于未来代码维护者非常有用。如果硬编码值需要在未来更新,则只需要更改代码中的一个地方即可。 变量屏蔽 变量屏蔽是指使用与先前变量相同的名称声明新变量。 Caironautes说第一个变量被第二个变量遮盖,这意味着当您使用变量名时,编译器将看到第二个变量。实际上,第二个变量超越第一个变量,使变量名称的任何使用为自己,直到它自己被屏蔽或作用域结束。我们可以使用相同变量的名称并重复使用let关键字来遮罩变量,如下所示: 文件名:src/lib.cairo

use debug::PrintTrait;
fn main() {
    let x = 5;
    let x = x + 1;
    {
        let x = x * 2;
        'Inner scope x value is:'.print();
        x.print()
    }
    'Outer scope x value is:'.print();
    x.print();
}

此程序首先将x绑定到值5,然后通过重复 let x = 创建新变量x,取原始值并将其加1,以便x的值为6。然后,在用花括号创建的内部范围内,第三个let语句也遮盖了x并创建了一个新变量,将以前的值乘以2,以使x的值为12。当该范围结束时,内部遮盖将结束,x将返回到6。当我们运行这个程序时,它会输出以下内容:

cairo-run src/lib.cairo
[DEBUG] Inner scope x value is:         (raw: 7033328135641142205392067879065573688897582790068499258)

[DEBUG]
                                        (raw: 12)

[DEBUG] Outer scope x value is:         (raw: 7610641743409771490723378239576163509623951327599620922)

[DEBUG]                                 (raw: 6)

运行成功完成,返回[] 变量屏蔽与将变量标记为mut不同,因为如果意外尝试重新分配给此变量而不使用let关键字,我们将获得编译时错误。使用let,我们可以对值执行一些转换,但在完成这些转换后,变量必须是不可变的。 mut和遮蔽之间的另一个区别是,当我们再次使用let关键字时,我们实际上正在创建一个新变量,这允许我们改变值的类型,同时重用相同的名称。如前所述,变量屏蔽和可变变量在较低级别上是等效的。唯一的区别是通过变量屏蔽,如果您更改其类型,编译器不会抱怨变量。例如,假设我们的程序在u64和felt252类型之间执行类型转换。

    let x = 2;
    x.print();
    let x: felt252 = x.into(); // converts x to a felt, type annotation is required.
    x.print()

第一个 x 变量具有 u64 类型,而第二个变量 x 具有 felt252 类型。因此,使用变量屏蔽可以避免我们必须想出不同的名称,例如 x_u64 和 x_felt252;相反,我们可以重用更简单的 x 名称。然而,如果我们尝试在此处使用 mut,则会在编译时出现错误:

    use debug::PrintTrait;
    use traits::Into;
    fn main() {
        let mut x = 2;
        x.print();
        x = x.into();
        x.print()
    }

错误信息表明我们期望取得一个 u64(原始类型),但我们获得了一个不同的类型:

    error: Unexpected argument type. Expected: "core::integer::u64", found: "core::felt252".
     --> lib.cairo:6:9
        x = x.into();
            ^******^
    Error: failed to compile: src/lib.cairo

现在,我们已经探讨了变量的工作方式,让我们来看一下它们可以具有的更多数据类型。

数据类型

Cairo中的每个值都是某种数据类型,这告诉Cairo正在指定哪种数据,以便它知道如何处理该数据。这一节涵盖了两个数据类型的子集:标量和复合数据类型。

请记住,Cairo是一种静态类型语言,这意味着在编译时必须确定所有变量的类型。编译器通常可以根据值及其用法推断出所需的类型。在许多类型可能的情况下,我们可以使用一个转换方法,其中指定所需的输出类型。例如,下面代码使用TryInto和OptionTrait trait将felt252类型转换为u32类型:

use traits::TryInto; use option::OptionTrait; fn main() { let x: felt252 = 3; let y: u32 = x.try_into().unwrap(); }

您将看到其他数据类型的不同类型注释。

标量数据类型

标量类型表示单个值。Cairo有三种基本标量类型:felts、整数和布尔值。您可能会从其他编程语言中认识到它们。让我们看看它们在Cairo中的工作原理。

Felt类型

在Cairo中,如果您没有指定变量或参数的类型,它的类型默认为一个位元素,表示为关键字felt252。在Cairo的上下文中,当我们说“一个位元素”时,我们意思是一个在范围0 <= x < P内的整数,其中P是一个非常大的素数,当前等于P = 2^{251} + 17 * 2^{192}+1。当执行加法、减法或乘法时,如果结果超出了指定范围的素数范围,将发生溢出,并添加或减去适当的P倍数,以将结果带回范围内(即,结果计算为模P)。

整数和字段元素之间最重要的区别是除法:字段元素的除法(因此在Cairo中的除法)与常规CPU的除法不同,其中整数除法x / y被定义为[x/y],其中返回商的整数部分(因此您获得7/3=2),并且它可能或可能不满足方程(x / y)*y == x,取决于x是否可被y整除。在Cairo中,x/y的结果被定义为始终满足方程(x / y)*y == x。如果y被作为整数划分x,您将在Cairo中得到预期结果(例如6/2确实会产生3)。但是当y不能整除x时,您可能会得到意想不到的结果。例如,由于2 * ((P+1)/2) = P+1 ≡ 1 mod[P],在Cairo中,1/2的值为(P+1)/2(而不是0或0.5),因为它满足上述方程式。

整数类型

felt252类型是一种基本类型,用作创建核心库中所有类型的基础。然而,强烈建议程序员只要可能就使用整数类型而不是felt252类型,因为整数类型带有附加的安全功能,可以提供额外的保护以防止代码中潜在的漏洞,例如溢出检查。通过使用这些整数类型,程序员可以确保他们的程序更加安全,并且不太容易受到攻击或其他安全威胁的影响。整数是没有分数部分的数字。此类型声明指示程序员可以用来存储整数的比特数。表3-1显示了Cairo中的内置整数类型。我们可以关于如何选择使用哪种类型的整数?尝试估计您的整数可以具有的最大值并选择合适的大小。使用usize的主要情况是在索引某种集合时。

数字运算

Cairo支持您预期的所有整数类型的基本数学运算:加法,减法,乘法,除法和余数运算。整数除法向最接近的整数向零截断。以下代码显示了您将如何在let语句中使用每个数值操作:

fn main() { // addition let sum = 5_u128 + 10_u128;

// subtraction
let difference = 95_u128 - 4_u128;

// multiplication
let product = 4_u128 * 30_u128;

// division
let quotient = 56_u128 / 32_u128; // result is 1
let quotient = 64_u128 / 32_u128; // result is 2

// remainder
let remainder = 43_u128 % 5_u128; // result is 3

}

这些语句中的每个表达式都使用数学运算符并评估为单个值,然后将其绑定到变量中。付录B包含Cairo提供的所有运算符的列表。

布尔类型

与大多数其他编程语言一样,Cairo中的布尔类型具有两个可能的值:true和false。布尔值的大小为一个felt252(注意和Python等其他语言可能不同)。Cairo中的布尔类型是使用bool指定的。例如:

fn main() { let t = true;

let f: bool = false; // with explicit type annotation

}

使用布尔值的主要方法是通过条件语句,例如if表达式。我们将在“控制流”部分中介绍if表达式的工作方式。

短字符串类型

Cairo没有原生的字符串类型,但可以将形成我们称之为“短字符串”的字符存储在felt252中。短字符串最大长度为31个字符。这是为了确保它可以适合单个felt中(一个felt为252位,一个ASCII字符为8位)。以下是在单引号之间放置的声明值的一些示例:

let my_first_char = 'C';
let my_first_string = 'Hello world';

类型转换

在Cairo中,可以使用TryInto和Into特征提供的try_into和into方法将标量类型从一种类型转换为另一种类型。在目标类型可能不适合源值的情况下,try_into方法允许安全类型转换。请记住,try_into返回Option 类型,您需要解包它以访问新值。另一方面,当成功保证时,例如当源类型小于目标类型时,可以使用into方法进行类型转换。要执行转换,请在源值上调用var.into()或var.try_into(),以将其转换为另一种类型。必须明确定义新变量的类型,如下例所示:

` use traits::TryInto; use traits::Into; use option::OptionTrait;

fn main() { let my_felt252 = 10; // Since a felt252 might not fit in a u8, we need to unwrap the Option type let my_u8: u8 = my_felt252.try_into().unwrap(); let my_u16: u16 = my_u8.into(); let my_u32: u32 = my_u16.into(); let my_u64: u64 = my_u32.into(); let my_u128: u128 = my_u64.into(); // As a felt252 is smaller than a u256, we can use the into() method let my_u256: u256 = my_felt252.into(); let my_usize: usize = my_felt252.try_into().要从元组中获取单个值,我们可以使用模式匹配来解构元组值,就像这样:

use debug::PrintTrait; fn main() { let tup = (500, 6, true);

let (x, y, z) = tup;

if y == 6 {
    'y is six!'.print();
}

}

该程序首先创建一个元组并将其绑定到变量tup。然后使用带有let的模式将tup变成三个单独的变量x、y和z。这称为解构,因为它将单个元组分成三个部分。最后,程序打印y is six,因为y的值为6。

我们还可以同时声明元组的值和类型。例如: fn main() { let (x, y): (i32, i32) = (2, 3); }

单元类型()

单元类型是仅有一个值()的类型。它表示一个没有元素的元组。它的大小始终为零,并且保证不会存在于编译后的代码中。

#函数

函数在Cairo代码中相当常见。您已经看到了语言中最重要的函数之一:main函数,它是许多程序的入口点。您也看到了fn关键字,它允许您声明新函数。

在Cairo代码中,通常将蛇式命名法作为函数和变量名称的惯用风格,其中所有字母都是小写的,下划线分隔单词。以下是包含示例函数定义的程序:

use debug::PrintTrait;

fn another_function() { 'Another function.'.print(); }

fn main() { 'Hello, world!'.print(); another_function(); }

在Cairo中通过输入fn后跟函数名称和一组括号来定义函数。花括号告诉编译器函数体的开始和结束位置。我们可以通过输入其名称后跟一组括号来调用我们定义的任何函数。因为another_function在程序中已定义,所以可以从main函数中调用它。请注意,我们将another_function定义放在了主函数之前的源代码中;我们也可以在之后定义它。Cairo不关心您定义函数的位置,只要它们在调用方可见的作用域中定义即可。

参数

我们可以定义具有参数的函数,这些参数是函数签名的一部分的特殊变量。当一个函数具有参数时,可以为其提供这些参数的具体值。从技术上讲,具体值称为参数,但在日常会话中,人们倾向于互换使用参数和参数这两个词,无论是函数定义中的变量还是在调用函数时传递的具体值。

在这个版本的another_function中,我们添加了一个参数:

use debug::PrintTrait;

fn main() { another_function(5); }

fn another_function(x: felt252) { x.print(); }

another_function的声明有一个名为x的参数。 x的类型指定为felt252。当我们向another_function传递5时,.print()函数在控制台中输出5。

在函数签名中,必须声明每个参数的类型。这是Cairo设计中的一个有意决策:在函数定义中要求类型注释意味着编译器几乎永远不需要在代码的其他地方使用它们来确定您所需的类型。如果编译器知道函数期望什么类型,它还能够提供更有用的错误消息。 在定义多个参数时,使用逗号分隔参数声明,例如:

use debug::PrintTrait;

fn main() { another_function(5, 6); }

fn another_function(x: felt252, y: felt252) { x.print(); y.print(); }

该例子创建了一个名为another_function的函数,并带有两个参数。第一个参数命名为x,是一个felt252。第二个参数命名为y,类型也为felt252。然后函数打印出了felt x和然后是felt y的内容。

尝试运行此代码。用前面的示例替换functions项目的src / lib.cairo文件,并使用cairo-run src / lib.cairo运行它:因为我们使用了5作为x的值,6作为y的值来调用函数,因此程序输出包含这些值。

语句和表达式

函数体由一系列语句组成,并以一个可选的表达式结束。到目前为止,我们涵盖的函数没有包含一个结束表达式,但是您已经在一句话中看到了一个表达式。因为Cairo是一种基于表达式的语言,所以了解这一点很重要。其他语言没有相同的区别,因此让我们看看语句和表达式是什么,以及它们的差异如何影响函数体。

•语句是执行某些操作并不返回值的指令。 •表达式求值为结果值。让我们来看一些例子。

实际上,我们已经使用了语句和表达式。使用let关键字创建变量并将其赋值是一个语句。在2-1清单中,let y = 6;是一条语句。

fn main() { let y = 6; }

函数定义也是语句;整个前面的例子本身就是一个语句。

语句不返回值。因此,您不能将let语句分配给另一个变量,正如以下代码尝试的那样;您将收到一个错误:

fn main() { let x = (let y = 6); }

当运行这个程序时,您会收到以下错误:

error: Missing token TerminalRParen. --> src/lib.cairo:2:14 let x = (let y = 6); ^

error: Missing token TerminalSemicolon. --> src/lib.cairo:2:14 let x = (let y = 6); ^

error: Missing token TerminalSemicolon. --> src/lib.cairo:2:14 let x = (let y = 6); ^

error: Skipped tokens. Expected: statement. --> src/lib.cairo:2:14 let x = (let y = 6);

let y = 6语句不返回值,因此没有任何东西可以绑定到x。这与其他语言(例如C和Ruby)的情况不同,其中分配返回分配的值。在这些语言中,您可以编写x = y = 6,并且x和y都具有值6;这在Cairo中不是这种情况。

表达式求值为一个值,并占据了你在Cairo中编写的大部分代码。考虑一个数学运算,例如5 + 6,这是一个表达式,其评估为值11。表达式可以是语句的一部分:在2-1清单中,在语句let y = 6;中的6是一个表达式,其评估为值6。调用函数是一种表达式。用花括号创建的新作用域块是一个表达式,例如:

use debug::PrintTrait; fn main() { let y = { let x = 3; x + 1 };

y.print();

}

这个表达式:

{ let x = 3; x + 1 }

是一个块,它在这种情况下计算为4。该值将作为let语句的一部分绑定到y。请注意,x + 1行没有分号结尾,这与您看到的大多数行不同。表达式不包括结束分号。如果在表达式的末尾添加分号,它将变成语句,然后它将不返回值。在您探索函数返回值和表达式之后,请记住这一点。

带返回值的函数

函数可以向调用它们的代码返回值。我们不给返回值命名,但必须在箭头(->)后声明其类型。在Cairo中,函数的返回值与函数体块中的最后一个表达式的值是同义的。您可以这是一些使用Cairo语言编写的代码。第一个程序中定义了一个返回5的函数,然后在main函数中调用该函数以初始化变量x,最终输出结果为5。第二个程序中定义了一个名称为plus_one的函数,它接受一个u32类型的参数,并返回将该参数加1后的结果。在main函数中,我们使用plus_one(5)来初始化变量x,并输出x的值,该值是6。但是,如果我们在函数中将x + 1的末尾加上分号,我们将得到编译错误,因为这会使该语句成为语句而非表达式,导致函数返回类型不匹配的错误。在这种情况下,编译器希望函数返回一个u32类型的值,但实际返回了一个unit类型的值(()),这与函数定义不符,导致出现错误。

Comments

控制流

控制流是大多数编程语言中的基本构建块,它允许根据条件运行一些代码,并在某个条件为真时重复运行一些代码。让您控制Cairo代码执行的最常见结构是if表达式和循环。

if表达式

if表达式允许根据条件分支代码。您提供一个条件,然后声明:“如果满足此条件,请运行此代码块。如果条件不成立,请不要运行此代码块。”

文件名:main.cairo

use debug::PrintTrait;

fn main() {
    let number = 3;

    if number == 5 {
        'condition was true'.print();
    } else {
        'condition was false'.print();
    }
}

所有if表达式都以关键字if开头,后跟条件。在这种情况下,条件检查变量number是否具有等于5的值。我们将要执行的代码块放在条件后面的花括号内,用于执行条件为true的条件块。

可选地,我们还可以包含else表达式,我们选择在这里使用它,以便程序有替代代码块可执行,如果条件评估为false。如果您不提供else表达式并且条件为false,则程序将跳过if块并继续执行下一段代码。

尝试运行此代码; 您应该会看到以下输出:

$ cairo-run main.cairo
[DEBUG]	condition was false

让我们尝试更改number的值,以使条件为真,看看会发生什么:

let number = 5;
$ cairo-run main.cairo
condition was true

还值得注意的是,此代码中的条件必须是bool类型。如果条件不是bool,则会出现错误。

$ cairo-run main.cairo
thread 'main' panicked at 'Failed to specialize: `enum_match<felt252>`. Error: Could not specialize libfunc `enum_match` with generic_args: [Type(ConcreteTypeId { id: 1, debug_name: None })]. Error: Provided generic argument is unsupported.', crates/cairo-lang-sierra-generator/src/utils.rs:256:9

使用else if处理多个条件

您可以通过将if和else组合在else if表达式中来使用多个条件。例如:

文件名:main.cairo

use debug::PrintTrait;

fn main() {
    let number = 3;

    if number == 12 {
        'number is 12'.print();
    } else if number == 3 {
        'number is 3'.print();
    } else if number - 2 == 1 {
        'number minus 2 is 1'.print();
    } else {
        'number not found'.print();
    }
}

这个程序有四个可能的路径。运行它后,您应该会看到以下输出:

[DEBUG]	number is 3

当此程序执行时,它依次检查每个if表达式,并执行第一个条件为真的主体。请注意,即使number - 2 == 1为真,我们也看不到输出number minus 2 is 1' .print(),也没有从else块中看到not found文本。这是因为Cairo仅执行第一个真条件的块,并且一旦它找到一个条件,它甚至不会检查其余部分。使用太多的else if表达式可能会使您的代码混乱,因此如果有多个,您可能需要重构代码。第5章描述了用于这些情况的强大的Cairo分支结构,称为match。

在let语句中使用if

因为if是一个表达式,我们可以在let语句的右侧使用它,将结果赋给一个变量。

文件名:main.cairo

use debug::PrintTrait;

fn main() {
    let condition = true;
    let number = if condition {
        5
    } else {
        10
    };

    number.print();
}

在这个例子中,我们声明了一个变量condition并将其设置为true。然后我们定义了一个新变量number,并使用if表达式初始化它。如果条件为真,则将5分配给number。否则,将10分配给number。

最终,我们调用print()方法来打印变量的值。尝试运行此程序,您应该会看到输出:

[DEBUG]	5

循环

循环结构允许您多次执行一种或多种操作。Cairo支持while和for循环。

while循环

while循环不断地重复执行代码,直到条件为false为止。例如,以下代码片段计算从1到10的数字的总和:

文件名:main.cairo

use debug::PrintTrait;

fn main() {
    let mut sum = 0;
    let mut i = 1;

    while i <= 10 {
        sum += i;
        i += 1;
    }

    sum.print();
}

在这个例子中,我们定义了两个变量:sum和i。我们使用while循环来重复执行代码块,每次将i添加到sum中,然后递增i的值。当i等于11时,while循环的条件评估为false,并退出循环。

您可以使用类似的方式编写无限循环:只需编写while true或while 1就可以了。

for循环

for循环是一种更常见的循环结构,它按照一定的步骤重复执行代码。例如,以下代码片段打印从1到10的数字:

文件名:main.cairo

use debug::PrintTrait;

fn main() {
    for i in 1..11 {
        i.print();
    }
}

在这个例子中,我们使用for循环将i从1逐步增加到10,并在每次迭代时打印它的值。请注意,1..11是一个范围对象,表示从1到10的整数(包括1,但不包括11)。您可以使用类似的方式声明其他类型的范围:例如,'a'..'z'将为字母a到z创建一个范围。

break和continue语句

Cairo还支持break和continue语句,它们可以在循环内部控制执行流程。

break语句用于立即停止当前循环并退出该循环。例如,以下代码片段计算从1开始的第一个平方和大于100的数字:

文件名:main.cairo

use debug::PrintTrait;

fn main() {
    let mut sum = 0;
    let mut i = 1;

    loop {
        let square = i * i;

        if sum + square > 100 {
            break;
        }

        sum += square;
        i += 1;
    }

    sum.print();
}

在这个例子中,我们使用loop语句来创建一个无限循环。在每次迭代中,我们计算当前数字的平方并检查是否将其添加到sum中将使得sum超过100。如果是这样,我们使用break语句停止循环。否则,我们将数字的平方添加到sum中,并递增i的值。

continue语句用于立即跳过当前迭代并开始下一次迭代。例如,以下代码片段打印从1到10之间的偶数:

文件名:main.cairo

use debug::PrintTrait;

fn main() {
    for i in 1..11 {
        if i % 2 == 1 {
            continue;
        }

        i.print();
    }
}

在这个例子中,我们在for循环内部使用if表达式来检查i是否为奇数。如果是这样,我们使用continue语句跳过该迭代。否则,我们打印数字的值。因此,我们只会看到输出2、4、6、8和10。

总结

控制流是编程中非常重要的概念,它允许您根据条件运行一些代码,并在某些情况下重复运行一些代码。Cairo支持if表达式、while循环和for循环。您还可以使用break和continue语句来控制循环内部的执行流程。熟练掌握这些结构对于编写高效和可读性良好的代码至关重要。

常用集合类型

Cairo提供了一组常见的集合类型,可以用于存储和操作数据。这些集合旨在高效、灵活和易于使用。本节介绍了Cairo1中可用的主要集合类型:Array和Felt252Dict(即将推出)。

Array

数组是相同类型元素的集合。您可以导入array::ArrayTrait trait创建和使用数组方法。

需要注意的重要事项是,数组具有有限的修改选项。实际上,数组是队列,其值无法修改。这与一旦写入内存插槽就不能被覆盖而只能从中读取有关。您只能将项目附加到数组的末尾并使用pop_front从前面删除项目。

创建Array

使用ArrayTrait :: new()调用来创建Array。下面是创建一个数组并向其中添加3个元素的示例:

use array::ArrayTrait;

fn main() {
    let mut a = ArrayTrait::new();
    a.append(0);
    a.append(1);
    a.append(2);
}

您可以在实例化数组时传递数组中项的预期类型:

let mut arr = ArrayTrait::<u128>::new();

更新Array

添加元素

要将元素添加到数组的末尾,可以使用append()方法:

    a.append(0);

删除元素

您只能使用pop_front()方法从数组的前面删除元素。此方法返回包含已移除元素的Option,或如果数组为空,则返回Option :: None。

use option::OptionTrait;
use array::ArrayTrait;
use debug::PrintTrait;

fn main() {
    let mut a = ArrayTrait::new();
    a.append(10);
    a.append(1);
    a.append(2);

    let first_value = a.pop_front().unwrap();
    first_value.print(); // print '10'
}

上面的代码将打印10,因为我们删除了添加的第一个元素。

在Cairo中,内存是不可变的,这意味着一旦将元素添加到数组中,就无法修改它们。您只能将元素添加到数组的末尾,并从数组的前面删除元素。这些操作不需要内存突变,因为它们涉及更新指针而不是直接修改内存单元。

读取Array中的元素

要访问数组元素,可以使用get()或at()数组方法返回不同类型的快照。使用arr.at(index)相当于使用下标运算符arr[index]。

get函数返回Option<Box <@ T>>,这意味着如果该元素存在于数组中,则返回指向指定索引处元素的Box类型(Cairo的智能指针类型)的选项快照。如果不存在该元素,则get返回None。这种方法在您希望访问可能不在数组范围内的索引并且希望优雅地处理此类情况而不会发生崩溃时很有用。引用和快照的详细信息将在参考和快照章节中进行解释。

另一方面,at函数使用unbox()运算符直接返回指定索引处元素的快照,以提取存储在框中的值。如果索引超出范围,则会发生恐慌错误。只有当您希望程序在所提供的索引超出数组范围时发生panic错误时,才应使用at,这可以防止意外行为。

总之,当您想要在越界尝试访问时发生panic时,请使用at,并且在您希望优雅地处理这些情况而不会发生panic时,请使用get。

use array::ArrayTrait;
fn main() {
    let mut a = ArrayTrait::new();
    a.append(0);
   a.append(1);

    let first = *a.at(0);
    let second = *a.at(1);
}

在这个例子中,名为first的变量将得到值0,因为它是数组中索引为0的值。名为second的变量将从数组中的索引1获取值1。

下面是使用get()方法的示例:

use array::ArrayTrait;
use box::BoxTrait;
fn main() -> u128 {
    let mut arr = ArrayTrait::<u128>::new();
    arr.append(100);
    let index_to_access =
        1; // Change this value to see different results, what would happen if the index doesn't exist?
    match arr.get(index_to_access) {
        Option::Some(x) => {
            *x.unbox()
        // Don't worry about * for now, if you are curious see Chapter 3.2 #desnap operator
        // It basically means "transform what get(idx) returned into a real value"
        },
        Option::None(_) => {
            let mut data = ArrayTrait::new();
            data.append('out of bounds');
            panic(data)
        }
    }
}

大小相关的方法

要确定数组中元素的数量,请使用len()方法。返回类型为usize。

如果您想检查数组是否为空,则可以使用is_empty()方法,如果数组为空则返回true,否则返回false。

使用Enum存储多种类型

如果要在数组中存储不同类型的元素,则可以使用枚举定义自定义数据类型,该类型可以容纳多种类型。

use array::ArrayTrait;
use traits::Into;

#[derive(Copy, Drop)]
enum Data {
    Integer: u128,
    Felt: felt252,
    Tuple: (u32, u32),
}

fn main() {
    let mut messages: Array<Data> = ArrayTrait::new();
    messages.append(Data::Integer(100));
    messages.append(Data::Felt('hello world'));
    messages.append(Data::Tuple((10, 30)));
}

Span

Span是一个表示Array快照的结构体。它旨在提供对数组元素的安全和可控访问,而不修改原始数组。跨度特别适用于确保数据完整性并避免传递函数之间或执行只读操作时出现借用问题(参见“引用和快照”)

所有由Array提供的方法也可以与Span一起使用,除了append()方法。

将Array转换为span

要创建Array的Span,请调用span()方法:

array.span()

总结

您已经完成了本章!这是一个相当大的章节:您学习了变量、数据类型、函数、注释、if表达式、循环和常见集合!为了练习本章中讨论的概念,请尝试编写以下程序:

  • 生成第n个斐波那契数。
  • 计算数字n的阶乘。

当您准备好继续时,我们将讨论Cairo与Rust共享的一个概念,在其他编程语言中通常不存在:所有权。

Understanding Ownership

What is Ownership?

References and Snapshots

Using Structs to Structure Related Data

Defining and Instantiating Structs

An Example Program Using Structs

Method Syntax

Enums and Pattern Matching

Enums

The Match Control Flow Construct

Managing Cairo Projects with Packages, Crates and Modules

Packages and Crates

Defining Modules to Control Scope

Paths for Referring to an Item in the Module Tree

Bringing Paths into Scope with the use Keyword

Generic Types

Generic Functions

Traits in Cairo

Testing Cairo Programs

How To Write Tests

Testing Organization

附录

A - 开发工具