Escaping from Rust's Borrow Checker

Rust is a a programming language that provides strong assurances about safety. It achieves that by being confident about who is responsible for what at any given time. One part of how it achieves that is through its ownership system and "move semantics".

During a move, values are copied and the original values are marked as invalid by the compiler. This can be a strange thing to get your head around for new Rust programmers as it makes variables in local scope inaccessible later on within a function.

The following code snippet sets up a situation that will break with the error "use of moved value: `car`" when you attempt to compile it:

fn main () {  
  let me = Driver{};
  let car = Car{};

  me.drive(car);
  me.drive(car); // illegal, has been moved
}

struct Driver;  
struct Car;

impl Driver {  
  fn drive(&self, car: Car) -> () {
    println!("Zoom zoom!");
  }
}

The type signature of Driver.drive() tells us the level of authority it needs over its arguments. When you're interepreting a signature such as fn drive(&self, car: Car), the ampersand (&) says "I need a reference reference to self" and no decoration indicates that ownership of car is required.

One of the reasons that taking ownership is an issue later on within local scope of main() earlier is that objects are deleted (via the Drop trait) when their owners no longer need them. That is, once Driver.drive() returns, car is dropped.

Using References

If we are in charge of the code that requires ownership, perhaps we could just adjust our function to not need it?

In our tiny application, our definition of Driver.drive() changes from this..

impl Driver {  
  fn drive(&self, car: Car) -> () {
    println!("Zoom zoom!");
  }
}

..to this:

impl Driver {
  fn drive(&self, car: &Car) -> () {
    println!("Zoom zoom!");
  }
}

Our main() function also changes. We now need to adjust our code to match the signature that we just defined:

fn main () {  
  let me = Driver{};
  let car = Car{};

  me.drive(car);
  me.drive(car); // illegal, has been moved
}

Is now..

fn main () {
  let me = Driver{};
  let car = Car{};

  me.drive(&car);
  me.drive(&car);
}

Now, when we run the code, we hear our car roar!

Zoom, zoom!  
Zoom, zoom!  

Using Clone

Using references is a good sign that you're doing things the Rust way. Duplicating data unnecessarily is frowned upon, as its probably runs slower than using a reference and will almost certainly lead to more memory being used.

Still, the Clone trait is perfectly valid Rust and can be a convienent way to sidestep the borrow checker while you're focusing on learning Rust.

It's quite easy to use. In our main() function, we only need to call car.clone(). Make sure you're cloning at the first call to me.drive(), otherwise you would be trying to clone data that has already been moved.

To implement the trait on our data, you generally need to implement a function that will be able to duplicate the struct you're interested in duplicating. In our case, as we're using a unit struct, we can use use a code annotation to have the implementation written for us by the Rust compiler.

fn main () {
  let me = Driver{};
  let car = Car{};

  me.drive(car.clone()); 
  me.drive(car);
}

struct Driver;
#[derive(Clone)]
struct Car;

impl Driver {
  fn drive(&self, car: Car) -> () {
    println!("Zoom zoom!");
  }
}

When you run that code, you should see the familiar:

Zoom, zoom!  
Zoom, zoom!  

Using Copy

If you are fairly new to Rust, you may be wondering why issues with the borrow checker don't seem to crop up when dealing with integers and floats. That's because they're not being moved, they're being copied.

Copy is available to a few primitive data types (e.g. number types and a few others) and types that only use number types. Luckily for us, unit structs don't have any data members and are also able to make use of copy.

To make use of Copy in our code, our program changes to the the following:

fn main () {
  let me = Driver{};
  let car = Car{};

  me.drive(car); 
  me.drive(car);
}

struct Driver;
#[derive(Clone, Copy)]
struct Car;

impl Driver {
  fn drive(&self, car: Car) -> () {
    println!("Zoom zoom!");
  }
}

The application code, e.g. main() becomes simpler here. Unfortunately though, Copy is only available for a fairly limited number of use cases. Still, it's available to you when you might need it.

Wrapping Up

Programming in Rust, can feel pedantic and perhaps a little stuffy at first. Everyone has been stung by the borrow checker. Its job is to prevent you from being stung much more severely at runtime under load. Hopefully these little tricks have explained what's going on and have offered you some tips if you get stuck.