Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Basics of Ownership: Rust Crash Course Lesson 2, Part 1

DZone's Guide to

Basics of Ownership: Rust Crash Course Lesson 2, Part 1

We continue this series on the Rust language by exploring the various concepts surrounding ownership in Rust. Read on to get Rusty!

· Web Dev Zone ·
Free Resource

Bugsnag monitors application stability, so you can make data-driven decisions on whether you should be building new features, or fixing bugs. Learn more.

Arguably the biggest distinguishing aspect of Rust versus other popular programming languages is its ownership model. In this lesson, we’re going to hit the basics of ownership in Rust. You can read much more in the Rust book chapter on ownership.

This post is part of a series based on teaching Rust at FP Complete. If you’re reading this post outside of the blog, you can find links to all posts in the series at the top of the introduction post. You can also subscribe to the RSS feed.

Format

I’m going to be experimenting a bit with lesson format. I want to cover both:

  • More theoretical discussions of ownership.
  • Trying to implement an actual program.

As time goes on in this series, I intend to spend more time on the latter and less on the former, though we still need significant time on the former right now. I’m going to try approaching this by having the beginning of this post discuss ownership, and then we’ll implement the first version of bouncy afterward.

This format may feel a bit stilted, feedback appreciated if this approach works for people.

Comparison With Haskell

I’m going to start by comparing Rust with Haskell since both languages have a strong concept of immutability. However, Haskell is a garbage collected language. Let’s see how these two languages compare. In Haskell:

  • Everything is immutable by default.
  • You use explicit mutability wrappers (like IORef or MVar) to mark mutability.
  • References to data can be shared however much you like.
  • Garbage collection frees up memory non-deterministically.
  • When you need deterministic resource handling (like file handles), you need to use the bracket pattern or similar.

In Rust, data ownership is far more important: it’s a primary aspect of the language and allows the language to bypass garbage collection. It also allows data to often live on the stack instead of the heap, leading to better performance. Also, it runs deterministically, making it a good approach for handling other resources like file handles.

Ownership starts off with the following rules:

  • Each value in Rust has a variable that’s called its owner.
  • There can only be one owner at a time.
  • When the owner goes out of scope, the value will be dropped.

Simple Example

Consider this code:

#[derive(Debug)]
struct Foobar(i32);

fn uses_foobar(foobar: Foobar) {
    println!("I consumed a Foobar: {:?}", foobar);
}

fn main() {
    let x = Foobar(1);
    uses_foobar(x);
}

Syntax note#[...] is a compiler pragma. #[derive(...)] is one example, and is similar to using deriving for type classes in Haskell. For some traits, the compiler can automatically provide an implementation if you ask it.

Syntax note{:?} in a format string means “use the Debug trait for displaying this.”

Foobar is what’s known as a newtype wrapper: it’s a new data type wrapping around, and with the same runtime representation of, a signed 32-bit integer (i32).

In the main function, x contains a Foobar. When it calls uses_foobar, ownership of that Foobar passes to uses_foobar. Using that x again in main would be an error:

#[derive(Debug)]
struct Foobar(i32);

fn uses_foobar(foobar: Foobar) {
    println!("I consumed a Foobar: {:?}", foobar);
}

fn main() {
    let x = Foobar(1);
    uses_foobar(x);
    uses_foobar(x);
}

Results in:

error[E0382]: use of moved value: `x`
  --> foo.rs:11:17
   |
10 |     uses_foobar(x);
   |                 - value moved here
11 |     uses_foobar(x);
   |                 ^ value used here after move
   |
   = note: move occurs because `x` has type `Foobar`, which does not implement the `Copy` trait

error: aborting due to previous error

For more information about this error, try `rustc --explain E0382`.

Side note on copy trait way below…

Dropping

When a value goes out of scope, its Drop trait (like a type class) is used, and then the memory is freed. We can demonstrate this by writing a Drop implementation for Foobar:

Challenge: Guess what the output of the program below is before seeing the output.

#[derive(Debug)]
struct Foobar(i32);

impl Drop for Foobar {
    fn drop(&mut self) {
        println!("Dropping a Foobar: {:?}", self);
    }
}

fn uses_foobar(foobar: Foobar) {
    println!("I consumed a Foobar: {:?}", foobar);
}

fn main() {
    let x = Foobar(1);
    println!("Before uses_foobar");
    uses_foobar(x);
    println!("After uses_foobar");
}

Output

Before uses_foobar
I consumed a Foobar: Foobar(1)
Dropping a Foobar: Foobar(1)
After uses_foobar

Notice that the value is dropped before After uses_foobar. This is because the value was moved into uses_foobar, and when that function exits, the drop is called.

Exercise 1: There’s a function in the standard library, std::mem::drop, which drops a value immediately. Implement it.

Lexical Scoping

