引用和快照
The issue with the tuple code in previous Listing 4-3 is that we have to return the Array
to the calling function so we can still use the Array
after the call to calculate_length
, because the Array
was moved into calculate_length
.
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".
Here is how you would define and use a calculate_length
function that takes a snapshot of an array as a parameter instead of taking ownership of the underlying value. In this example, the calculate_length
function returns the length of the array passed as a parameter. As we're passing it as a snapshot, which is an immutable view of the array, we can be sure that the calculate_length
function will not mutate the array, and ownership of the array is kept in the main
function.
文件名: src/lib.cairo
fn main() {
let mut arr1: Array<u128> = array![];
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
println!("The length of the array when the snapshot was taken is {}", first_length);
println!("The current length of the array is {}", second_length);
}
fn calculate_length(arr: @Array<u128>) -> usize {
arr.len()
}
Note: it is only possible to call the
len()
method on an array snapshot because it is defined as such in theArrayTrait
trait. If you try to call a method that is not defined for snapshots on a snapshot, you will get a compilation error. However, you can call methods expecting a snapshot on non-snapshot types.
这个程序的输出是:
$ scarb cairo-run
Compiling no_listing_09_snapshots v0.1.0 (listings/ch04-understanding-ownership/no_listing_09_snapshots/Scarb.toml)
Finished `dev` profile target(s) in 3 seconds
Running no_listing_09_snapshots
The length of the array when the snapshot was taken is 0
The current length of the array is 1
Run completed successfully, returning []
First, notice that all the tuple code in the variable declaration and the function return value is gone. Second, note that we pass @arr1
into calculate_length
and, in its definition, we take @Array<u128>
rather than Array<u128>
.
让我们仔细看一下这里的函数调用:
let second_length = calculate_length(@arr1); // Calculate the current length of the array
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 always implement the Drop
trait, never the Destruct
trait, even dictionary snapshots.
同样,函数的签名使用@
来表示参数arr
的类型是一个快照。让我们添加一些解释性的注解:
fn calculate_length(
array_snapshot: @Array<u128> // array_snapshot is a snapshot of an Array
) -> usize {
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 Operator
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
操作符 *
将快照转换回值。
#[derive(Drop)]
struct Rectangle {
height: u64,
width: u64,
}
fn main() {
let rec = Rectangle { height: 3, width: 10 };
let area = calculate_area(@rec);
println!("Area: {}", area);
}
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
}
But, what happens if we try to modify something we’re passing as a snapshot? Try the code in Listing 4-4. Spoiler alert: it doesn’t work!
文件名: 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;
}
这里有一个错误:
$ scarb cairo-run
Compiling listing_04_04 v0.1.0 (listings/ch04-understanding-ownership/listing_04_attempt_modifying_snapshot/Scarb.toml)
error: Invalid left-hand side of assignment.
--> listings/ch04-understanding-ownership/listing_04_attempt_modifying_snapshot/src/lib.cairo:15:5
rec.height = rec.width;
^********^
error: Invalid left-hand side of assignment.
--> listings/ch04-understanding-ownership/listing_04_attempt_modifying_snapshot/src/lib.cairo:16:5
rec.width = temp;
^*******^
error: could not compile `listing_04_04` due to previous error
error: `scarb metadata` exited with error
编译器阻止我们修改与快照相关的值。
Mutable References
We can achieve the behavior we want in Listing 4-4 by using a mutable reference instead of a snapshot. Mutable references are actually mutable values passed to a function that are implicitly returned at the end of the function, returning ownership to the calling context. By doing so, they allow you to mutate the value passed while keeping ownership of it by returning it automatically at the end of the execution. In Cairo, a parameter can be passed as mutable reference using the ref
modifier.
Note: In Cairo, a parameter can only be passed as mutable reference using the
ref
modifier if the variable is declared as mutable withmut
.
In Listing 4-5, we use a mutable reference to modify the value of the height
and width
fields of the Rectangle
instance in the flip
function.
#[derive(Drop)]
struct Rectangle {
height: u64,
width: u64,
}
fn main() {
let mut rec = Rectangle { height: 3, width: 10 };
flip(ref rec);
println!("height: {}, width: {}", rec.height, rec.width);
}
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
实例的值。
程序的输出是:
$ scarb cairo-run
Compiling listing_04_05 v0.1.0 (listings/ch04-understanding-ownership/listing_05_mutable_reference/Scarb.toml)
Finished `dev` profile target(s) in 3 seconds
Running listing_04_05
height: 10, width: 3
Run completed successfully, returning []
正如预期的那样, rec
变量的 height
和 width
字段被调换了。
Small Recap
Let’s recap what we’ve discussed about the linear type system, ownership, snapshots, and references:
- At any given time, a variable can only have one owner.
- You can pass a variable by-value, by-snapshot, or by-reference to a function.
- If you pass-by-value, ownership of the variable is transferred to the function.
- If you want to keep ownership of the variable and know that your function won’t mutate it, you can pass it as a snapshot with
@
. - If you want to keep ownership of the variable and know that your function will mutate it, you can pass it as a mutable reference with
ref
.