引用和快照

示例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;
}

示例4-6:试图修改一个快照值

这里有一个错误:

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函数中的heightwidth字段的值。

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;
}

示例 4-7:使用一个可变的引用来修改一个值

首先,我们把rec改成mut。然后我们用 ref recrec 的可变引用传入 flip ,并更新函数签名,用 ref rec: Rectangle接受可变引用。这很清楚地表明,flip函数将改变作为参数传递的Rectangle实例的值。

程序的输出是:

[DEBUG]
                                (raw: 10)

[DEBUG]	                        (raw: 3)

正如预期的那样, rec 变量的 heightwidth 字段被调换了。

小结

Let’s recap what we’ve discussed about the linear type system, ownership, snapshots, and references:

  • 在任何时候,一个变量只能有一个所有者。
  • 你可以将一个变量以值的方式、以快照的方式、或以引用的方式传递给一个函数。
  • 如果你按值传递,变量的所有权就会转移到函数中。
  • 如果你想保留变量的所有权,并且知道你的函数不会改变它,你可以用@把它作为一个快照传递。
  • 如果你想保留变量的所有权,并且知道你的函数会改变它,你可以用ref把它作为一个可改变的引用来传递。
Last change: 2023-12-08, commit: 7c6a72a