30 Days of Rust - Day 16

johnnylarner

johnnylarner

Posted on May 10, 2023

30 Days of Rust - Day 16

Evening all,
Today was a hectic one. I'm jetting off for my first summer(ish) vacation next week so I have to clean up a few dangling ends at work. Today's post will focus on Rust's ownership concept and its rules. We'll also look at String again and tie that in with ownership.

Yesterday's questions answered

  • By assigning something a 'static lifetime value, you tell rust that the value should live for the entirety of program's duration.

Today's open questions

  • Do Rust's reference rules apply to slices?

Primitive types as binary code

The Rust book introduces its topic of ownership through the lens of a computer's available memory, which is split between a compile-time stack and a runtime heap. Non-primitive data types stored on the heap get allocated at runtime, but the pointers to the heap are added to the stack at compile-time.

What I find conceptually interesting, however, is how primitive types (e.g. string or boolean literals, i32 or f64) as well as other things (functions, structs) on the stack are baked into the binary produced by the Rust compiler. In this sense they are just slices in your binary!

Move variable, get out the way

So managing the stack and heap is not something novel to Rust. However, it's approach to managing data on the heap is novel and is what I've been referring to as ownership. In an earlier blog post I summarised the 3 holy rules of the ownership.

Rule number two states that a value on the heap can only have one owner at a time. When we bind an existing value to a new variable, we actually moving it into a new owner. In Python, this is somewhat related to the concept of mutability and the differentiation between pass-by-value and pass-by-reference. The key difference is that Rust will drop the old variable whenever you move it into a new reference. To copy value you'd have to call clone on it before hand.

Remember this behaviour only concerns values on the heap. Stuff on the stack can be copied easily, no questions asked.

Gimme a reference

Another way to move a value is to pass it into another scope (typically using a function). This may seem reasonable at first glance, but you quickly realise that as soon as you want to return something merely derived from a function's arguments, you end up with very messy code. Compare these two functions:

fn calculate_length_owned(s: String) -> (String, usize) {
    let length = s.len();
    (s, length)
}

fn calculate_length_borrowed(s: &String) -> usize {
    s.len()
}
Enter fullscreen mode Exit fullscreen mode

The Rust solution here is to create a reference to the value. This means that the value can be borrowed via its reference. If the value and its reference are declared mutable, changes to borrowed values can be made too.

References carry their own set of rules. These can be summarised as follows:

  • A value can have any number of immutable references
  • But a value may only have one active mutable reference at any given point
  • And whenever there is an active mutable reference, there cannot be any immutable references
  • Attempts to leave dangling references will leave you at odds with the compiler!

You can take a moment to think about the kind of side-effects (think, also, race conditions) which effectively become impossible with these rules enforced by the compiler.

Slices as view

Both strings and arrays can be sliced in Rust. The syntax is very similar to Python, though the colon is replaced by two dots: &my_string[0..10]. The view a slice gives on either a String or string literal is bound to the value. Thus, mutating the value will affect the slice.

💖 💪 🙅 🚩
johnnylarner
johnnylarner

Posted on May 10, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related