DEV Community

Cover image for The Borrow Checker and Memory Management
Steven Trotter
Steven Trotter

Posted on • Edited on

The Borrow Checker and Memory Management

Introduction

I've been playing around with Rust for about 12 months now on and off and one of the things I've learned is I love Rust. Whether I can justify Rust as something I would happily use for production stuff in my day to day job is another story (I think it's real close though). But I'd love to one day, I mean really love to.

So why is Rust great and worth your time. Well, recently MicroSoft said they're interested in it (see here), it's used behind the scenes in places in both AWS and Azure (see this link for AWS and this link for Azure), it can do green threading (https://tokio.rs/) and the performance is incredible (comparable to C/C++, from various internet sources and experience). Essentially it's worthy of several articles in itself, google "why do developers love Rust".

So why do I say I can't justify moving away from higher level languages like Java, JavaScript or Python. A few reasons:

  • No pulling punches, it's hard to get started. One problem is can we actually find developers to develop and maintain existing Rust without paying a fortune because they're scarce? I'm hoping in time there will be but for now it's probably easier to find Java or JavaScript devs.
  • Unit testing isn't great in my view. They probably haven't concentrated on this much as the philosophy seems to be "if it compiles, it probably works". Honestly, even if that's true (which it isn't, even for Rust) I still want to unit test properly. It's not dreadful, it can do it, in fact it's got pretty decent foundations. But when I compare it to working with JavaScript or Python... it's missing decent mocking libraries badly. As an addendum I found this since writing this and maybe that fits my requirements, I haven't fully checked yet.
  • For cloud technologies we're in infancy here really. I haven't played with this much but a quick google suggests this to be the case. Whilst trying to get Java/Node/Python stuff running on AWS or Azure there's tonnes of stuff around (often created by the vendors), but Rust, it's just not on peoples radar enough yet. Again I hope and think it will be more so in the next few years. I notice that we can write serverless stuff in Rust now though which is great so this stuff is certainly going somewhere.
  • Green threading needs work still. Comparing this with Go here which, whilst I strongly prefer Rust overall, gives me super nice green threading. In Rust it's just harder to achieve the same thing. I kinda prefer that Rust doesn't have this kind of thing coded into its runtime and Rust can do this with the Tokio crate which is great, but it's hard. I'd still recommend using Tokio and there is some serious stuff written using it, but for the faint hearted it is not. I believe this will improve dramatically in time, not even necessarily that long from now and it's not really a fair complaint as the core team are just getting futures and async/await sorted out properly now, still I want it. Normal multi-threading however is totally awesome, largely as a result of the stuff in this article.

So I was thinking to myself what can I do to help with the above problems. I mean I wanna use this thing, it's great. For any low level systems programming stuff I'd use it in a heartbeat but I wanna use it day to day in building microservices in the cloud. I decided maybe I can help with by writing a series of articles as tutorials on the challenging parts of learning Rust to give people a starting point. Now there are loads of articles about this kind of thing and I'll reference this truly excellent series here but this references Haskell similarities too much for my liking. I don't promise mine is really any better but I'm trying to aim it at a slightly lower level, ideally I'd like to aim this at those without programming experience but reasonably it's unlikely without a bit of experience, however I'd like to think those with some basic programming knowledge can gain something from these articles. Knowing roughly what a function, module, string, integer and memory is in some languages (not necessarily Rust) will be assumed, but just a basic grasp should suffice for this article to make sense.

I feel that the main initial stumbling block with Rust is the borrow checker and memory management so that will be the subject of this first article. This is super unfortunate in a lot of ways as the borrow checker is the best thing about Rust. My advice to those new to Rust is don't be afraid of it: it loves you, it wants you to write great bug free code, it wants to help you. Whilst Rust has a difficult and steep learning curve it gives super helpful compile time errors (as I'll try and persuade in this article). I've spoken to people who've given up on Rust because of the borrow checker, if that's true then this article is for you. Please give it another go after reading this and if you still don't understand something please ask me afterwards.

I have also noticed that reading up about Rust it's way better than it used to be. It does have this reputation of being super hard to use still but the Rust community is great, they listen to problems and they try and make it better and they have (see this article on non-lexical lifetimes for example). Whilst green threading isn't quite as good as I'd like yet, I know they're working super hard on it to improve things and before long I'm pretty sure it will be.

