11_Cairo's struct#
The version of the Cairo compiler used in this article is 2.0.0-rc0. Because Cairo is being updated rapidly, the syntax may vary slightly between different versions, and the content of 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, 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 variables also apply to struct variables. Therefore, if you want to modify a struct variable, the variable needs to be mutable (decorated 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 decorated with the mut keyword. Note: the entire struct variable must be decorated as mutable at the same time, and it is not allowed to only decorate some of the fields as mutable.
A convenient way to instantiate struct variables#
In many languages, a function is often used to specialize the instantiation of 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 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 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 Trait. 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 related 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 all the logic of the methods in the trait must be included.
Note: The requirements for the code in the impl block to be related to the struct are quite strict:
- The associated struct must be specified as the first parameter in the method
- And the parameter name must be self
- The self variable in one impl must be the same struct
It can be understood that impl is exclusive to a struct type and must implement the logic of all methods in the trait (trait means feature).
Constructor in trait#
In the 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 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 for Rectangle, and it does not have a self variable. When we need to instantiate a Rectangle variable, we can do: let square = RectangleImpl::square(10);
(Note: we use 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.