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
- Create
rex. At the top ofmainFunction, write a binding namedrexwith the typeOption<Dog>and the valueSome(Dog).Dogis a unit struct, so the name by itself is a complete value. - Create
buddy. On the next line, do the same with a newSome(Dog). This is a second instance, owned bybuddyand independent ofrex. - 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 fromSome(Dog), but the annotation tells the reader what each binding holds. Someis not a keyword. It is a constructor of theOptionenum that is always in scope, likeNone.- A unit struct takes no space. A
Dogwith 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. UseDog,Dog {}or a function likeDog::new(). - Using a moved binding. After
let buddy = rex;, usingrexis a compile error. - Forgetting
Some.let rex: Option<Dog> = Dog;fails with mismatched types, because aDogis not anOption<Dog>. - Using the wrong name. The check refers to
rex,buddyandlunaexactly. Any other name leaves one of them undefined. - Calling
unwrap()onNone. It panics and ends the program.
Key takeaways
- An object is an instance of a struct, made with a struct expression and no
newkeyword. - Every binding owns its instance, and assigning a non-
Copyvalue to another binding moves it. Option<T>is eitherSome(value)orNone, andis_some()tells which.Box::newputs a value on the heap, but ordinary instances don't need it.