This will be a series of articles about the difficult parts of Rust but it is not meant to be exhaustive, I'm not trying to replace the Rust book and I never will, the Rust book is great but it's a better reference than it is a tutorial. I'd strongly recommend you to try and get to grips with the Rust book too, you'll need it. If this article is confusing you could read the first 3 chapters of the Rust book (the easy chapters :) ).

Whilst Rust is hard, it is workable I promise and I will try my best to hold your hand (metaphorically of course) and guide you through the initial stages. There is nothing in this article that is rocket science or even that difficult, but there is a lot to learn in Rust and even for experienced programmers you may need to learn stuff from the ground up. These articles should prepare you well here and should have everything you need (once they're all written anyway, but even just this one will be enough for experienced programmers).

At the end of the day though, if you're after a nice easy programming language, Rust just isn't it. Rust I believe is a strong competitor for the lower level C/C++'s of the world and for complex programs in higher level languages like Python or Java. If you want a quick simple program and/or rapid prototyping Rust isn't what you're after.

It's recommended to try and run the programs below and experiment with them yourself. If you don't want to install Rust try the Rust playground. There will also be compiler warnings like warning: unused variable: i, this I would not allow in production stuff I'm writing, in tutorials I don't want to get bogged down with this but basically if I used it this would go away. It's telling me if I take away this variable the program is unchanged, it's trying to help me again. You should always fix compiler warnings, always, always, always.

Finally, I make no pretence of being a Rust expert but I've probably solved enough basics problems in it to be a bit above beginner now. I'd love to get some feedback from any Rustaceans out there as to what they think, please ping me.

The Borrow Checker

The main content of this article is all about the borrow checker. We'll discuss further things in future articles but for now we need to get a handle on this fully first. Once you understand the borrow checker you fairly well understand Rust and many other things will fall into place after this that maybe didn't before. We'll introduce some bits and pieces of syntax too but mostly this is a theoretical article about the hardest obstacle to learning Rust (in my view).

Ownership

OK, let's get started. First topic is ownership and always will be. It's important, learn and digest how this works. Once you do you're well on your way to doing cool stuff with Rust, I promise.

Memory management is the theme here basically. If you've programmed in C or C++ before you likely know about the pains memory management can cause. If you've programmed in higher level languages you may know less as it deals with it for you, but it does so with the expense of a garbage collector.

You may never have even cared about the garbage collector, it will be slowing things down for you and it only solves the freeing memory problem, Rust does more but I'll get to that later. But really garbage collectors are great, I much prefer that to C/C++ where memory management just gets in the way of what you're trying to do. Rust offers something in between essentially and in a lot of ways becomes superior as a result.

If you've not programmed in C/C++ before, well if you want to reserve memory in a dynamic way (now I need a 10 element list but soon I'll need 12, that kind of thing) you need to reserve memory and free it when done. For example, to reserve memory for an integer on the heap (the dynamic memory store basically, you don't really need to worry about this for now) I need to do tell C++ to create it as follows (don't worry about C++ syntax, it's not needed for this article):

int* i = new int;
*i = 1;
Enter fullscreen mode Exit fullscreen mode

and then when I'm done I need to do:

delete i;
Enter fullscreen mode Exit fullscreen mode

If I forgot the latter or do it twice then badness ensues, particularly in the latter case. It's as simple as that, the standards say if I free twice basically anything can happen. I tried freeing twice on Linux and nothing really happened, on a Mac I got the following (your results may vary):

➜  ./a.out
a.out(67865,0x1047a05c0) malloc: *** error for object 0x7fa4e8c02ac0: pointer being freed was not allocated
a.out(67865,0x1047a05c0) malloc: *** set a breakpoint in malloc_error_break to debug
[1]    67865 abort      ./a.out
Enter fullscreen mode Exit fullscreen mode

In C++ you need to think about the different types of memory here (stack and heap to those familiar), in Rust less so. The stack and heap still exist but honestly to begin with you don't need to worry so much. If you get deeper into Rust maybe but I've honestly not had to care yet. Nor is there GC (garbage collection) but it's as safe as when there is GC, more safe. I can't get the above runtime error from Rust, I've tried (even in unsafe Rust, which we're not touching here, I couldn't get it. Stay away from unsafe Rust to begin with, there be dragons). I get compiler errors but no runtime errors and of course compiler errors are better because I get them sooner and I know I must have broken something.

The reason has to do with ownership, in Rust everything has to be "owned" by something. Once it's no longer owned by something the memory will be cleared for us at that point. It's as if the compiler went and added the delete statement in C++ for me, (how cool is that, I don't need to remember it). But what does it mean to be "owned" by something.

Let take a very simple example here:

fn main() {
    let i: u32 = 32;
}
Enter fullscreen mode Exit fullscreen mode

That's an ultimately pointless Rust program. This declares a variable i to be of type u32 (unsigned 32 bit integer, Rust's type system is way more sane than C/C++) and sets it to have value 32. Rust's compiler notices that when that function returns, i is no longer needed so it does any cleanup necessary (technically this would work in C/C++ too because it's on the stack, but this would also work if it were on the heap in Rust). Essentially you don't need to worry about memory management, Rust has taken care of it for you.

