Start learning free

LESSON 208 OF 414 · FUNCTIONS & CLASSES

Creating Objects in Rust: Struct Instances and Option

Lesson Read the full explanation

TL ; DR:

  • An object is an instance of a struct — a real, usable copy made from the blueprint.


  • In Rust, you create a struct instance using the struct name followed by curly braces. No new keyword:

    let rex = Dog {};
    

  • Each binding creates a separate instance, even if they all come from the same struct.



Full lesson:

In the previous lesson you defined the Dog struct — the blueprint. But a blueprint isn't a dog. To get an actual dog you can interact with, you need to create an instance of the struct. An instance is also called an object.


Stack-allocated instances

The simplest way to create an instance in Rust is to use a struct literal:

let rex = Dog {};

This creates a Dog instance on the stack, bound to the name rex. Notice there's no new keyword — Rust uses struct literal syntax to create instances directly.


Many instances from one struct

You can create as many Dog instances as you want, and each one is independent:

let rex = Dog {};
let buddy = Dog {};
let luna = Dog {};

rex, buddy, and luna are all Dog instances, but they're different instances, each owned by its own binding. Modifying one won't affect the others.


Heap allocation (advanced — not used here)

Rust does also support heap allocation via Box::new(), which allocates on the heap and is automatically freed when it goes out of scope:

let rex = Box::new(Dog {});
// Automatically dropped when rex goes out of scope

Don't use this style for these lessons — stack instances are simpler and Rust's ownership system handles cleanup automatically.


Your task

The Dog struct is already defined for you as a unit struct (struct Dog;), so the name Dog by itself is a complete value. At the top of mainFunction, declare three Dog instances named rex, buddy, and luna, each wrapped in Some and typed Option<Dog>, for example let rex: Option<Dog> = Some(Dog);. The pre-filled validation will print one line per dog you've successfully created:

rex exists
buddy exists
luna exists
STEP 1 OF 3
    Ctrl + Enter to check

    RUST · FUNCTIONS & CLASSES

    Creating Objects in Rust, explained

    From blueprint to instance

    A struct is a blueprint, and a blueprint is not a dog. To get something you can use, you create an instance of the struct, also called an object. This lesson builds on the Dog struct from the previous one and creates three separate dogs.

    Rust has no new keyword. An instance comes into being when you write a struct expression: the struct's name, followed by its fields in braces. For a struct with no fields, the braces are empty.

    struct Dog {
        age: i32,
    }
    
    fn main() {
        let rex = Dog { age: 4 };
        println!("{}", rex.age); // 4
    }
    

    When a struct is declared as a unit struct, struct Dog;, the name alone is already a value, so Dog and Dog {} both create an instance.

    Many instances from one struct

    One struct can produce as many instances as you like, and each one is independent. Each binding owns its own data, so changing one doesn't touch the others:

    let mut rex = Dog { age: 4 };
    let buddy = Dog { age: 4 };
    rex.age += 1;
    println!("{} {}", rex.age, buddy.age); // 5 4
    

    Rust is strict about who owns an instance. Assigning one binding to another moves the value rather than copying it:

    let rex = Dog { age: 4 };
    let buddy = rex;
    let again = rex; // error: use of moved value: `rex`
    

    After let buddy = rex; the value belongs to buddy, and rex can't be used any more. To get a second, independent copy, create another instance, or derive Clone on the struct and call .clone().

    Why the lesson wraps each dog in Option

    The exercise has a pre-written check at the bottom of mainFunction that asks rex.is_some(), buddy.is_some() and luna.is_some(). Those calls exist on Option<T>, Rust's type for "a value that may or may not be there". An Option<Dog> is either Some(dog), holding a dog, or None, holding nothing.

    So each dog is created inside Some(...), and each binding gets the type Option<Dog>. The check then prints one line for every variable that holds an object:

    let mut maybe: Option<Dog> = None;
    println!("{}", maybe.is_some()); // false
    maybe = Some(Dog { age: 9 });
    println!("{}", maybe.is_some()); // true
    

    Reading the value back out is done with a pattern such as if let Some(d) = &maybe. Calling unwrap() on a None panics at run time, so use it only when you are sure a value is there.

    The lesson's exercise, step by step

    1. Create rex. At the top of mainFunction, write a binding named rex with the type Option<Dog> and the value Some(Dog). Dog is a unit struct, so the name by itself is a complete value.
    2. Create buddy. On the next line, do the same with a new Some(Dog). This is a second instance, owned by buddy and independent of rex.
    3. Create luna. The third line shows that one struct can produce any number of instances.

    The solution in Rust

    struct Dog;
    
    fn mainFunction() {
        let rex: Option<Dog> = Some(Dog);
        let buddy: Option<Dog> = Some(Dog);
        let luna: Option<Dog> = Some(Dog);
    
        if rex.is_some() { println!("rex exists"); }
        if buddy.is_some() { println!("buddy exists"); }
        if luna.is_some() { println!("luna exists"); }
    }
    

    Running mainFunction prints:

    rex exists
    buddy exists
    luna exists
    

    Details to know:

    • The type annotation. Option<Dog> is written out for clarity. Rust would infer it from Some(Dog), but the annotation tells the reader what each binding holds.
    • Some is not a keyword. It is a constructor of the Option enum that is always in scope, like None.
    • A unit struct takes no space. A Dog with no fields has size 0, so there is nothing to store, but the three bindings are still three separate values.
    • Checking for the object. if rex.is_some() { ... } needs the braces around the body, and it needs no parentheses around the condition.

    Heap allocation with Box

    Rust can also place an instance on the heap with Box::new:

    let rex = Box::new(Dog { age: 4 });
    

    The Box owns the value and frees it when the box goes out of scope. Ownership and scope already take care of cleanup for ordinary instances, so a plain struct expression is the normal choice. Reach for Box when you need something specific, for example a type whose size isn't known at compile time.

    Common mistakes

    • Looking for new. let rex = new Dog(); is not Rust. Use Dog, Dog {} or a function like Dog::new().
    • Using a moved binding. After let buddy = rex;, using rex is a compile error.
    • Forgetting Some. let rex: Option<Dog> = Dog; fails with mismatched types, because a Dog is not an Option<Dog>.
    • Using the wrong name. The check refers to rex, buddy and luna exactly. Any other name leaves one of them undefined.
    • Calling unwrap() on None. It panics and ends the program.

    Key takeaways

    • An object is an instance of a struct, made with a struct expression and no new keyword.
    • Every binding owns its instance, and assigning a non-Copy value to another binding moves it.
    • Option<T> is either Some(value) or None, and is_some() tells which.
    • Box::new puts a value on the heap, but ordinary instances don't need it.
    Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free