变量和可变性
Cairo使用一个不可改变的内存模型,这意味着一旦一个内存单元被写入就不能被覆盖, 只能被读出。为了反映这种不可变的内存模型,变量在Cairo中默认是不可变的。 然而,该语言对这种模式进行了抽象,让你可以选择变量是否可变。让我们来 探讨一下Cairo是如何以及为什么要强行规定变量不可变,以及如何使变量成为可变的。
When a variable is immutable, once a variable is bound to a value, you can’t change
that variable. To illustrate this, generate a new project called variables in
your cairo_projects directory by using scarb new variables
.
然后,在你新的 variables 目录下,打开 src/lib.cairo 并将其替换为下面的代码,这段代码还不会被编译:
文件名: src/lib.cairo
use debug::PrintTrait; fn main() { let x = 5; x.print(); x = 6; x.print(); }
保存并使用scarb cairo-run
运行该程序。你应该收到一条关于不可变性的错误,如下所示:
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
变量分配第二个值。
It’s important that we get compile-time errors when we attempt to change a value that’s designated as immutable because this specific situation can lead to bugs. If one part of our code operates on the assumption that a value will never change and another part of our code changes that value, it’s possible that the first part of the code won’t do what it was designed to do. The cause of this kind of bug can be difficult to track down after the fact, especially when the second piece of code changes the value only sometimes.
Cairo, unlike most other languages, has immutable memory. This makes a whole class of bugs impossible, because values will never change unexpectedly. This makes code easier to reason about.
But mutability can be very useful, and can make code more convenient to write.
Although variables are immutable by default, you can make them mutable by
adding mut
in front of the variable name. Adding mut
also conveys
intent to future readers of the code by indicating that other parts of the code
will be changing the value associated to this variable.
However, you might be wondering at this point what exactly happens when a variable
is declared as mut
, as we previously mentioned that Cairo's memory is immutable.
The answer is that the value is immutable, but the variable isn't. What value
the variable points to can be changed. Assigning to a mutable variable in Cairo
is essentially equivalent to redeclaring it to refer to another value in another memory cell,
but the compiler handles that for you, and the keyword mut
makes it explicit.
Upon examining the low-level Cairo Assembly code, it becomes clear that
variable mutation is implemented as syntactic sugar, which translates mutation operations
into a series of steps equivalent to variable shadowing. The only difference is that at the Cairo
level, the variable is not redeclared so its type cannot change.
例如,让我们把 src/lib.cairo 改为以下内容:
文件名: src/lib.cairo
use debug::PrintTrait; fn main() { let mut x = 5; x.print(); x = 6; x.print(); }
当我们现在运行该程序时,我们得到了这个结果:
$ scarb cairo-run
[DEBUG] (raw: 5)
[DEBUG] (raw: 6)
Run completed successfully, returning []
当使用 mut
时,我们将在x
绑定的值从5
改为 6
。
最终,决定是否使用可变性取决于你自己以及你认为在特定情况下什么是最清楚的。
常量
类似于不可变变量,常量 (constants) 是绑定到一个名称的不允许改变的值,不过常量与变量还是有一些区别。
首先,不允许对常量使用 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
。当该作用域结束时,内部 shadowing 的作用域也结束了, x
又返回到 6
。运行这个程序,它会有如下输出:
scarb cairo-run
[DEBUG] Inner scope x value is: (raw: 7033328135641142205392067879065573688897582790068499258)
[DEBUG]
(raw: 12)
[DEBUG] Outer scope x value is: (raw: 7610641743409771490723378239576163509623951327599620922)
[DEBUG] (raw: 6)
Run completed successfully, returning []
隐藏与将变量标记为 mut
是有区别的。
当不小心尝试对变量重新赋值时,如果没有使用 let
关键字,就会导致编译时错误。
通过使用 let
,我们可以用这个新值进行一些计算,不过计算完之后变量仍然是不可变的。
mut
与隐藏的另一个区别是,当再次使用 let
时,实际上创建了一个新变量,我们可以改变值的类型,并且复用这个名字。
如前所述,变量隐藏和可变量在较低层次上是等同的。
唯一的区别是,通过隐藏变量,即使你改变它的类型。编译器也不会检测到错误
例如,假设我们的程序在u64
和 felt252
类型之间进行转换。
use debug::PrintTrait; fn main() { let x: u64 = 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; fn main() { let mut x: u64 = 2; x.print(); x = 100_felt252; x.print() }
这个错误表示我们预期得到一个 u64
类型(原始类型),但实际得到了不同的类型:
$ scarb cairo-run
error: Unexpected argument type. Expected: "core::integer::u64", found: "core::felt252".
--> lib.cairo:9:9
x = 100_felt252;
^*********^
Error: failed to compile: src/lib.cairo
现在我们已经了解了变量如何工作,让我们看看变量可以被赋予的其他数据类型。