Image

Rust Ownership

Rust ownership is unique feature in Rust to manage memory without automatic garbage collection (slows the program down at runtime) or explicit, manual memory allocation (intense workflow for programmers).

Ownership Rules:

  • Each value in Rust has an owner.
  • There can only be one owner at a time.
  • When the owner goes out of scope, the value will be dropped.

Example with String type

String literals are immutable, but not every string can be known when we write our code (e.g. user input). Hence, the String type is made to manage data allocated on the heap and deal with unknown data. To create a String from a string literal, use the from function, as below:

let s = String::from("hello");

The double colon :: enables us to namespace this particular from function under the String type rather than using some sort of name like string_from.

let mut s = String::from("hello");
s.push_str(", world!"); // push_str() is built-in function
println!("{}", s);

Rust’s particular path is that memory is automatically returned once the variable that owns it goes out of scope. For example:

{
	let s = String::from("hello"); // s is valid from this point forward

	// do stuff with s
}                                  // this scope is now over, and s is no
								   // longer valid

Rust automatically calls drop function for any variable that goes out of scope.

Functions and Ownership

When inserting a variable into a function, the variable’s value moves into the function and is no longer valid outside that function in the present code. However, if using variables with Copy traits, the variable is still valid afterwards.

let s = String::from("hello"); // s comes into scope 
takes_ownership(s); // s's value moves into the function... 
					// ... and so is no longer valid here 
let x = 5; // x comes into scope 
makes_copy(x); // x would move into the function, 
				// but i32 is Copy, so it's okay to still 
				// use x afterward

The ownership of a variable follows the same pattern every time: assigning a value to another variable moves it.

But what if you don’t want to transfer ownership back and forth every time you want a function to use a certain variable? Rust has a feature for using a value without transferring ownership called references.

References and Borrowing

Instead of passing in the variable to a function, causing it to take ownership of it, one can instead insert a reference to the variable using & - e.g. function(&variable).

A reference is like a pointer in that it’s an address we can follow to access the data stored at that address; that data is owned by some other variable. Unlike a pointer, a reference is guaranteed to point to a valid value of particular type for the life of that reference.

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);
    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

The difference with references is that because ownership is not transferred, the value that the reference points to will not be dropped when the reference stops being used. So a function can use a variable by reference and when that function is done with the reference, the value of that variable will still be there. This act of creating references is called borrowing.

Borrowing means also that you cannot modify the variable. References, like variables by default, are immutable (one could say read-only).

Yet, you can make borrowing mutable by adding a mut operator after the & to create a mutable reference. However, there is one major restriction to this: you can only make a single mutable reference within the same scope, not multiple. No other reference is allowed to make changes. Two or more mutable references of the same variable will fail. If you make multiple references that are each in different scopes, then that’s fine.

Another matter to consider is that you cannot have both immutable and mutable reference in the same scope.

Users of an immutable reference don’t expect the value to suddenly change out from under them! However, multiple immutable references are allowed because no one who is just reading the data has the ability to affect anyone else’s reading of the data

Note that the scope of a reference begins from the moment it’s created to the last time it’s used in the program. If after the last usage, a mutable reference is created, that’s fine, as long as the immutable reference isn’t used again after the mutable one.

let mut s = String::from("hello");

let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{} and {}", r1, r2);
// variables r1 and r2 will not be used after this point

let r3 = &mut s; // no problem
println!("{}", r3);

Mutability is therefore highly controlled in Rust. The benefit of this is that Rust prevents data race (similar to race conditions), which happens when these three behaviours occur:

  • Two or more pointers access the same data at the same time.
  • At least one of the pointers is being used to write to the data.
  • There’s no mechanism being used to synchronize access to the data.

Slice Type

A slice is a reference to part of a variable. In the case of a String, it would be a portion of the string. Slices are created using a range within brackets specifying [starting_index..ending_index], where starting_index is the first position in the slice and ending_index is one more than the last position in the slice.

let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];

// in the case of `let world = &s[6..11];`, `world` would be a slice that contains a pointer to the byte at index 6 of `s` with a length value of `5`.

If you want to start at index zero, you can just use &s[..2]. Likewise, if the slice includes the last index, you can just employ &s[3..].

Example using the flexibility of &str instead of &String in the function signature.

fn main() {
    let my_string = String::from("hello world");

    // `first_word` works on slices of `String`s, whether partial or whole
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);

    // `first_word` also works on references to `String`s, which are equivalent
    // to whole slices of `String`s
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word` works on slices of string literals, whether partial or whole
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}

© Filip Niklas 2024. All poetry rights reserved. Permission is hereby granted to freely copy and use notes about programming and any code.