StarknetAstro

StarknetAstro

11_Cairo's struct (structure)

Struct in Cairo#

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

Basic Usage#

Define a struct:

#[derive(Copy, Drop)]
struct User {
    active: bool,
    username: felt252,
    email: felt252,
    sign_in_count: u64,
}

Create a struct variable (note: when creating, all fields need to be assigned values, otherwise the compiler will report an error: Missing member error):

#[derive(Copy, Drop)]
struct User {
    active: bool,
    username: felt252,
    email: felt252,
    sign_in_count: u64,
}

fn main() {
    let user1 = User {
        active: true, username: 'someusername123', email: '[email protected]', sign_in_count: 1
    };
}

Use the fields of the struct variable:

use debug::PrintTrait;

#[derive(Copy, Drop)]
struct User {
    active: bool,
    username: felt252,
    email: felt252,
    sign_in_count: u64,
}

fn main() {
    let user1 = User {
        active: true, 
        username: 'someusername123', 
        email: '[email protected]', 
        sign_in_count: 1
    };

    user1.active.print();
    user1.username.print();
}

Modify the value of a field in the struct variable. The mutability and immutability of a variable also apply to struct variables, so if you want to modify a struct variable, the variable needs to be mutable (marked with the mut keyword). For example:

use debug::PrintTrait;

#[derive(Copy, Drop)]
struct User {
    active: bool,
    username: felt252,
    email: felt252,
    sign_in_count: u64,
}

fn main() {
    let mut user1 = User {
        active: true, 
        username: 'someusername123', 
        email: '[email protected]', 
        sign_in_count: 1
    };

    user1.username = 'shalom';
    user1.username.print();
}

The user1 variable above is marked with the mut keyword. Note: the entire struct variable must be marked as mutable at the same time, and it is not allowed to only mark some of the fields as mutable.

A convenient way to instantiate struct variables#

In many languages, a function is often used to specifically instantiate a struct variable, such as:

use debug::PrintTrait;

#[derive(Copy, Drop)]
struct User {
    active: bool,
    username: felt252,
    email: felt252,
    sign_in_count: u64,
}

fn init_user(active: bool, username: felt252, email: felt252, sign_in_count: u64) -> User {
    User { active: active, username: username, email: email, sign_in_count: sign_in_count }
}

fn main() {
    let mut user1 = init_user(true, 'someusername123', '[email protected]', 1);

    user1.username = 'shalom';
    user1.username.print();
}

In the above code, the init_user function is used to instantiate the User struct variable.

In this init_user function, all the parameters have the same names as the fields of the struct, so we can simplify it like this:

fn init_user_simplify(active: bool, username: felt252, email: felt252, sign_in_count: u64) -> User {
    // The 'active:' part is omitted here...
    User { active, username, email, sign_in_count }
}

When assigning values to the fields of the User struct inside the function, there is no need to specify the field names, just use the same names as the function parameters.

Defining member methods (methods) for a struct#

Most high-level programming languages have the feature of defining member methods for structs (or objects), and Cairo is no exception. However, implementing this feature in Cairo requires the use of Traits. For example:

use debug::PrintTrait;

struct Rectangle {
    width: u32,
    high: u32
}

// Here, a Trait is declared
trait GeometryTrait {
    fn area(self: Rectangle) -> u32;
}

impl RectangleImpl of GeometryTrait {
    // Here, the area method in the Trait is implemented, and the first parameter in the method is what connects it to the struct
    fn area(self: Rectangle) -> u32 {
        self.width * self.high
    }
}

fn main() {
    let r = Rectangle {
        width: 10,
        high: 2,
    };

    r.area().print();
}

The trait provides the signature of the method, which identifies the name, parameters, and return value of the method. In the impl block, the specific logic of the method is implemented, and it must include the logic of all methods in the trait.

Note: The requirements for the code in the impl block to be connected to the struct are quite strict:

  1. The associated struct must be specified as the first parameter in the method
  2. The parameter name must be self
  3. The self variable in one impl block must be the same struct

It can be understood that impl is exclusive to a struct type, and it must implement the logic of all methods in the trait (trait means feature).

Constructor function in trait#

In a trait, not all functions are struct members. It can include functions that do not use the self parameter, and this type of function is often used as a constructor function for the struct. For example:

struct Rectangle {
    width: u32,
    high: u32
}

trait RectangleTrait {
    fn square(size: u32) -> Rectangle;
}

impl RectangleImpl of RectangleTrait {
    fn square(size: u32) -> Rectangle {
        Rectangle { width: size, high: size }
    }
}

The square function above is the constructor function for Rectangle, and it does not have a self variable inside. When we need to instantiate a Rectangle variable, we can do: let square = RectangleImpl::square(10); (Note the use of impl here!).

One struct associated with multiple traits#

Struct and trait are independent of each other, and the same struct can implement multiple traits. For example:

struct Rectangle {
    width: u32,
    high: u32
}

struct Rectangle2 {
    width: u32,
    high: u32
}

trait RectangleCalc {
    fn area(self: Rectangle) -> u32;
}
impl RectangleCalcImpl of RectangleCalc {
    fn area(self: Rectangle) -> u32 {
        (self.width) * (self.high)
    }
}

trait RectangleCmp {
    fn can_hold(self: Rectangle, other: Rectangle) -> bool;
}

impl RectangleCmpImpl of RectangleCmp {
    fn can_hold(self: Rectangle, other: Rectangle) -> bool {
        self.width > other.width && self.high > other.high
    }
}

fn main() {}

In the above code, Rectangle implements both RectangleCalc and RectangleCmp traits.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.