控制流

根据条件是否为真来决定是否执行某些代码,以及根据条件是否为真来重复运行一段 代码的能力是大部分编程语言的基本组成部分。Cairo 代码中最常见的用来控制执行 流的结构是 if 表达式和循环。

if表达式

if 表达式允许根据条件执行不同的代码分支。你提供一个条件并表示 “如果条件满 足,运行这段代码;如果条件不满足,不运行这段代码。”

文件名: src/lib.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 的值使条件为 true 时看看会发生什么:

    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处理多个条件

你可以通过在一个 else if 表达式中结合 if 和 else 来使用多个条件。比如说:

文件名: src/lib.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 表达式,并执行条件求值为 true 的第一个体。请注意,即使 number - 2 == 1true,我们也看不到输出 number minus 2 is 1'.print() ,也看不到 else 块中的 number not found 文本。这是因为 Cairo 只执行第一个真条件的代码块,一旦找到一个真条件,就不会再检查其他条件。使用过多的 else if 表达式会使代码变得杂乱无章,所以如果你有一个以上的 else if 表达式,你可能需要重构你的代码。Chapter 6 介绍了一种强大的Cairo语言分支结构,称为 "match",用于处理这些情况。

let 语句中使用 if

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

文件名: src/lib.cairo

use debug::PrintTrait;

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

    if number == 5 {
        'condition was true'.print();
    }
}
$ cairo-run main.cairo
[DEBUG]	condition was true

number 变量将会绑定到表示 if 表达式结果的值上。这里将是 5。

使用循环重复执行

多次执行同一段代码是很常用的,Cairo 为此提供了多种 循环(loops)。一个循环执行循环体中的代码直到结尾并紧接着回到开头继续执行。为了实验一下循环,让我们新建一个叫做 loops 的项目。

Cairo 目前只有一种循环:loop

使用 loop 重复执行代码

loop 关键字告诉 Cairo 一遍又一遍地执行一段代码直到你明确要求停止。

作为一个例子,将你的_loops_目录下的_src/lib.cairo_文件修改如下:

文件名: src/lib.cairo

use debug::PrintTrait;
fn main() {
    let mut i: usize = 0;
    loop {
        if i > 10 {
            break ;
        }
        'again!'.print();
    }
}

当运行这个程序时,我们会看到程序不停的反复打印 again!,直到我们手动停止程序,因为程序从未达到停止条件。 虽然编译器阻止我们编写没有停止条件(break语句)的程序,但该停止条件可能永远不会达到,从而会程序导致无限循环。 大多数终端支持键盘快捷键 ctrl-c 来中断卡在无限循环的程序。试一试吧:

$ scarb cairo-run --available-gas=20000000
[DEBUG]	again                          	(raw: 418346264942)

[DEBUG]	again                          	(raw: 418346264942)

[DEBUG]	again                          	(raw: 418346264942)

[DEBUG]	again                          	(raw: 418346264942)

Run panicked with err values: [375233589013918064796019]
Remaining gas: 1050

注意:Cairo通过包含一个 gas 计量器来防止我们运行无限循环的程序。 gas 计量器是一种限制程序中可进行的计算量的机制。通过给 --available-gas 标志设置一个值,我们可以设置程序的最大可用 gas 量。gas 是一个计量单位,表示一条指令的计算成本。当设置的最大gas值耗尽时,程序将停止。在这种情况下,程序会抛出 gas 耗尽的错误(panic),尽管从未达到停止条件。 对于部署在 Starknet 上的智能合约,它特别重要,因为它可以防止在网络上运行无限循环。 如果你正在编写一个需要运行循环的程序,你需要在执行时将 --available-gas 标志设置为一个足够大的值来运行该程序。

要退出循环,您可以在循环内部放置 break 语句,告诉程序何时停止循环。 我们可以通过在这个程序里加入可达的停止条件 i > 10 ,来修复无限循环。

use debug::PrintTrait;
fn main() {
    let mut i: usize = 0;
    loop {
        if i > 10 {
            break;
        }
        'again'.print();
        i += 1;
    }
}

关键字 continue 告诉程序进入循环的下一个迭代,并跳过现在这个迭代中的其他代码。让我们给我们的循环添加一个continue语句,使得当i等于5时跳过print语句。

use debug::PrintTrait;
fn main() {
    let mut i: usize = 0;
    loop {
        if i > 10 {
            break;
        }
        if i == 5 {
            i += 1;
            continue;
        }
        i.print();
        i += 1;
    }
}

i等于5时,执行这个程序将不会打印i的值。

从循环中返回值

loop 的一个用例是重试可能会失败的操作,比如检查线程是否完成了任务。然而你 可能会需要将操作的结果传递给其它的代码。如果将返回值加入你用来停止循环的 break 表达式,它会被停止的循环返回,如下所示:

use debug::PrintTrait;
fn main() {
    let mut counter = 0;

    let result = loop {
        if counter == 10 {
            break counter * 2;
        }
        counter += 1;
    };

    'The result is '.print();
    result.print();
}

在循环之前,我们声明一个名为 counter 的变量,并将其初始化为 0。然后我们声明一个名为 result 的变量,用来保存从循环中返回的值。 在循环的每一次迭代中,我们检查 counter 是否等于 10,然后在 counter 变量中加 1。当条件得到满足时,我们使用 break 关键字,其值为 counter * 2。在循环之后,我们用一个 分号来结束给result赋值的语句。最后,我们打印result中的值,在本例中是20

总结

你成功了!这一章很重要:你学到了变量、数据类型、函数、注释、 if 表达式和循环!要练习本章讨论的概念、 尝试编写程序来完成下列操作:

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

现在,我们将在下一章回顾 Cairo 中常见的集合类型。

Last change: 2023-11-19, commit: a15432b