引用和快照
示例4-5中元组代码的问题是,因为 Array
的所有权被移到了calculate_length
中。
我们必须返回Array
给调用的函数,这样我们在调用calculate_length
后才仍然可以使用Array
。
快照(Snapshots)
In the previous chapter, we talked about how Cairo's ownership system prevents us from using a variable after we've moved it, protecting us from potentially writing twice to the same memory cell. However, it's not very convenient. Let's see how we can retain ownership of the variable in the calling function using snapshots.
In Cairo, a snapshot is an immutable view of a value at a certain point in time. Recall that memory is immutable, so modifying a value actually creates a new memory cell. The old memory cell still exists, and snapshots are variables that refer to that "old" value. In this sense, snapshots are a view "into the past".
下面是你如何定义和使用一个calculate_length
函数,它以一个快照作为参数,而不是获取底层值的所有权。在这个例子中、
calculate_length
函数返回作为参数的数组的长度。
因为我们是以快照的形式传递的,这是一个不可改变的数组视图,我们可以确定
calculate_length
函数不会改变数组,数组的所有权被保留在主函数中。
文件名: src/lib.cairo
use debug::PrintTrait; fn main() { let mut arr1 = ArrayTrait::<u128>::new(); let first_snapshot = @arr1; // Take a snapshot of `arr1` at this point in time arr1.append(1); // Mutate `arr1` by appending a value let first_length = calculate_length( first_snapshot ); // Calculate the length of the array when the snapshot was taken //ANCHOR: function_call let second_length = calculate_length(@arr1); // Calculate the current length of the array //ANCHOR_END: function_call first_length.print(); second_length.print(); } fn calculate_length(arr: @Array<u128>) -> usize { arr.len() }
注意:只有在数组快照上才能调用
len()
方法,因为它在ArrayTrait
trait中被定义成这样。如果你试图在一个快照上调用一个没有为快照定义的方法,你会得到一个编译错误。然而,你可以在非快照类型上调用快照的方法。
这个程序的输出是:
[DEBUG] (raw: 0)
[DEBUG] (raw: 1)
Run completed successfully, returning []
首先,注意到变量声明和函数返回值中的所有元组代码都消失了。
第二,注意看我们把@arr1
传入calculate_length
,因此在它的定义中,我们采用@Array<u128>
,而不是Array<u128>
。
让我们仔细看一下这里的函数调用:
use debug::PrintTrait; fn main() { let mut arr1 = ArrayTrait::<u128>::new(); let first_snapshot = @arr1; // Take a snapshot of `arr1` at this point in time arr1.append(1); // Mutate `arr1` by appending a value let first_length = calculate_length( first_snapshot ); // Calculate the length of the array when the snapshot was taken let second_length = calculate_length(@arr1); // Calculate the current length of the array first_length.print(); second_length.print(); } fn calculate_length(arr: @Array<u128>) -> usize { arr.len() }
The @arr1
syntax lets us create a snapshot of the value in arr1
. Because a snapshot is an immutable view of a value at a specific point in time, the usual rules of the linear type system are not enforced. In particular, snapshot variables are always Drop
, never Destruct
, even dictionary snapshots.
同样,函数的签名使用@
来表示参数arr
的类型是一个快照。让我们添加一些解释性的注解:
fn calculate_length(
array_snapshot: @Array<u128>
) -> usize { // array_snapshot is a snapshot of an Array
array_snapshot.len()
} // Here, array_snapshot goes out of scope and is dropped.
// However, because it is only a view of what the original array `arr` contains, the original `arr` can still be used.
变量array_snapshot
的有效范围与任何函数参数的范围相同,但当array_snapshot
停止使用时,快照的底层值不会被丢弃。当函数有快照作为参数而不是实际的值时,我们将不需要返回值以归还原始值的所有权,因为我们从未拥有过它。
Desnap 操作符
To convert a snapshot back into a regular variable, you can use the desnap
operator *
, which serves as the opposite of the @
operator.
Only Copy
types can be desnapped. However, in the general case, because the value is not modified, the new variable created by the desnap
operator reuses the old value, and so desnapping is a completely free operation, just like Copy
.
在下面的示例中,我们要计算一个矩形的面积,但我们不想在calculate_area
函数中取得矩形的所有权,因为我们可能想在函数调用后再次使用该矩形。由于我们的函数不会更改矩形实例,因此我们可以将矩形的快照传递给函数,然后使用 desnap
操作符 *
将快照转换回值。
use debug::PrintTrait; #[derive(Copy, Drop)] struct Rectangle { height: u64, width: u64, } fn main() { let rec = Rectangle { height: 3, width: 10 }; let area = calculate_area(@rec); area.print(); } fn calculate_area(rec: @Rectangle) -> u64 { // As rec is a snapshot to a Rectangle, its fields are also snapshots of the fields types. // We need to transform the snapshots back into values using the desnap operator `*`. // This is only possible if the type is copyable, which is the case for u64. // Here, `*` is used for both multiplying the height and width and for desnapping the snapshots. *rec.height * *rec.width }
但是,如果我们试图修改我们作为快照传递的东西会发生什么?试试下面的代码 示例4-6。剧透一下:它不起作用!
文件名: src/lib.cairo
#[derive(Copy, Drop)] struct Rectangle { height: u64, width: u64, } fn main() { let rec = Rectangle { height: 3, width: 10 }; flip(@rec); } fn flip(rec: @Rectangle) { let temp = rec.height; rec.height = rec.width; rec.width = temp; }
这里有一个错误:
error: Invalid left-hand side of assignment.
--> ownership.cairo:15:5
rec.height = rec.width;
^********^
编译器阻止我们修改与快照相关的值。
可变引用
在示例4-6中,我们也可以通过使用 mutable reference 而不是快照来实现我们想要的行为。可变引用实际上是传递给函数的可变值,在函数结束时被隐式返回,将所有权返回给调用的上下文。通过这样做,它们允许你对传递的值进行改变,同时通过在执行结束时自动返回来保持对它的所有权。
在Cairo中,一个参数可以使用ref
修饰符作为 mutable reference 传递。
注意:在Cairo中,只有在变量用
mut
声明为可变的情况下,才能使用ref
修饰符将参数作为可变的引用传递。
在示例4-7中,我们使用一个可变的引用来修改Rectangle
实例在flip
函数中的height
和width
字段的值。
use debug::PrintTrait; #[derive(Copy, Drop)] struct Rectangle { height: u64, width: u64, } fn main() { let mut rec = Rectangle { height: 3, width: 10 }; flip(ref rec); rec.height.print(); rec.width.print(); } fn flip(ref rec: Rectangle) { let temp = rec.height; rec.height = rec.width; rec.width = temp; }
首先,我们把rec
改成mut
。然后我们用 ref rec
将 rec
的可变引用传入 flip
,并更新函数签名,用 ref rec: Rectangle
接受可变引用。这很清楚地表明,flip
函数将改变作为参数传递的Rectangle
实例的值。
程序的输出是:
[DEBUG]
(raw: 10)
[DEBUG] (raw: 3)
正如预期的那样, rec
变量的 height
和 width
字段被调换了。
小结
Let’s recap what we’ve discussed about the linear type system, ownership, snapshots, and references:
- 在任何时候,一个变量只能有一个所有者。
- 你可以将一个变量以值的方式、以快照的方式、或以引用的方式传递给一个函数。
- 如果你按值传递,变量的所有权就会转移到函数中。
- 如果你想保留变量的所有权,并且知道你的函数不会改变它,你可以用
@
把它作为一个快照传递。 - 如果你想保留变量的所有权,并且知道你的函数会改变它,你可以用
ref
把它作为一个可改变的引用来传递。