Hello! Welcome back to our course on Solana development.
In our last few lessons, we've built a solid foundation. We started with Rust's basic data types, then dove into its unique ownership system, and finally explored how borrowing and references allow us to access data safely without transferring ownership. You now have the tools to manage how data and memory are handled in a Rust program.
Today, we'll take the next logical step: creating our own custom, complex data types using structs. While primitive types like integers and booleans are essential, real-world applications require us to group related data into meaningful structures. For a Solana program, this might be a user profile, a game item, or the state of a financial contract. Your experience with objects in JavaScript or interfaces in TypeScript will be a helpful reference point here; structs serve a very similar purpose in Rust.
By the end of this lesson, you will be able to define your own custom data types by defining and instantiating structs.
What is a Struct?
A struct, short for "structure," is a custom data type that lets you name and package together multiple related values that make up a meaningful group. It's a way to move beyond simple tuples and give each piece of data a clear name, or field.
Let's start by reading the introduction from the official Rust book, which clearly explains the concept and syntax.
Defining and Instantiating Structs
Please read the following section from 'The Rust Programming Language' book. It introduces the basic syntax for defining a struct and creating an instance of it.
Read the entire first section titled 'Defining and Instantiating Structs'. Focus on the User struct example and how an instance user1 is created.
As you've just read, defining a struct is like creating a template for a new type. Instantiating it is like creating a concrete value from that template.
Defining, Instantiating, and Accessing Structs
Now, let's see this in action. The following video provides a practical, step-by-step demonstration of working with structs.
Demo: Structs [16 of 35] | Rust for Beginners
This video from Microsoft Developer provides a clear demonstration of how to define, instantiate, and use a classic struct with named fields.
Watch the video from the beginning up to 02:04. This will cover: Defining a Person struct with named fields. Creating an instance of the Person struct. Accessing the struct's fields using dot notation.
To summarize the key points from the video and the reading:
- Definition: Use the
structkeyword followed by the name and a block offield: typepairs.struct User { username: String, email: String, active: bool, } - Instantiation: Create an instance by specifying the struct name and
key: valuepairs for its fields. The order of fields does not matter during instantiation.let user1 = User { email: String::from("user@example.com"), username: String::from("user123"), active: true, }; - Access: Use dot notation (
.) to get the value of a specific field.println!("User's email: {}", user1.email); - Mutability: If you want to change a field's value, the entire struct instance must be declared as mutable with
mut. Rust doesn't allow marking individual fields as mutable.let mut user2 = User { /* ... */ }; user2.email = String::from("new_email@example.com"); // This is allowed // user1.active = false; // This would cause a compile error because user1 is not mutable
Convenient Shorthands
Rust provides some nice syntactic sugar to make working with structs more concise.
- Field Init Shorthand: When a variable or function parameter has the same name as a struct field, you can use a shorthand instead of writing
field: variable. This is extremely common in "constructor" functions. - Struct Update Syntax: To create a new instance of a struct that uses most of an old instance’s values, you can use the
..syntax. This is very useful in functional-style programming and for updating state in Solana programs.
The Rust book explains these two features very clearly.
Defining and Instantiating Structs
Let's continue with the Rust book to learn about two powerful shorthands for initializing structs.
Read the sections 'Using the Field Init Shorthand' and 'Creating Instances from Other Instances with Struct Update Syntax'.
Here is a quick example combining both features:
fn build_user(email: String, username: String) -> User {
User {
email, // Field init shorthand
username, // Field init shorthand
active: true,
}
}
fn main() {
let user1 = build_user(String::from("one@test.com"), String::from("user_one"));
// Create a new user based on user1, but with a different email
let user2 = User {
email: String::from("two@test.com"),
..user1 // Struct update syntax for username and active
};
// Note: because `user1.username` (a String) was moved to `user2`,
// you can no longer use `user1.username`. The `user1` variable is partially moved.
}
Other Types of Structs
Besides the classic named-field struct, Rust offers two other variations. The following short video gives a great overview of all three.
This '300 seconds of Rust' video quickly introduces the three kinds of structs available in Rust.
Watch from the beginning to 01:22. This covers named-field structs, which we've seen, and introduces tuple structs.
Let's break them down:
1. Tuple Structs
These are useful when you want to give a whole tuple a name and make it a distinct type, but naming each field would be redundant. For example, Point and Color are classic use cases.
struct Color(u8, u8, u8); // RGB
struct Point(i32, i32, i32); // x, y, z
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
// Access elements with dot notation and index
println!("The first value of black is: {}", black.0);
// `black` and `origin` are different types, even if they have the same structure.
// A function expecting a `Color` cannot accept a `Point`.
}
2. Unit-Like Structs
These structs have no fields at all. They are useful when you need to implement a trait on some type but don't have any data you want to store in the type itself. We'll explore traits in a later module, but it's good to know this exists.
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
// We can now work with the `subject` instance.
}
Test your understanding!
In Solana, every account has some core properties. Let's model a simplified version of a Solana account using a struct.
- Define a struct named
SolanaAccountwith the following named fields:lamports: Au64to store the balance.owner: AStringto store the public key of the program that owns the account.executable: Aboolflag.
- In your
mainfunction, create aSolanaAccountinstance for a user's wallet. It should have1_000_000_000lamports (1 SOL), be owned by theSystemProgram, and not be executable. - Next, create a second account for a program. Use the struct update syntax to base it on the first account, but change
executabletotrueand theownertoMyProgram.
Show answer
Here's one possible solution:
// 1. Define the struct
struct SolanaAccount {
lamports: u64,
owner: String,
executable: bool,
}
fn main() {
// 2. Create the first instance for a user's wallet
let wallet_account = SolanaAccount {
lamports: 1_000_000_000,
owner: String::from("SystemProgram"),
executable: false,
};
println!("Wallet Account Owner: {}", wallet_account.owner);
// 3. Create the second instance for a program account
let program_account = SolanaAccount {
owner: String::from("MyProgram"),
executable: true,
..wallet_account // Use the `lamports` value from wallet_account
};
println!("Program Account Owner: {}", program_account.owner);
println!("Program Account Lamports: {}", program_account.lamports);
// Note: after this, `wallet_account.owner` is moved and cannot be used.
// However, `wallet_account.lamports` and `wallet_account.executable` can still be used
// because u64 and bool implement the `Copy` trait.
}
A Practical Matter: Printing Structs for Debugging
You've defined a struct, but what happens when you try to print it?
// This code won't compile!
// println!("My account: {}", wallet_account);
By default, Rust doesn't know how to display a custom struct for end-users. However, it provides a very easy way to "opt-in" to a debug format that is perfect for developers.
An Example Program Using Structs
The following resource explains why you can't just print a struct and shows you the simple attribute needed to enable debug printing. It's a must-know for practical development.
Read the section 'Adding Useful Functionality with Derived Traits'. Pay close attention to the #[derive(Debug)] attribute and the {:?} and {:#?} format specifiers.
The key takeaway is to add #[derive(Debug)] above your struct definition. This automatically implements a Debug trait for your struct, allowing you to print it using the {:?} (compact) or {:#?} (pretty-print) format specifiers.
#[derive(Debug)] // Opt-in to debug printing
struct SolanaAccount {
lamports: u64,
owner: String,
executable: bool,
}
fn main() {
let wallet_account = SolanaAccount { /* ... */ };
// Now this works!
println!("My account (compact): {:?}", wallet_account);
println!("My account (pretty):\n{:#?}", wallet_account);
// The dbg! macro is also very useful
dbg!(&wallet_account);
}
A Final Note on Structs and Ownership
You may have noticed we used the owned String type in our examples, not the borrowed &str slice type. This was a deliberate choice. We generally want a struct to own all of its data. This ensures the data inside the struct is valid for as long as the struct itself is valid.
Storing references (&str) inside a struct is possible, but it requires an advanced feature called lifetimes to guarantee that the references don't outlive the data they point to. We'll cover lifetimes much later in the course. For now, the best practice is to use owned types like String, Vec, and other structs inside your structs.
Conclusion
Excellent work! You've just learned one of the most fundamental building blocks in Rust. With structs, you can now move beyond primitive types and start modeling the complex data that your applications will manage.
Here are the key takeaways from this lesson:
- Structs are used to create custom data types by grouping related values into named fields.
- Rust has three types of structs: named-field structs (the most common), tuple structs, and unit-like structs.
- You instantiate a struct using
key: valuepairs and access its fields with dot notation (.). - Convenient features like field init shorthand and struct update syntax make working with structs more ergonomic.
- To make your structs printable for debugging, you must add the
#[derive(Debug)]attribute and use the{:?}format specifier.
So far, our structs only hold data. But what if we want to add behavior to them? For example, how could we add a function to our SolanaAccount that checks if it has enough balance for a transaction?
In our next lesson, we will learn how to implement methods on structs using impl blocks. This will allow us to bundle data and the functions that operate on that data together, which is a cornerstone of building robust and maintainable programs.
Can't find a good explanation? Sign up and we'll make it for you
Sign up