llms.txt
Skip to main content

Custom Types with Struct

A struct is a user-defined type that groups related values into a single unit, giving a name to both the group and each value inside it. If you are familiar with object-oriented languages, a struct is similar to an object's data attributes. Instead of passing around a loose title, artist, and release year, an application can define a Record type and handle all three as one value.

Custom types are the backbone of a Move program: they describe the application's data, and - as later sections will show - the module that defines a type controls everything that can be done with its values. In this section we introduce the struct definition and how to use it.

Defining a Struct

To define a custom type, use the public struct keywords followed by the name of the type, and a block of fields. Each field is defined with the field_name: field_type syntax, and field definitions must be separated by commas. The fields can be of any type, including other structs.

Move does not support recursive structs, meaning a struct cannot contain itself as a field.

use std::string::String;

/// A struct representing an artist.
public struct Artist {
/// The name of the artist.
name: String,
}

/// A struct representing a music record.
public struct Record {
/// The title of the record.
title: String,
/// The artist of the record. Uses the `Artist` type.
artist: Artist,
/// The year the record was released.
year: u16,
/// Whether the record is a debut album.
is_debut: bool,
/// The edition of the record, if defined.
edition: Option<u16>,
}

In the example above, we define an Artist struct with a single field, and a Record struct with five fields. The title field is of type String, the artist field uses the custom Artist type we just defined, the year field is of type u16, the is_debut field is of type bool, and the edition field is of type Option<u16> to represent that the edition is optional.

The String type is not built into the language - it is defined in the Standard Library and brought into scope with the use statement at the top of the example; imports are covered in the Importing Modules section. The angle brackets in Option<u16> denote a type parameter: Option<u16> is an Option that holds a u16. Type parameters are covered in the Generics section.

A struct definition can also declare abilities - properties that relax the default restrictions on values of the type. They are listed with the has keyword, either before the fields - public struct Foo has copy, drop { ... } - or after them, terminated with a semicolon - public struct Foo { ... } has copy, drop;. Abilities are introduced in the Abilities Introduction section.

Creating an Instance

We described the definition of a struct. Now let's see how to create an instance of one. Creating an instance of a struct is called packing, and it is done with the StructName { field1: value1, field2: value2, ... } syntax. The fields can be set in any order, but all of them must be set - a struct cannot be partially initialized.

The examples on this page live inside a test function in the same module that defines the structs - as we are about to see, structs can only be created and taken apart within their module. The assert_eq! used throughout is a macro - hence the ! in the name - that compares two values and fails if they differ; it is covered in the Testing section.

let mut artist = Artist {
name: "The Beatles",
};

In the example above, we create an instance of the Artist struct and set the name field to the string "The Beatles". The value "The Beatles" is a string literal: the compiler sees that the name field expects a String and infers the type of the literal automatically. Strings are covered in more detail in the String section.

Move also offers a shorthand: if a local variable has the same name as the field, the field name can be given just once. This is called field name punning.

let name: String = "Queen";

// The local variable `name` has the same name as the field, so
// instead of `Artist { name: name }` we can write:
let queen = Artist { name };

Accessing Fields

To access the fields of a struct, use the . (dot) operator followed by the field name. Fields can be read, and, if the variable is declared as mut, assigned a new value.

// Read the `name` field of the `Artist` struct.
assert_eq!(artist.name, "The Beatles");

// Mutate the `name` field. Requires `artist` to be declared as `mut`.
artist.name = "Led Zeppelin";

// Check that the `name` field has been mutated.
assert_eq!(artist.name, "Led Zeppelin");

Accessing fields this way works only in the module that defines the struct. To understand why, let's take a closer look at struct visibility.

Field Visibility

As you may have noticed, every struct is declared with the public modifier - it is required, and declaring a struct without it is a compilation error. The public modifier makes the struct type visible to other modules: it can be imported, used in type definitions, and in function signatures.

However, the contents of a struct always stay internal to the module that defines it. Unlike some languages, Move has no per-field visibility modifiers - there is no way to mark a field public. Outside of the defining module it is impossible to:

  • read or write the fields of a struct;
  • create ("pack") an instance of a struct;
  • destroy ("unpack") an instance of a struct.

This is a feature, not a limitation. It means the module has full control over how its types are created, used, and destroyed, and no external code can violate the rules the module sets. In the Object Model chapter, we show how this property is used to model assets and enforce guarantees on them.

Note that just because a struct field is not accessible from other modules does not mean its value is confidential - it is always possible to read the contents of an onchain object from outside of Move. You should never store unencrypted secrets inside of objects.