Scoping in Rust is currently lexical, though there’s a Non-Lexical Lifetimes (NLL) proposal being merged in (there’s a nice explanation on Stack Overflow of what NLL does). We can demonstrate the currently-lexical nature of scoping:

#[derive(Debug)]
struct Foobar(i32);

impl Drop for Foobar {
    fn drop(&mut self) {
        println!("Dropping a Foobar: {:?}", self);
    }
}

fn main() {
    println!("Before x");
    let _x = Foobar(1);
    println!("After x");
    {
        println!("Before y");
        let _y = Foobar(2);
        println!("After y");
    }
    println!("End of main");
}

Syntax note: Use a leading underscore _ for variables that are not used.

The output from this program is:

Before x
After x
Before y
After y
Dropping a Foobar: Foobar(2)
End of main
Dropping a Foobar: Foobar(1)

Challenge: Remove the seemingly-superfluous braces and run the program. Extra points: guess what the output will be before looking at the actual output.

Borrows/References (Immutable)

Sometimes you want to be able to share a reference to a value without moving ownership. Easy enough:

#[derive(Debug)]
struct Foobar(i32);

impl Drop for Foobar {
    fn drop(&mut self) {
        println!("Dropping a Foobar: {:?}", self);
    }
}

fn uses_foobar(foobar: &Foobar) {
    println!("I consumed a Foobar: {:?}", foobar);
}

fn main() {
    let x = Foobar(1);
    println!("Before uses_foobar");
    uses_foobar(&x);
    uses_foobar(&x);
    println!("After uses_foobar");
}

Things to notice:

  • uses_foobar now takes a value of type &Foobar, which is “immutable reference to a Foobar.”
  • Inside uses_foobar, we don’t need to explicitly dereference the foobar value, this is done automatically by Rust.
  • In main, we can now use x in two calls to uses_foobar
  • In order to create a reference from a value, we use & in front of the variable.

Challenge: When do you think the Dropping a Foobar: message gets printed?

Remember from the last lesson that there is special syntax for a parameter called self. The signature of drop above looks quite different from uses_foobar. When you see &mut self, you can think of it as self: &mut Self. Now it looks more similar touses_foobar.

Exercise 2: We’d like to be able to use object syntax for uses_foobar as well. Create a method use_it on the Foobar type that prints the I consumed message. Hint: you’ll need to do this inside impl Foobar { ... }.

Multiple Live References

We can change our main function to allow two references to x to live at the same time. This version also adds explicit types on the local variables, instead of relying on type inference:

fn main() {
    let x: Foobar = Foobar(1);
    let y: &Foobar = &x;
    println!("Before uses_foobar");
    uses_foobar(&x);
    uses_foobar(y);
    println!("After uses_foobar");
}

This is allowed in Rust, because:

  1. Multiple read-only references to a variable cannot result in any data races.
  2. The lifetime of the value outlives the references to it. In other words, in this case, x lives at least as long as y.

Let’s see two ways to break this.

Reference Outlives Value

Remember that std::mem::drop from before? Check this out:

fn main() {
    let x: Foobar = Foobar(1);
    let y: &Foobar = &x;
    println!("Before uses_foobar");
    uses_foobar(&x);
    std::mem::drop(x);
    uses_foobar(y);
    println!("After uses_foobar");
}

This results in the error message:

error[E0505]: cannot move out of `x` because it is borrowed
  --> foo.rs:19:20
   |
16 |     let y: &Foobar = &x;
   |                       - borrow of `x` occurs here
