Ownership, Scope, and Moves

RustBeginner
Practice Now

Introduction

Rust uses ownership to decide which part of a program is responsible for a value and when that value can be cleaned up. Think of ownership as responsibility and permission, not as a permanent label attached to one variable name: that responsibility can move to a new binding. This prevents several memory errors without requiring a garbage collector.

In this lab, you will first see that a small integer can be copied. You will then intentionally ask Rust to use an owned String after it has moved, read the resulting compiler diagnostic, and repair the data flow by using the ownership returned from a function. The code framework is prepared, so every edit stays small and focused.

Observe Scope and a Copied Integer

In this step, you will copy a small integer into an inner scope and confirm that the original binding remains usable.

Enter the prepared project and open the source file:

cd /home/labex/project/ownership-demo
nano src/main.rs

A scope is the region where a binding is valid. Braces create a new inner scope. The binding local_count exists only between the inner pair of braces, while warehouse_count belongs to the surrounding main scope.

On the Step 1 line, replace only 0 with warehouse_count:

        let local_count = warehouse_count; // Step 1: replace the placeholder.

The integer type i32 implements Rust's Copy behavior. Assigning it to local_count copies the small value instead of invalidating warehouse_count.

Save with Ctrl+O, press Enter, and exit with Ctrl+X. Run the program:

cargo run --quiet

The first two lines prove that both bindings can be used:

Local count: 3
Warehouse count: 3

When the inner scope ends, local_count stops being valid. The outer warehouse_count remains valid until the end of main.

Read an Intentional Move Error

In this step, you will intentionally use a String after it moves, then interpret Rust's compiler diagnostic. The failed check is expected and is the learning result for this step.

String::from("PX-104") creates owned text. Unlike a small i32, a String manages a separately allocated text buffer whose size can change. Rust gives responsibility for that buffer to one owner so it can be cleaned up exactly once.

The call record_parcel(parcel_id) passes the String by value. Ownership moves from the parcel_id binding into the function. The function returns the value, and the call stores that returned owner in _returned_id. The leading underscore tells Rust that this temporary binding is intentionally unused for now.

The value is not destroyed when it moves. Only the old binding loses permission to use it. The ownership path in this program is:

parcel_id in main → parcel_id parameter in record_parcel → _returned_id in main

At every point, one current owner remains responsible for the same text.

Open the file:

nano src/main.rs

Find the line below the Step 2 comment and remove only the leading // so it becomes:

    println!("Original after move: {parcel_id}");

Save and exit. Before running the next command, predict the result: parcel_id is no longer the owner, so the compiler should reject this use.

Run the check. It is expected to fail:

cargo check

Find these stable parts of the diagnostic:

error[E0382]: borrow of moved value: `parcel_id`
...
value moved here
...
value borrowed here after move

The word borrow appears because println! only needs temporary access to display the value. That access is still impossible through parcel_id, because ownership already moved. In the next step you will use the binding that received ownership back from the function.

Continue with the Returned Owner

In this step, you will repair the move error by naming and using the value returned from record_parcel.

The function signature fn record_parcel(parcel_id: String) -> String says that the function takes ownership of a String and returns a String. Its final parcel_id expression transfers ownership back to the caller.

Open the file:

nano src/main.rs

Make two small edits. First, rename _returned_id to returned_id on the function-call line. This removes the temporary underscore because you are about to use the value.

Then replace the failing print line with the following line:

    println!("Returned owner: {returned_id}");

The old parcel_id binding remains invalid after the move. Renaming _returned_id to returned_id does not recover the old binding; it gives a normal name to the binding that already received ownership back. returned_id is now the current owner and therefore the correct name to use.

Save and exit, then check the repaired program:

cargo check

A Finished result without the E0382 error confirms that the ownership flow is valid. Run the program:

cargo run --quiet

The complete output should be:

Local count: 3
Warehouse count: 3
Recorded parcel: PX-104
Returned owner: PX-104

At the end of main, returned_id leaves scope and Rust cleans up its owned String. There is still only one owner responsible for that data.

Summary

You connected lexical scope with value lifetime, saw an i32 copy without invalidating its original binding, followed a String move into a function, interpreted E0382, and continued safely with the ownership returned to the caller.