Similarly, we can consider the following program

fn main() {                        // Scope of `main` starts here
    let i: u32 = 32;
    {                              // A new inner scope starts here
        let j: u32 = i + 5;
        println!("j is: {}", j);
    }                              // The inner scope ends here and `j` is dropped
}                                  // The `main` scope ends here and `i` is dropped
Enter fullscreen mode Exit fullscreen mode

Running this program prints the line j is: 37. We won't explain the println statement thoroughly but essentially it prints the string given and every time we have a {} inside that string we can substitute a variable value. Here the {} is replaced with the value of j.

Here we have created another scope in the middle with the braces. If you're not familiar with a scope like this from other C-family languages, basically a scope is created by these curly braces, anything between a left curly brace { and a right curly brace } is a scope. In this case I have a scope for the main function created between lines 1 and 7 an inner scope created after defining i in line 3 that ends on line 6. Everything in this inner scope has access to the variables in the scope above it but the main scope itself doesn't have access to stuff in the middle. So for example the inner scope has i and j but the main scope only has i. Having inner scopes like this is not uncommon in Rust (a bit like JavaScript in this sense). We can limit the time we need variables for with scopes like this. You can also shadow variables by calling them the same name in inner scopes, see here for example.

Now returning to the code example, we have i as before that goes out of scope once main returns. However, now we have another scope by the inner pair of braces. In this scope we create a new u32 variable j, we also have access to i so we can set j equal to i + 5, or indeed 37. Now once we get to the end of the inner scope, i.e. once the program goes past the right curly brace on line 6, then we can no longer access j. For example consider the following program:

fn main() {
    let i: u32 = 32;
    {
        let j: u32 = i + 5;
    }
    println!("j is: {}", j);
}
Enter fullscreen mode Exit fullscreen mode

When we compile this we get:

➜ rustc simple_rust_program.rs
error[E0425]: cannot find value `j` in this scope
 --> simple_rust_program.rs:6:26
  |