...
19 |     std::mem::drop(x);
   |                    ^ move out of `x` occurs here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0505`.

Mutable Reference With Other References

You can also take mutable references to a value. In order to avoid data races, Rust does not allow value to be referenced mutably and accessed in any other way at the same time.

fn main() {
    let mut x: Foobar = Foobar(1);
    let y: &mut Foobar = &mut x;
    println!("Before uses_foobar");
    uses_foobar(&x);
    uses_foobar(y);
    println!("After uses_foobar");
}

Notice how the type of y is now &mut Foobar. Like Haskell, Rust tracks mutability at the type level. Yay!

Challenge

Try to guess which lines in the code below will trigger a compilation error:

#[derive(Debug)]
struct Foobar(i32);

fn main() {
    let x = Foobar(1);

    foo(x);
    foo(x);

    let mut y = Foobar(2);

    bar(&y);
    bar(&y);

    let z = &mut y;
    bar(&y);
    baz(&mut y);
    baz(z);
}

// move
fn foo(_foobar: Foobar) {
}

// read only reference
fn bar(_foobar: &Foobar) {
}

// mutable reference
fn baz(_foobar: &mut Foobar) {
}

Mutable Reference vs. Mutable Variable

Something I didn’t explain above was the mut before x in:

fn main() {
    let mut x: Foobar = Foobar(1);
    let y: &mut Foobar = &mut x;
    println!("Before uses_foobar");
    uses_foobar(&x);
    uses_foobar(y);
    println!("After uses_foobar");
}

By default, variables are immutable, and therefore do not allow any kind of mutation. You cannot take a mutable reference to an immutable variable, and therefore x must be marked as mutable. Here’s an easier way to see this:

#[derive(Debug)]
struct Foobar(i32);

fn main() {
    let mut x = Foobar(1);

    x.0 = 2; // changes the 0th value inside the product

    println!("{:?}", x);
}

If you remove the mut, this will fail.

Moving Into Mutable

This bothered me, and I assume it will bother other Haskellers. As just mentioned, the following code will not compile:

#[derive(Debug)]
struct Foobar(i32);

fn main() {
    let x = Foobar(1);

    x.0 = 2; // changes the 0th value inside the product

    println!("{:?}", x);
}

Obviously, you can’t mutate x. But let’s change this ever so slightly:

#[derive(Debug)]
struct Foobar(i32);

fn main() {
    let x = Foobar(1);
    foo(x);
}

fn foo(mut x: Foobar) {

    x.0 = 2; // changes the 0th value inside the product

    println!("{:?}", x);
}

Before learning Rust, I would have objected to this: x is immutable, and therefore we shouldn’t be allowed to pass it to a function that needs a mutable x. However, this isn’t how Rust views the world. The mutability here is a feature of the variable, not the value itself. When you move the x into foo, main no longer has access to x, and doesn’t care if it’s mutated. Inside foo, we’ve explicitly stated that x can be mutated, so we’re cool.

This is fairly different from how Haskell looks at things.

Copy Trait

We touched on this topic last time with numeric types vs String. Let’s hit it a little harder. Will the following code compile or not?

fn uses_i32(i: i32) {
    println!("I consumed an i32: {}", i);
}

fn main() {
    let x = 1;
    uses_i32(x);
    uses_i32(x);
}

It shouldn’t work, right? x is moved into uses_i32, and then used again. However, it compiles just fine! What gives?

Rust has a special trait, Copy, which indicates that a type is so cheap that it can automatically be passed-by-value. That’s exactly what happens with i32. You can explicitly do this with the Clone trait if desired:

#[derive(Debug, Clone)]
struct Foobar(i32);

impl Drop for Foobar {
    fn drop(&mut self) {
        println!("Dropping: {:?}", self);
    }
}

fn uses_foobar(foobar: Foobar) {
    println!("I consumed a Foobar: {:?}", foobar);
}

fn main() {
    let x = Foobar(1);
    uses_foobar(x.clone());
    uses_foobar(x);
}

Challenge: Why don’t we need to use x.clone() on the second uses_foobar? What happens if we put it in anyway?

Exercise 3: Change the code below, without modifying the main function at all, so that it compiles and runs successfully. Some hints: Debug is a special trait that can be automatically derived, and in order to have a Copy implementation, you also need a Cloneimplementation.

#[derive(Debug)]
struct Foobar(i32);

fn uses_foobar(foobar: Foobar) {
    println!("I consumed a Foobar: {:?}", foobar);
}

fn main() {
    let x = Foobar(1);
    uses_foobar(x);
    uses_foobar(x);
}

Lifetimes

The term that goes along most with ownership is lifetimes. Every value needs to be owned, and it's owned for a certain lifetime until it’s dropped. So far, everything we’ve looked at has involved implicit lifetimes. However, as code gets more sophisticated, we need to be more explicit about these lifetimes. We’ll cover that another time.

Exercise 4

Add an implementation of the double function to get this code to compile, run, and output the number 2:

#[derive(Debug)]
struct Foobar(i32);

fn main() {
    let x: Foobar = Foobar(1);
    let y: Foobar = double(x);
    println!("{}", y.0);
}

Remember: to provide a return value from a function, put -> ReturnType after the parameter list.

Notes on Structs and Enums

I mentioned above that struct Foobar(i32) is a newtype around an i32. That’s actually a special case of a more general positional struct, where you can have 0 or more fields, named by their numeric position. And positions start numbering at 0, as god and Linus Torvalds intended.

There are some more examples:

struct NoFields; // may seem strange, we might cover examples of this later
struct OneField(i32);
struct TwoFields(i32, char);

You can also use record syntax:

struct Person {
    name: String,
    age: u32,
}

structs are known as product types, which means they contain multiple values. Rust also provides enums, which are sum types, or tagged unions. These are alternatives, where you select one of the options. A simple enum would be:

enum Color {
    Red,
    Blue,
    Green,
}

But enums variants can also take values:

enum Shape {
    Square(u32), // size of one side
    Rectangle(u32, u32), // width and height
    Circle(u32), // radius
}

That's all for Part 1. Tune in next time as we use the principles discussed today to create a basic app using Rust. 

Monitor application stability with Bugsnag to decide if your engineering team should be building new features on your roadmap or fixing bugs to stabilize your application.Try it free.

Topics:
rust programming language ,rust ownership ,web dev ,tutorial

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}