Defining and Instantiating Structs
Structs are similar to tuples, discussed in the Data Types section, in that both hold multiple related values. Like tuples, the pieces of a struct can be different types. Unlike with tuples, in a struct you’ll name each piece of data so it’s clear what the values mean. Adding these names means that structs are more flexible than tuples: you don’t have to rely on the order of the data to specify or access the values of an instance.
To define a struct, we enter the keyword struct and name the entire struct. A
struct’s name should describe the significance of the pieces of data being
grouped together. Then, inside curly brackets, we define the names and types of
the pieces of data, which we call fields. For example, Listing
5-1 shows a struct that stores information about a user
account.
Filename: src/lib.cairo
#[derive(Drop)]
struct User {
active: bool,
username: ByteArray,
email: ByteArray,
sign_in_count: u64,
}
Listing 5-1: A User struct
definition
Note :
You can derive multiple traits on structs, such asDrop,PartialEqfor comparison andDebugfor debug-printing.
See the Appendix on Derivable Traits for a complete list and examples.
To use a struct after we’ve defined it, we create an instance of that struct by specifying concrete values for each of the fields. We create an instance by stating the name of the struct and then add curly brackets containing key: value pairs, where the keys are the names of the fields and the values are the data we want to store in those fields. We don’t have to specify the fields in the same order in which we declared them in the struct. In other words, the struct definition is like a general template for the type, and instances fill in that template with particular data to create values of the type.
For example, we can declare two particular users as shown in Listing 5-2.
Filename: src/lib.cairo
#[derive(Drop)]
struct User {
active: bool,
username: ByteArray,
email: ByteArray,
sign_in_count: u64,
}
#[executable]
fn main() {
let user1 = User {
active: true, username: "someusername123", email: "[email protected]", sign_in_count: 1,
};
let user2 = User {
sign_in_count: 1, username: "someusername123", active: true, email: "[email protected]",
};
}
Creating two instances of the User struct
To get a specific value from a struct, we use dot notation. For example, to
access user1's email address, we use user1.email. If the instance is
mutable, we can change a value by using the dot notation and assigning into a
particular field. Listing 5-3 shows how to change the value in the
email field of a mutable User instance.
Filename: src/lib.cairo
#[derive(Drop)]
struct User {
active: bool,
username: ByteArray,
email: ByteArray,
sign_in_count: u64,
}
#[executable]
fn main() {
let mut user1 = User {
active: true, username: "someusername123", email: "[email protected]", sign_in_count: 1,
};
user1.email = "[email protected]";
}
fn build_user(email: ByteArray, username: ByteArray) -> User {
User { active: true, username: username, email: email, sign_in_count: 1 }
}
fn build_user_short(email: ByteArray, username: ByteArray) -> User {
User { active: true, username, email, sign_in_count: 1 }
}
the value in the email field of a User instance
Note that the entire instance must be mutable; Cairo doesn’t allow us to mark only certain fields as mutable.
As with any expression, we can construct a new instance of the struct as the last expression in the function body to implicitly return that new instance.
Listing 5-4 shows a build_user function that returns a User
instance with the given email and username. The active field gets the value of
true, and the sign_in_count gets a value of 1.
Filename: src/lib.cairo
#[derive(Drop)]
struct User {
active: bool,
username: ByteArray,
email: ByteArray,
sign_in_count: u64,
}
#[executable]
fn main() {
let mut user1 = User {
active: true, username: "someusername123", email: "[email protected]", sign_in_count: 1,
};
user1.email = "[email protected]";
}
fn build_user(email: ByteArray, username: ByteArray) -> User {
User { active: true, username: username, email: email, sign_in_count: 1 }
}
fn build_user_short(email: ByteArray, username: ByteArray) -> User {
User { active: true, username, email, sign_in_count: 1 }
}
build_user function that takes an email and username and returns a User
instance.
It makes sense to name the function parameters with the same name as the struct
fields, but having to repeat the email and username field names and
variables is a bit tedious. If the struct had more fields, repeating each name
would get even more annoying. Luckily, there’s a convenient shorthand!
Using the Field Init Shorthand
Because the parameter names and the struct field names are exactly the same in
Listing 5-4, we can use the field init shorthand syntax to
rewrite build_user so it behaves exactly the same but doesn’t have the
repetition of username and email, as shown in Listing
5-5.
Filename: src/lib.cairo
#[derive(Drop)]
struct User {
active: bool,
username: ByteArray,
email: ByteArray,
sign_in_count: u64,
}
#[executable]
fn main() {
let mut user1 = User {
active: true, username: "someusername123", email: "[email protected]", sign_in_count: 1,
};
user1.email = "[email protected]";
}
fn build_user(email: ByteArray, username: ByteArray) -> User {
User { active: true, username: username, email: email, sign_in_count: 1 }
}
fn build_user_short(email: ByteArray, username: ByteArray) -> User {
User { active: true, username, email, sign_in_count: 1 }
}
A build_user function that uses field init shorthand because the username
and email parameters have the same name as struct fields.
Here, we’re creating a new instance of the User struct, which has a field
named email. We want to set the email field’s value to the value in the
email parameter of the build_user function. Because the email field and
the email parameter have the same name, we only need to write email rather
than email: email.
Creating Instances from Other Instances with Struct Update Syntax
It’s often useful to create a new instance of a struct that includes most of the values from another instance, but changes some. You can do this using struct update syntax.
First, in Listing 5-6 we show how to create a new
User instance in user2 regularly, without the update syntax. We set a new
value for email but otherwise use the same values from user1 that we created
in Listing 5-2.
Filename: src/lib.cairo
#[derive(Drop)]
struct User {
active: bool,
username: ByteArray,
email: ByteArray,
sign_in_count: u64,
}
#[executable]
fn main() {
// --snip--
let user1 = User {
email: "[email protected]", username: "someusername123", active: true, sign_in_count: 1,
};
let user2 = User {
active: user1.active,
username: user1.username,
email: "[email protected]",
sign_in_count: user1.sign_in_count,
};
}
Listing 5-6: Creating a new
User instance using all but one of the values from user1
Using struct update syntax, we can achieve the same effect with less code, as
shown in Listing 5-7. The syntax .. specifies that the
remaining fields not explicitly set should have the same value as the fields in
the given instance.
Filename: src/lib.cairo
use core::byte_array;
#[derive(Drop)]
struct User {
active: bool,
username: ByteArray,
email: ByteArray,
sign_in_count: u64,
}
#[executable]
fn main() {
// --snip--
let user1 = User {
email: "[email protected]", username: "someusername123", active: true, sign_in_count: 1,
};
let user2 = User { email: "[email protected]", ..user1 };
}
Listing 5-7: Using struct update syntax
to set a new email value for a User instance but to use the rest of the
values from user1
The code in Listing 5-7 also creates an instance of user2
that has a different value for email but has the same values for the
username, active, and sign_in_count fields as user1. The ..user1 part
must come last to specify that any remaining fields should get their values from
the corresponding fields in user1, but we can choose to specify values for as
many fields as we want in any order, regardless of the order of the fields in
the struct’s definition.
Note that the struct update syntax uses = like an assignment; this is because
it moves the data, just as we saw in the "Moving Values"
section. In this example, we can no longer use user1 as a whole after creating
user2 because the ByteArray in the username field of user1 was moved
into user2. If we had given user2 new ByteArray values for both email
and username, and thus only used the active and sign_in_count values from
user1, then user1 would still be valid after creating user2. Both active
and sign_in_count are types that implement the Copy trait, so the behavior
we discussed in the "Copy Trait" section would apply.