6 |     println!("j is: {}", j);
  |                          ^ help: a local variable with a similar name exists: `i`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0425`.
Enter fullscreen mode Exit fullscreen mode

Let's take a moment now and look at how brilliant this error output is here. This tells me exactly what I need to know, j doesn't exist at line 6. It even tells me a command to explain the error more fully and it thinks maybe I meant i. I know exactly what it's complaining about and it really tries to help me fix it. I really love the borrow checker for this, it's really trying to help (I recommend trying to remember this).

So we've covered what ownership means, it's pretty straight forward right, what's the big deal? Well let's move onto the next topic where we pass stuff around.

Transferring ownership

Sometimes of course we want to pass variables around, in functions calls for example we may want to pass in a variable as a parameter. Now we have 3 ways (these 3 ways come up a lot in Rust) to do this, transfer ownership, pass by reference or pass by mutable reference. We cover transferring ownership now (The other two will be covered under the Borrowing section below).

Transferring ownership is really easy syntactically and just what you'd expect. The following program shows something being passed by ownership:

fn take_ownership(a: String) {
    println!("I have a: {}", a);                 // and a is now owned by take_ownership
}

fn main() {
    let a: String = "Test String".to_string();   // a is owned by main
    take_ownership(a);                           // Now we pass a to take_ownership
    //println!("I try to print a: {}", a);
    // If I uncomment this line above then I get an error because this scope no longer owns a
}
Enter fullscreen mode Exit fullscreen mode

To explain briefly, the .to_string() creates a String that we can alter out of a String literal. See Section 8.2 of the book for more details.

So if you pass ownership like this, it's gone from the original function, only one thing can own a variable at any one time. Unless you pass it back of course. I can alter the above as follows:

fn take_ownership(a: String) -> String {
    println!("I have a: {}", a);
    return a;
}

fn main() {
    let a: String = "Test String".to_string();
    let a = take_ownership(a);
    println!("a: {}", a);            // We've passed a back so we're OK now
}
Enter fullscreen mode Exit fullscreen mode

As an aside, the above program doesn't return in a very Rust like way. Equivalently this function can be written:

fn take_ownership(a: String) -> String {
    println!("I have a: {}", a);
    a
}
Enter fullscreen mode Exit fullscreen mode

This is exactly the same thing. Because there's no semi-colon on the last line Rust knows this is a return. There's an explanation involving statements and expressions but I don't want to go into this here, just be aware if you see this, this is just a return.

So simple enough right. Well no, not quite. There's a complication that we'll talk about shortly but first we cover mutability now.

Mutability

Next on the agenda is to cover mutability, or you may prefer to think of non-constant variables or variables you can alter (or just variables maybe, surely I can alter a variable or it doesn't vary, just go with it). Essentially, by default, everything in Rust is immutable, that is it's constant and can't have its value altered. If I have the following program:

fn main() {
    let i: u32 = 1;
    i = 2;
    println!("i is: {}", i);
}
Enter fullscreen mode Exit fullscreen mode

If I try I get something like the following:

error[E0384]: cannot assign twice to immutable variable `i`
 --> src/main.rs:3:5
  |
2 |     let i: u32 = 1;
  |         -
  |         |
  |         first assignment to `i`
  |         help: make this binding mutable: `mut i`
3 |     i = 2;
  |     ^^^^^ cannot assign twice to immutable variable

error: aborting due to previous error

For more information about this error, try `rustc --explain E0384`.
Enter fullscreen mode Exit fullscreen mode

Pretty clear where the problem is, right? But how do I do that? Quite easy actually, we can create a mutable variable (one that can be changed) by simply adding the keyword mut as per the code below. In fact it tells me in the error exactly how to do this.

fn main() {
    let mut i: u32 = 1;        // Added mut, now I can change it
    i = 2;
    println!("i is: {}", i);
}
Enter fullscreen mode Exit fullscreen mode

What's interesting about this is that I can pass ownership of immutable variable around to be a mutable variable and that's absolutely fine. For example:

fn take_ownership_and_pass_back(mut a: String) -> String {
    a = a + "2";
    a
}

fn main() {
    let a: String = "TEST".to_string();
    let a = take_ownership_and_pass_back(a);
    println!("a: {}", a);
}
Enter fullscreen mode Exit fullscreen mode

So I change a from immutable to mutable, and that's fine. If I run this I get a line a: TEST2. But a was immutable? Well Rust makes no guarantees you won't purposefully change it to mutable but you've got to try to change it. It's not trying to tell you what to do it's trying to get you to stop changing things you never meant to. Generally, immutable variables are a great thing for this reason and that's why they're the default. The point really is that I can limit the amount of time something needs to be mutable for and thus limit the places where it's possible to alter values (and create bugs according to functional programming, we wouldn't want that).

Another interesting code snippet is the following:

fn main() {
    let mut s = "TEST1".to_string();
    let t = s;
    // Can't do the following as s is now moved
    //println!("s={}", s);
    s = "TEST2".to_string();
    println!("s={}, t={}", s, t);
}
Enter fullscreen mode Exit fullscreen mode

Here we create a mutable string s with value "TEST1". Next we let t take ownership of this string, at this point I can no longer read from s as it's moved its value elsewhere and is empty. However I can reassign to s as it's mutable and then read from it as it's now initialised, that's absolutely fine as shown by running the above. I get the output s=TEST2, t=TEST1.

There's another common gotcha here that we're not allowed to do, let's take a look at the following.

fn main() {
    let x = "TEST".to_string();
    for i in 0..10 {
        let y = x + &i.to_string();
        println!("{}", y);
    }
}
Enter fullscreen mode Exit fullscreen mode

Here we try to loop 10 times and try to build a string from concatenating "TEST" and i the integer. Unfortunately we have a problem, can you see why without compiling?

If we try to compile this we get the following error:

error[E0382]: use of moved value: `x`
 --> src/main.rs:4:17
  |
2 |     let x = "TEST".to_string();
  |         - move occurs because `x` has type `std::string::String`, which does not implement the `Copy` trait
3 |     for i in 0..10 {
4 |         let y = x + &i.to_string();
  |                 ^ value moved here, in previous iteration of loop

error: aborting due to previous error

For more information about this error, try `rustc --explain E0382`.
Enter fullscreen mode Exit fullscreen mode

So because x was moved on the first loop and then goes out of scope at the end of this, x is dropped after one loop and thus we have no access to x on the second loop. The compiler recognises this and throws the above error. There are a variety of ways around this, including the following initially surprising (though after thought reasonable) method:

fn main() {
    let mut x = "TEST".to_string();
    for i in 0..10 {
        let y = x + &i.to_string();
        println!("{}", y);
        x = "TEST".to_string();
    }
}
Enter fullscreen mode Exit fullscreen mode

We now try and look at copying and cloning as a way around this kind of thing. Using references from later will also work here.

Copying and Cloning

So I said earlier that you can transfer ownership of a variable and I showed you how to do it. I also told you there's a minor complication, well here it is. Let's have a look at the following example:

fn main() {
    let i: u32 = 123;

    {
        let mut j = i;     // <-- If i didn't satisfy copy this would be a move

        j = 124;

        println!("Not finished and j is: {}", j);
    }

    println!("Finished and i is: {}", i);
    // ^^ And if i didn't satisfy copy and hence moved above I wouldn't be able to read i here
}
Enter fullscreen mode Exit fullscreen mode

By the logic I mentioned previously, here i should be moved into j in the inner block when j is created, but then I can no longer access i in the final println as it's gone. Yet I run it, it compiles successfully and I get:

Not finished and j is: 124
Finished and i is: 123
Enter fullscreen mode Exit fullscreen mode

So I can see I've copied the value of i into j, not moved anything as I thought, and I can happily carry on using an unchanged i. Rust has helped me because it knows u32's satisfy the Copy trait. See here for more details about the Copy trait but I'll explain what this means briefly. If you've come across interfaces from other languages, traits are kinda like interfaces, read "interface" for now instead of trait and it's close enough. Essentially a trait is something that can be satisfied and then things can act accordingly based on this. In this case anything that satisfies the Copy trait, the compiler knows its value gets copied rather than moved.

On the other hand if I change i to be a String s as in the following code:

fn main() {
    let s: String = "123".to_string();

    {
        let mut t = s;

        t = "124".to_string();

        println!("Not finished and t is: {}", t);
    }

    println!("Finished and s is: {}", s);
}
Enter fullscreen mode Exit fullscreen mode

Then I get an error as follows:

error[E0382]: borrow of moved value: `s`
  --> src/main.rs:12:39
   |
2  |     let s: String = "123".to_string();
   |         - move occurs because `s` has type `std::string::String`, which does not implement the `Copy` trait
...
5  |         let mut t = s;
   |                     - value moved here
...
12 |     println!("Finished and s is: {}", s);
   |                                       ^ value borrowed here after move

error: aborting due to previous error

For more information about this error, try `rustc --explain E0382`.
Enter fullscreen mode Exit fullscreen mode

Again, look at the amazing output (gcc or clang eat your heart out). This tells me exactly what I need to know, s was created here on line 2, moved here on line 5, but now I've tried to use it on line 12 and I'm not allowed.

So the error is great but what do I do here now? Well there's a few things you can do. If you're happy with the copy you can simply add a .clone() to the line changing the value for t, as in:

        t = "124".to_string().clone();
Enter fullscreen mode Exit fullscreen mode

Now Rust copies it because I've told it to. Whatever's being cloned needs to satisfy the Clone trait, most things tend to but not always. Cloning means the whole memory of the value of that variable is copied into memory elsewhere and if something is likely to be particularly big, you may not want it to satisfy this trait so you can't accidentally do this.

But basically what this really means is that whenever we pass a u32 like this it copies the data rather than moving it. So by copying it I get a whole new variable and it's totally unrelated to the original one. Rust is not alone here, I've seen a few languages that do this, Python for example. It's usually small fixed size things this is true of by default (booleans and numbers mostly). If you ever find something like this worked when it looked like it shouldn't then probably just Rust copied it and is trying to help you. Of course if this happens and you change the second variable the first is unchanged, one to be careful of.

So now I've covered most of what you need to know to get going with ownership. But we won't get very far unless it's a pretty simple program (and I told you not to bother using Rust in that case). Sometimes we just want to borrow something which we'll take a look at now.

Borrowing

Now we know how to move and copy stuff, but what if I don't want to move or copy it? I want access to it I guess to see what's in there but it's not mine, or even I want to change some stuff but it's not mine. Well you've a few choices as follows:

  • If I want to only be able to read from it I can create a "reference" to it, I can have as many of these as I like (as long as no "mutable references" from the next exist).
  • If I want to be able to update it too I can create a "mutable reference" to it, but I can only have one of these, period. If I have a "mutable reference" the compiler will stop me from creating any other "references" mutable or not ("references" are immutable by default, as indeed everything is in Rust).
  • I can create "smart pointers" to it. I'll discuss this in a future article, this can be a bit of a last ditch effort but sometimes is needed.

So what are references and mutable references? Well instead of passing ownership I pass a reference (kinda like a pointer in C if you're familiar with this). A reference holds a memory address essentially, that points to where the original is. When you take a reference a of a variable b you leave the ownership of a alone but you say to b "you can point over here and take a look at a, or even you can point at a and alter it if it's a mutable reference I've taken, but a still belongs to me". We'll look at an example now.

For the code example above I really wanted to be able to change it in another scope without taking ownership, maybe in another function or something. Well I could do this and this works:

fn main() {
    let mut s: String = "123".to_string();

    {
        let t = &mut s;

        *t = "124".to_string();

        println!("Not finished and t is: {}", t);
    }

    println!("Finished and s is: {}", s);
}
Enter fullscreen mode Exit fullscreen mode

If I run this I get what I want:

Not finished and t is: 124
Finished and s is: 124
Enter fullscreen mode Exit fullscreen mode

So what have I done here? First, I've had to change the original string to be mutable because I'm changing it, I didn't need to before because I never wanted to change the original. I also created a mutable reference t by doing let t = &mut s;. This & is the syntax for me taking a reference and mut implies I want a mutable reference, something I can change. If I'd just written let t = &s; then t would just be a reference and I could only read from it. When I want to perform a read or write (if it's mutable) to the original I need to dereference it and I do so by writing *t, the * here means the value this address is pointing to. If I had written t = "124".to_string() as you might think I'd get the following error:

 --> src/main.rs:7:13
  |
7 |         t = "124".to_string();
  |             ^^^^^^^^^^^^^^^^^ expected mutable reference, found struct `std::string::String`
  |
  = note: expected type `&mut std::string::String`
             found type `std::string::String`
help: consider dereferencing here to assign to the mutable borrowed piece of memory
  |
7 |         *t = "124".to_string();
  |         ^^

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
Enter fullscreen mode Exit fullscreen mode

I'll stop gushing about Rust's error reporting after this one but look, told me exactly how to fix it.

So we understand now from above about dereferencing and anyone whose looked at Rust before is probably thinking "I never do that in most cases". Quite right, I lied to you a moment ago, I'm sorry. Well the truth is, thankfully, Rust is smart. In non-ambiguous cases the Rust compiler knows places where I clearly meant to be dereferencing this and adds the dereference operator automatically. So I put the * to dereference the t in the line *t = "124".to_string(); because, as we see above, Rust doesn't know I don't want to assign directly to t. But in the next line where I print the variable I can see that I don't need to dereference, there's no harm in doing so, but if I don't the compiler knows "well he obviously meant to". In fact most Rust devs from what I can tell wouldn't bother putting the * on this line either.

Now I said earlier I can only take one mutable reference and if I have one I can't take any other references. Let's try and create another reference after creating the immutable reference and see what happens. Well we'd get an error similar to the following:

error[E0502]: cannot borrow `s` as immutable because it is also borrowed as mutable
 --> src/main.rs:6:17
  |
5 |         let t = &mut s;
  |                 ------ mutable borrow occurs here
6 |         let u = &s;
  |                 ^^ immutable borrow occurs here
7 |
8 |         *t = "124".to_string();
  |         -- mutable borrow later used here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0502`.
