DEV Community

Cover image for Learning Rust πŸ¦€: 08 - Ownership: Functions and References
Fady GA 😎
Fady GA 😎

Posted on

Learning Rust πŸ¦€: 08 - Ownership: Functions and References

Now we will see how Rust's Ownership affects Functions behavior. Let's dig in!

Please check the previous 2 articles in this series to recap what we have discussed about Ownership.

⚠️ Remember!

You can find all the code snippets for this series in its accompanying repo

If you don't want to install Rust locally, you can play with all the code of this series in the official Rust Playground that can be found on its official page.

⚠️⚠️ The articles in this series are loosely following the contents of "The Rust Programming Language, 2nd Edition" by Steve Klabnik and Carol Nichols in a way that reflects my understanding from a Python developer's perspective.

⭐ I try to publish a new article every week (maybe more if the Rust gods πŸ™Œ are generous 😁) so stay tuned πŸ˜‰. I'll be posting "new articles updates" on my LinkedIn and Twitter.

Table of Contents:

Ownership and functions:

Passing variables to functions behaves similarly to assigning values to variables. It either copies or moves the variable into the function's scope.

fn main() {
    let s1 = String::from("string");
    take_scope(s1); // s1 is moved into 'take_scope'

    // Error: borrow of moved value: `s1`
    //println!("s1: {s1}")

    let n1 = 5;
    take_num(n1); // n1 is COPIED into 'take_num'
    println!("n1 from main: {n1}"); // this is fine as n1 is copied not moved.
}

fn take_scope(s: String) -> () {
    println!("s1 from take_scope {s}")
}

fn take_num(n: i32) -> () {
    println!("n: {n}")
}
Enter fullscreen mode Exit fullscreen mode

Because s1 is a String type, it will be moved to the 'take_scope' function. So, calling it again from the 'main' function will cause a compilation error. But for n1 as it is of i32 type (simple type), it will be copied not moved into the 'take_num' function so it's OK to call it again from the 'main' function.

Functions can return move back the variables they receive if they are set as the return value.

fn main() {
    let s1 = String::from("String 2"); 
    let s2 = take_and_return_scope(s1); // s1 is moved to 'take_and_return_scope' then moved back into s2
    println!("S2: {s2}");
}

fn take_and_return_scope(s: String) -> String {
    s
}
Enter fullscreen mode Exit fullscreen mode

The 'take_and_return_scope' function takes only one variable s and returns it. It is said that it's moved back into s2. But generally, functions also return computed values that are needed elsewhere in your app. So, in order to do that functions can return a tuple containing the original variable to be moved back into its original scope in addition to the calculated values.

fn main() {
    let s3 = String::from("String 3");
    let (s4, len) = return_length(s3); // 'return_length' moves back the original string value along with the computed value.
    println!("s4 is '{s4}', length is {len}");
}

fn return_length(s: String) -> (String, usize) {
    let length: usize = s.len();
    (s, length)
}
Enter fullscreen mode Exit fullscreen mode

But it is very tedious and time consuming to design your functions to move back the passed variables. What if you have a large number of passed variables?! Your code will be difficult to maintain and organize. And what if we don't want to move the variable from its original scope?! Enter: References and Borrowing.

References and borrowing:

Think of references as pointers to a piece of data (addresses) that can be used to refer to the data without changing ownership. This is called borrowing in Rust. We can re-write the tuple example from the last section as follows:

fn main() {
    let s1 = String::from("references");
    let len = return_length(&s1);
    println!("s1: '{s1}' and its length is {len}"); // won't error out as s1 wasn't moved into 'return_length'
}

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

References are defined by & as we have passed a reference to s1 to the 'return_length' function like this return_length(&s1). Note that in the 'return_length' function signature, references must be defined too. References are immutable by default. So, the following code will fail:

fn main() {
    let word = String::from("Hello");
    add_hello(&word);
    println!("word: {word}");
}

fn add_hello(s: &String) {
    s.push_str(", World!");
}
Enter fullscreen mode Exit fullscreen mode

To make this work, the variable must be mutable and its reference to usint the mut keyword:

fn main() {
    let mut word = String::from("Hello");
    add_hello(&mut word);
    println!("word: {word}");
}

fn add_hello(s: &mut String) {
    s.push_str(", World!");
}
Enter fullscreen mode Exit fullscreen mode

You can create immutable references as much as you like as long as there aren't a mutable reference for this variable. Also you can't create more than one mutable reference for the same variable. Therefore the following will fail:

fn main() {

    let mut s = String::from("Mutable");
    let s1 = &mut s;
    // ERROR: cannot borrow `s` as mutable more than once at a time
    let s2 = &mut s;
    println!("s1: {s1}, s2 {s2}");


    let mut r = String::from("ref");
    let r1 = &r; // no problem
    let r2 = &r; // no problem
    // ERROR: cannot borrow `r` as mutable because it is also borrowed as immutable
    let r3 = &mut r; // problem
    println!("{r1} {r2} {r3}");
}
Enter fullscreen mode Exit fullscreen mode

Which makes sense, as if you have referenced a variable as immutable, you don't want it to be changed mid-application (referenced as mutable). But, this can work if the immutable references go out-of-scope before the mutable reference. This allows variable mutations but in a very controlled manner.

fn main() {
    let mut x = String::from("new ref");
    let x1 = &x; // no problem
    let x2 = &x; // no problem
    println!("{x1}, {x2}");
    let x3 = &mut x; // It's OK now as x1 and x2 went out of scope.
    println!("{x3}");
}
Enter fullscreen mode Exit fullscreen mode

The rules of references:

To summarize, references follow the following two rules:

  1. You can have either one mutable reference or multiple immutable references for the same variable. (Understanding scopes will make understanding this rule easier)
  2. References must always be valid. Rust takes care of that as it never deallocates a reference as long as it is still in use.

In this article we have discussed a very interesting feature of Rust, borrowing. In the next article we will build on that knowledge and explore the Slice Type. See you then πŸ‘‹.

Top comments (0)