Defining and Instantiating Structs
Structs are the primary way to create custom data types in Move. They group related fields together under a single name, much like structs in Rust or C.
Defining a Struct
A struct is defined using the struct keyword followed by the struct name, optional abilities, and the fields inside curly braces.
module my_addr::profiles {
use std::string::String;
struct UserProfile has key, store {
name: String,
age: u8,
balance: u64,
}
}
Key points about struct definitions:
- The struct name must start with an uppercase letter by convention.
- Each field has a name and a type, separated by a colon.
- Fields are separated by commas.
- Abilities (
key,store,copy,drop) are declared afterhas.
Creating Struct Instances
You create a struct instance by specifying the struct name and providing values for each field.
module my_addr::profiles {
use std::string::{Self, String};
struct UserProfile has key, store, drop {
name: String,
age: u8,
balance: u64,
}
public fun create_profile(name: String, age: u8): UserProfile {
UserProfile {
name,
age,
balance: 0,
}
}
}
Field Init Shorthand
When a variable has the same name as the struct field, you can use the shorthand syntax. In the example above, name and age are used directly without writing name: name and age: age.
Accessing Struct Fields
You access struct fields using dot notation:
public fun get_name(profile: &UserProfile): &String {
&profile.name
}
public fun get_age(profile: &UserProfile): u8 {
profile.age
}
Modifying Struct Fields
To modify fields, you need a mutable reference to the struct:
public fun update_age(profile: &mut UserProfile, new_age: u8) {
profile.age = new_age;
}
public fun add_balance(profile: &mut UserProfile, amount: u64) {
profile.balance = profile.balance + amount;
}
Destructuring Structs
You can destructure a struct to extract its fields. This is required when you want to destroy a struct that does not have the drop ability.
public fun destroy_profile(profile: UserProfile): (String, u8, u64) {
let UserProfile { name, age, balance } = profile;
(name, age, balance)
}
You can also partially destructure with .. to ignore fields:
public fun get_balance_from_profile(profile: UserProfile): u64 {
let UserProfile { balance, .. } = profile;
balance
}
Storing Structs in Global Storage
Structs with the key ability can be stored in global storage under an account address:
module my_addr::profiles {
use std::signer;
use std::string::String;
struct UserProfile has key, store {
name: String,
age: u8,
balance: u64,
}
/// Store a profile under the caller's account
public entry fun register(account: &signer, name: String, age: u8) {
let profile = UserProfile {
name,
age,
balance: 0,
};
move_to(account, profile);
}
/// Read the profile for a given address
#[view]
public fun get_profile_age(addr: address): u8 acquires UserProfile {
UserProfile[addr].age
}
}
Nested Structs
Structs can contain other structs as fields:
module my_addr::game {
struct Position has copy, drop, store {
x: u64,
y: u64,
}
struct Player has key, store {
position: Position,
health: u64,
score: u64,
}
}
Best Practices
- Use descriptive field names: Make field names clear and self-documenting.
- Minimize abilities: Only add the abilities your struct actually needs.
- Group related data: If several values are always used together, put them in a struct.
- Document with
///: Add doc comments above your struct definitions explaining their purpose. - Use
keyfor top-level storage: Only structs that need to be stored directly under an address needkey.