Enter fullscreen mode Exit fullscreen mode

This is because as I hinted earlier, if I try and create any other reference when a mutable reference exists then Rust will block me. The reasons are to do with enforcing safe multi-threading programming, race conditions are no fun and they are largely eliminited by imposing these rules. So if one thread had a mutable reference that could be altering stuff away, I might get data race conditions if I try and read it in another thread. So Rust absolutely guarantees safety here, it makes sure you can't mess stuff up in multithreading environments at the cost of having to understand this kind of article. Once you've worked out the stuff in this article the rest of the theory is a lot easier.

Because of the rules around references I've just explained, limiting the scope by creating an inner scope is a common thing to see in Rust code. Once the scope changes things get cleared up and it's a great way out of some problems you'll encounter. It's a common pattern to create a really small scope for changing stuff.

Similarly if I try and create a mutable reference after a reference exists (I have to use it though otherwise Rust is smart enough to know there's no problem):

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  --> src/main.rs:6:17
   |
5  |         let u = &s;
   |                 -- immutable borrow occurs here
6  |         let t = &mut s;
   |                 ^^^^^^ mutable borrow occurs here
...
11 |         println!("Not finished and u is: {}", u);
   |                                               - immutable borrow later used here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0502`.
Enter fullscreen mode Exit fullscreen mode

I often wondered in my early dealings with Rust, how is this stuff safe if I have a reference to a mutable variable and the mutable variable starts getting changed. I should have just tested this back then but instead I just got confused. What I mean is, consider the following program:

fn main() {
    let mut x = "123".to_string();
    let y = &x;
    x += "456";
    println!("{}", y);
}
Enter fullscreen mode Exit fullscreen mode

I can't take a mutable reference to x after the reference because that's dangerous. But if x can still change the value then how is this any different. Well this fails compilation too because Rust blocks even the owner making changes while these references exist. I'm not sure this is very well documented, I certainly wasn't aware of this for a long time but this is how it ensures safe multi-threading. If I compile the above program I get:

error[E0502]: cannot borrow `x` as mutable because it is also borrowed as immutable
 --> src/main.rs:4:5
  |
3 |     let y = &x;
  |             -- immutable borrow occurs here
4 |     x += "456";
  |     ^^^^^^^^^^ mutable borrow occurs here
5 |     println!("{}", y);
  |                    - immutable borrow later used here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0502`.
Enter fullscreen mode Exit fullscreen mode

Similarly the following program fails:

fn main() {
    let mut x = "123".to_string();
    let y = &mut x;
    x += "456";
    println!("{}", y);
}
Enter fullscreen mode Exit fullscreen mode

Once I've created the mutable reference I can only use that to change things while it exists, even the owner can't change it any more. Compiler error this time:

error[E0499]: cannot borrow `x` as mutable more than once at a time
 --> src/main.rs:4:5
  |
3 |     let y = &mut x;
  |             ------ first mutable borrow occurs here
4 |     x += "456";
  |     ^ second mutable borrow occurs here
5 |     println!("{}", y);
  |                    - first borrow later used here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0499`.
Enter fullscreen mode Exit fullscreen mode

So in a nutshell Rust will ensure safety as we have:

  • Rust will make sure that if more than one thing can read a variable then nothing can change that variable at all.
  • Rust will make sure that if something is able to change a variable (mutable variable or mutable reference) then only that one thing is able to read and write from that variable.
  • The counterexample to either of the above is if Rust has been smart enough to know it's safe anyway which it occasionally is. For example when I tried to take a reference and a mutable reference in the same scope earlier it wasn't a problem until I used both of them.
  • These checks are all enforced at the compiler level, nothing in this article is done at runtime so you know sooner if you've done something potentially dangerous.

That just about covers the basics of the borrow checker. Easy right? Well there's more, of course, and I'll cover that off in future entries in this series. Hopefully you can now tackle up to Chapter 10 of the book will make sense now and you can hopefully read all up until Chapter 10 without finding anything too challenging. In the next few articles in this series I'll try to cover structs, options, threads, smart pointers, lifetimes and closures and how to work with the borrow checker with these things. Stay tuned.

Top comments (11)

Collapse
 
oh_god_it_works profile image
mawOfMalfunctions

The one thing I never seem to get into my head are lifetime parameters. The syntax is just so strange and it seems hard to find examples for specific cases anywhere. It feels like fighting the borrow checker but without even knowing what it wants.

Collapse
 
strottos profile image
Steven Trotter

Agreed, lifetimes are more difficult and I figured perhaps warrants an article in itself. I certainly haven't tried to cover them here, would have been too much. In principal they're not so bad but in practice can be a different experience, I still struggle at times and have been known to use other techniques in order to avoid having to (which is clearly not the right way of doing things).

I'll try and put together a lifetimes article at some point as well. I'd be curious to have an example of the kind of thing you do struggle with if you'd be so kind/have one to hand.

Collapse
 
krusenas profile image
Karolis

I would identify myself as mainly a Go developer but in some cases I wish I wrote few services in Rust :) Especially where something is really simple and just need to take care of used memory.
The approach that I took in learning Rust was first reading a book, then do a small service, then more github.com/rust-lang/rustlings to better understand various types and their use cases.

All in all, Rust is not an easy language and I am still far from being comfortable when writing code in it. I hope it will change. Agree regarding unit tests, compared to Go's unit testing they are not straightforward, well... nothing is in this language :D But it's fun!

Collapse
 
strottos profile image
Steven Trotter

Honestly, I'd say if it's simple there's not much point in learning Rust for this purpose unless performance is really critical. I'd stick to Go in this case. If you feel like unhappy with Rust then maintainability alone should be a good enough reason.

That said though I really think Rust is workable and not as hard as people think. I've come across articles before complaining that "Rust can't do this thing" and I've tried it and it works fine so I think it's come and is still coming a long way. Really my aim here is hopefully to make people realise some of the "hard" things aren't as bad as they seem, and maybe to get a few people to give it another go as I'm pretty sure it's better than it used to be a few years back.

But yeah, it's not easy, but I think it's worth it and I think it'll be a super valuable skill in time. I think it's time is yet to come I guess.

Collapse
 
strottos profile image
Steven Trotter

Thanks for reading anyway. Hope you enjoyed it or gained something from it. There is more to come hopefully when I get time :)

Collapse
 
strottos profile image
Steven Trotter

For anyone that is still confused by any of the above it might be worth a read of this article too, this does a great job of explaining: medium.com/@thomascountz/ownership...

Collapse
 
batorshikh profile image
Bat-Orshikh

Thanks for the great article.
Recent few days, I really like to learn rust programming language cuz it's little bit similar to c, c++.

Collapse
 
lepidora profile image
Mia

Thanks for the article! I've been meaning to learn Rust for a while and this is a nice and concise introduction to it.

Collapse
 
strottos profile image
Steven Trotter

Really glad you enjoyed it, thanks.

Collapse
 
deepu105 profile image
Deepu K Sasidharan • Edited

Great article. The first line of the article says The Borrow checker but the following content doesn't fit, is it a typo?

Collapse
 
strottos profile image
Steven Trotter

Yeah that wasn't clear, have edited it so that hopefully it makes more sense now. Basically there was one section called the borrow checker which is the whole article, but yeah, that's just dumb and unclear.

Glad you enjoyed the article and thanks very much for the feedback.