rust-structs-definicion-instanciacion

Structs in Rust: creating and using custom data types

  • 5 min

An struct is a custom type that groups related fields under a single name.

Up to this point in the course, we have been manipulating atoms: an i32 integer, an f64 float, a bool boolean. Programming is about modeling reality, and in reality, things are complex molecules made up of many parts.

If you come from object-oriented languages (C#, Java, C++), you will be looking for classes. In Rust, we don’t have classes, we have structs (structures).

Although at first glance they seem the same (they group data), Rust’s structs are closer to C structs or plain data objects (POCO/DTO) than to Java classes. The data and its implementation blocks are declared separately.

Today we are going to see how to define the shape of our data.

Defining a classic struct

A struct allows us to name and package multiple related values. Each piece of data is called a field.

To define it, we use the keyword struct, give it a name (in PascalCase), and define its fields and types within braces.

struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}
Copied!

Note on String vs &str: Notice that in the struct we used String, not &str. This is intentional. We want the struct to be the owner of its data. If we used &str (references), we would need to use Lifetimes ('a), an advanced concept we will cover later. For now, always use String inside structs.

Creating an instance

Once the blueprint is defined, we can build (instantiate) concrete objects. We don’t use the new keyword like in other languages. We simply write the struct name and fill in the key-value fields.

fn main() {
    // Create an immutable instance
    let user1 = User {
        email: String::from("[email protected]"),
        username: String::from("luisllamas"),
        active: true,
        sign_in_count: 1,
    };

    // Access fields with dot notation (.)
    println!("The user is: {}", user1.username);
}
Copied!

The order in which we define the fields does not matter when instantiating.

Mutability in structs

You cannot mark only one field as mutable. Mutability is a property of the entire instance, not its parts.

  • If the variable is let, all of the struct is immutable.
  • If the variable is let mut, all fields are mutable.
fn main() {
    let mut user1 = User {
        email: String::from("[email protected]"),
        username: String::from("admin"),
        active: true,
        sign_in_count: 0,
    };

    user1.email = String::from("[email protected]"); // ✅ Correct
    // user1.username is also mutable
}
Copied!

Syntax shortcuts

Rust has a couple of tricks to write less code when initializing structs, very similar to those found in modern JavaScript.

If you have variables with the same name as the fields of the struct, there is no need to repeat field: field.

fn create_user(email: String, username: String) -> User {
    User {
        email,      // Equivalent to email: email
        username,   // Equivalent to username: username
        active: true,
        sign_in_count: 0,
    }
}
Copied!

Often you will want to create a new instance of a struct based on another, changing only a few values. Instead of manually copying field by field, we use the update syntax ...

fn main() {
    let user1 = User {
        email: String::from("[email protected]"),
        username: String::from("user1"),
        active: true,
        sign_in_count: 0,
    };

    // Create user2 copying EVERYTHING from user1, except the email
    let user2 = User {
        email: String::from("[email protected]"),
        ..user1 // <--- Reuse the remaining fields
    };
}
Copied!

Beware of Ownership: The ..user1 syntax moves the data. In the previous example, the username field is a String (it does not implement Copy). Therefore, user1’s username has been moved to user2.

Result: user1 is partially moved and we can no longer use it as a complete value. We could still access fields that were not moved, like email or Copy fields, but not username.

Sometimes you want a new data type, but naming each field is redundant or excessive. For this, Rust offers Tuple Structs. They are hybrids between tuples and structs: they have a type name, but their fields are anonymous.

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);

    // Access by index, like tuples
    println!("The red component is: {}", black.0);
}
Copied!

What is this for if we already have normal tuples (i32, i32, i32)? To provide meaning and type safety.

Although Color and Point store the same data (three integers), for Rust they are different types. You cannot pass a Color variable to a function expecting a Point. This prevents silly bugs where you mix coordinates with RGB colors.

Unit-like Structs

Finally, we can define structs that have no fields.

struct AlwaysEqual;
Copied!

They behave like the unit type (). They might seem useless now, but they become very important when we work with Traits. Sometimes we need a type just to implement a behavior (an interface), without needing to store any data.