Getters and Setters

Because fields are only accessible inside the defining module, the module needs to expose public functions if other modules should read or update them. A function that returns the value of a field is conventionally called a getter, and a function that updates a field is called a setter.

A getter typically takes a reference to the struct and returns the field value:

/// Returns the name of the artist. A "getter" for the `name` field.
public fun name(artist: &Artist): String {
artist.name
}

A setter takes a mutable reference to the struct and the new value:

/// Updates the name of the artist. A "setter" for the `name` field.
public fun set_name(artist: &mut Artist, name: String) {
artist.name = name;
}

Both functions can then be called with the . operator, just like field access:

// Call the setter and then the getter defined above.
artist.set_name("Pink Floyd");
assert_eq!(artist.name(), "Pink Floyd");

Because these functions are public, any module that imports Artist can call them. Note the parentheses: artist.name() is a function call and works anywhere the function is visible, while the field access artist.name would not compile outside the defining module.

The public fun syntax defines a public function; functions are covered in detail in the Functions section. The & and &mut in the signatures are references - they allow a function to read or modify a value without taking ownership of it. We cover them in the References section, and the dot-call syntax in the Struct Methods section.

While getters are very common, setters are defined less often, and usually with extra checks. The choice of which functions to expose is what defines the interface of the type - the module decides what external code can and cannot do with its structs.

Unpacking a Struct

Structs are non-discardable by default: a struct value cannot simply be left behind at the end of a function - code that does so will not compile. Every created value must be used: either stored (for example, placed inside another struct, or kept in onchain storage, as shown in the Using Objects chapter) or unpacked. Unpacking a struct means deconstructing it into its fields, and it is the mirror image of packing: the let keyword, followed by the struct name and the field names to bind.

// Unpack the `Artist` struct, binding the value of the `name`
// field to a new variable `name`.
let Artist { name } = artist;

In the example above we unpack the Artist struct and create a new variable name with the value of the name field. The struct value no longer exists after this line - it has been broken up into its parts.

If a field is not needed, it can be ignored by binding it to the underscore _. However, since the struct itself cannot be discarded, all of its fields must still be listed in the pattern:

// Unpack the `Artist` struct and ignore the `name` field.
let Artist { name: _ } = queen;

For structs with many fields, listing every ignored field gets verbose. The .. pattern - the rest pattern - matches all of the remaining fields at once:

let record = Record {
title: "Abbey Road",
artist: Artist { name: "The Beatles" },
year: 1969,
is_debut: false,
edition: option::none(),
};

// Unpack the `Record`, keeping `title` and `artist`, and
// ignoring all of the other fields with `..`.
let Record { title, artist, .. } = record;

assert_eq!(title, "Abbey Road");

// The `artist` binding holds a non-discardable `Artist` value,
// so it, in turn, must be unpacked as well.
let Artist { name: _ } = artist;

In the example above, we pack a full Record - the option::none() call creates an empty Option value, see the Option section - and then unpack it, keeping the title and artist fields and ignoring the rest with ...

Note that ignoring a field - whether with _ or .. - discards its value, which is only allowed for values that can be discarded. Simple values like String, u16, and bool can be discarded freely, but Artist cannot - which is why the example unpacks the artist binding as well instead of ignoring it. Which values can be discarded and which cannot is determined by abilities, explained in the next sections - Abilities Introduction and Ability: Drop.

Positional Structs

So far, every struct on this page had named fields. Move also supports positional structs, whose fields have no names and are identified by their position instead. A positional struct is defined with parentheses instead of curly braces, and the definition has no body - it ends right after the field list:

/// The duration of a record: minutes and seconds.
public struct Duration(u64, u64)

Abilities can be placed before or after the fields here as well; in the post-fix form they follow the parentheses: public struct Duration(u64, u64) has copy, drop;.

Positional structs are packed and unpacked with parentheses as well, and their fields are accessed with the . operator followed by the field index, starting at zero:

// Pack a positional struct - parentheses instead of curly braces.
let duration = Duration(3, 5);

// Access the fields by their position, starting at 0.
assert_eq!(duration.0, 3);
assert_eq!(duration.1, 5);

// Unpack the struct, binding each field by its position.
let Duration(minutes, seconds) = duration;

Positional structs are a good fit when field names would not add anything to what the type name already says - typically in small wrapper types with one or two fields. All of the rules described on this page still apply to them: fields are only accessible within the defining module, and a value must be used - stored or unpacked. For structs with more fields, named fields are usually the better choice.

Further Reading

llms.txt