Ownership in action

Apart from the let binding example, there are other places where you will find ownership in effect, and it's important to recognize these and the errors the compiler gives us.

Functions: If you pass parameters to functions, the same ownership rules are in effect:

// ownership_functions.rs

fn take_the_n(n: u8) { }

fn take_the_s(s: String) { }

fn main() {
let n = 5;
let s = String::from("string");

take_the_n(n);
take_the_s(s);

println!("n is {}", n);
println!("s is {}", s);
}

The compilation fails in a similar way:

String does not implement the Copy trait, so the ownership of the value is moved inside the take_the_s function. When that function returns, the scope of the value comes to an end and drop is called on s, which frees the heap memory used by s. Therefore, s cannot be used after the function call anymore. However, since String implements Clone, we can make our code work by adding a .clone() call at the function call site:

take_the_s(s.clone());

Our take_the_n works fine as u8 (a primitive type) implements Copy.

This is to say that, after passing move types to a function, we cannot use that value later. If you want to use the value, we must clone the type and send a copy to the function instead. Now, if we only need read access to variable s, another way we could have made this code work is by passing the string s back to main. This looks something like this:

// ownership_functions_back.rs

fn take_the_n(n: u8) { }

fn take_the_s(s: String) -> String {
println!("inside function {}", s);
s
}

fn main() {
let n = 5;
let s = String::from("string");

take_the_n(n);
let s = take_the_s(s);

println!("n is {}", n);
println!("s is {}", s);
}

We added a return type to our take_the_s function and return the passed string s back to the caller. In main, we receive it in s. With this, the last line of code in main works.

Match expressions: Within a match expression, a move type is also moved by default, as shown in the following code:

// ownership_match.rs

#[derive(Debug)]
enum Food {
Cake,
Pizza,
Salad
}

#[derive(Debug)]
struct Bag {
food: Food
}

fn main() {
let bag = Bag { food: Food::Cake };
match bag.food {
Food::Cake => println!("I got cake"),
a => println!("I got {:?}", a)
}

println!("{:?}", bag);
}

In the preceding code, we create a Bag instance and assign it to bag. Next, we match on its food field and print some text. Later, we print the bag with println!. We get the following error upon compilation:

As you can clearly read, the error message says that bag has already been moved and consumed by the a variable in the match expression. This invalidates the variable bag for any further use. We'll see how to make this code work when we get to the concept of borrowing.

Methods: Within an impl block, any method with self as the first parameter takes ownership of the value on which the method is called. This means that after you call the method on the value, you cannot use that value again. This is shown in the following code:

// ownership_methods.rs

struct Item(u32);

impl Item {
fn new() -> Self {
Item(1024)
}

fn take_item(self) {
// does nothing
}
}

fn main() {
let it = Item::new();
it.take_item();
println!("{}", it.0);
}

Upon compilation, we get the following error:

take_item is an instance method that takes self as the first parameter. After its invocation, it is moved inside the method and deallocated when the function scope ends. We cannot use it again later. We'll make this code work when we get to the borrowing concept.

Ownership in closures: A similar thing happens with closures. Consider the following code snippet:

// ownership_closures.rs

#[derive(Debug)]
struct Foo;

fn main() {
let a = Foo;

let closure = || {
let b = a;
};

println!("{:?}", a);
}

As you can already guess, the ownership of Foo is moved to b inside the closure by default on assignment, and we can't access a again. We get the following output when compiling the preceding code:

To have a copy of a, we can call a.clone() inside the closure and assign it to b or place a move keyword before the closure, like so:

    let closure = move || {
let b = a;
};

This will make our program compile.

Note: Closures take values differently depending on how a variable is used inside the closure.

With these observations, we can already see that the ownership rule can be quite restrictive as it allows us to use a type only once. If a function needs only read access to a value, then we either need to return the value back again from the function or clone it before passing it to the function. The latter might not be possible if the type does not implement Clone. Cloning the type might seem like an easy thing to get around the ownership principle, but it defeats the whole point of the zero-cost promise as Clone  always duplicates types always, possibly making a call to the memory allocator APIs, which is a costly operation involving system calls.

With move semantics and the ownership rule in effect, it soon gets unwieldy to write programs in Rust. Fortunately, we have the concept of borrowing and reference types that relax the restrictions imposed by the rules but still maintains the ownership guarantees at compile time.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset