StarknetAstro

StarknetAstro

Snapshot and References in 15_Cairo1.0

Snapshot and Reference in 15_Cairo1.0#

The version of Cairo compiler used in this article: 2.0.0-rc0. Since Cairo is being updated rapidly, the syntax may vary slightly in different versions, and the article will be updated to the stable version in the future.

In the article 14_Cairo1.0 Variable Ownership, we mentioned the Copy trait, which allows objects that implement the Copy trait to be automatically copied and passed into functions as variables. Additionally, if a variable does not implement the Copy trait, a move operation will occur when it is passed into a function. However, in the programming process, we often want to retain ownership of variables in the context without performing a copy operation. In this case, we can use snapshots and references.

Snapshot#

A snapshot is the value of a variable at a certain moment, with the characteristics of being immutable and read-only. Therefore, it is suitable for use when passing parameters to functions without modifying the parameters (similar to the view function in contracts).

Basic Usage#

Cairo uses the @ operator and the * operator to work with snapshots. The @ operator is used to obtain a snapshot, and the * operator is used to read the value of the snapshot. Take a look:

use debug::PrintTrait;

#[derive(Drop, Copy)]
struct Student {
    name: felt252,
    age: u8,
}

fn print_student(s: @Student) {
    // s.name.print(); // Compilation error
    (*s).name.print();
}

fn main() {
    let mut s = Student { name: 'sam', age: 17 };

    let snapshot01 = @s;
    // Modify the value of s and see if snapshot01 changes
    s.name = 'tom';
    print_student(snapshot01);
    print_student(@s);
}

In the above code, we create a mutable Student struct variable s, obtain snapshot01 using the @ operator, and then modify the name field of s. We use the * operator to print both the name field of snapshot01 and the latest snapshot. The output is: one is "sam" and the other is "tom". It can be seen that the snapshot does not change with the changes of the original variable.

There is also a requirement: the type must implement the Copy trait in order to use the * operator to read the value of the snapshot.

Note: In the chained call (*s).name.print();, the * operator has the lowest priority, so parentheses are needed to prioritize taking the value of the snapshot.

Arrays and Snapshots#

In 12_Cairo1.0 Array, we mentioned that arrays do not implement the Copy trait, so when they are used as function parameters, a move operation will occur. In addition, types that do not implement the Copy trait cannot use the * operator to extract values from snapshots. Even so, we can still use snapshots to avoid moving arrays. For example:

use debug::PrintTrait;
use array::ArrayTrait;

fn use_array(arr: @Array<usize>) {
    arr.len().print();
    let v_z = *arr.at(0);
    v_z.print();
}

fn main(){
    let mut arr = ArrayTrait::<usize>::new();
    arr.append(9);

    use_array(@arr);
}

The entire array cannot use the * operator, but its elements can be used. Since at returns a snapshot of the element, we can extract the element.

There is a magical point: the snapshot of the array can be represented by [] instead of at. Let's take a look:

use debug::PrintTrait;
use array::ArrayTrait;

fn use_array(arr: @Array<usize>) {
    arr.len().print();
    // Here, `*arr[0]` is used instead of `*arr.at(0)`
    let v_z = *arr[0];
    v_z.print();
}

fn main(){
    let mut arr = ArrayTrait::<usize>::new();
    arr.append(9);

    use_array(@arr);
}

It can still compile successfully, but it is limited to the snapshot of the array. Directly reading the array will result in an error.

In addition, the get member function of the array seems to be unusable. The following code will result in a compilation error:

use debug::PrintTrait;
use array::ArrayTrait;
use option::OptionTrait;

fn use_array(arr: @Array<usize>) {
    match arr.get(0) {
        Option::Some(v) => {
            // Unable to use `*` to read the value here
            let tem = *v.unbox();
            tem.print();
        },
        Option::None(_) => {}
    }
}

fn main() {
    let mut arr = ArrayTrait::<usize>::new();
    arr.append(9);

    use_array(@arr);
}

Reference#

When we need to modify the value of a parameter in a function and want to retain the context when calling the function, we need to use references. Let's see how to use them:

use core::debug::PrintTrait;

#[derive(Copy, Drop)]
struct Rectangle {
    width: felt252,
    high: felt252,
}

fn setWidth(ref r: Rectangle, new_width: felt252) {
    r.width = new_width;
}

fn main() {
    let mut r = Rectangle { width: 100, high: 200 };
    setWidth(ref r, 300);
    r.width.print();
}

In the above code, the width printed is the value set in the setWidth function. We can see that even if the variable r is passed into the setWidth function, it does not affect the value read by the main function, and the printed value is the one set by the setWidth function.

References are specified using the ref keyword, which needs to be specified when defining a function and when passing parameters, indicating that a reference is being passed. In addition, variables marked with ref must be mutable variables.

In Cairo_book, it is mentioned that references are actually a shorthand for two move operations, which means that the passed variable is moved into the called function and then the ownership is implicitly moved back.

Example of using reference with arrays#

use debug::PrintTrait;
use array::ArrayTrait;

fn main() {
    let mut arr0 = ArrayTrait::new();
    fill_array(ref arr0);
    // Print the values added by fill_array
	arr0.print();
}

fn fill_array(ref arr0: Array<felt252>) {
    arr0.append(22);
    arr0.append(44);
    arr0.append(66);
}
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.