Welcome! Today, we will navigate the realm of variable ownership in Rust. This principle forms the crux of Rust's performance and safety. To visualize this, consider how you solely possess a book before handing it to a sibling. Rust variables adhere to a similar convention. We'll delve into Copy
and non-Copy
types, understand variable ownership within functions.
Variable ownership is the star feature of Rust that differentiates it from other languages. The three rules of ownership are:
-
Each value in Rust has a variable that’s called its owner. This means that there's always one and only one variable bound to any given piece of data. There can only be one owner at a time.
-
When you assign the value of one variable to another, the first variable will no longer hold that value if its type does not implement the
Copy
trait. We could say it's a bit like passing a baton in a relay race! -
When the owner goes out of scope, the value will be dropped. This means once the variable that owns the data is done (like at the end of the function or a block of code), Rust automatically cleans up and frees the memory associated with that data. It's like when you're done reading a library book and return it, the book is no longer in your possession and can be borrowed by someone else.
Rust features the Copy
trait for types of a fixed size that can be safely duplicated. When Copy
types are assigned, the data is reproduced.
The following data types are Copy
types:
- integers and floating points (
i32
,f64
,u32
, etc.) char
bool
Let's take a look at an example:
Rust1fn main() { 2 let x = 5; // x, an integer, is a Copy type 3 let y = x; // y receives a copy of x's value 4 println!("x = {}, y = {}", x, y); // Here, x and y are both valid 5}
In this code, y
is assigned a duplicate of x
’s value. Therefore, after the assignment operation, both x
and y
are valid. x
and y
each own their own value of 5.
Rust also encompasses non-Copy
types, such as String
, Vec<T>
, etc. For these types, the actual data isn't copied, but the reference is. The 2nd rule of ownership dictates that when you assign the value of one variable to another, the first variable will no longer hold that value if its type does not implement the Copy
trait.
Consider Strings (non-Copy types) as an example:
Rust1fn main() { 2 let s1 = String::from("hello"); // s1 is a String, hence it's a non-Copy data type 3 let s2 = s1; // here, s1's ownership is transferred to s2 4 println!("{}", s1); // This will result in a compile-time error 5}
In this snippet, once s1
is assigned to s2
, s2
becomes the owner of the value, and s1
is invalidated.
Functions, in Rust, operate similarly. When a variable is passed to a function, its ownership is transferred.
Here's an example:
Rust1fn main() { 2 let s = String::from("hello"); 3 take_ownership(s); // s transfers ownership to `take_ownership` 4 // After this point, s becomes invalid 5} 6fn take_ownership(some_string: String) { 7 println!("{}", some_string); 8}
Here's a step-by-step of what's happening:
-
let s = String::from("hello");
- A newString
object is created. The variables
becomes the owner of thisString
. -
take_ownership(s);
- Thes
string is passed totake_ownership
function. When we passs
to this function, we are transferring the ownership ofs
to the function's parametersome_string
. Once the function takes ownership,s
no longer has access to theString
object. -
some_string: String
- The function declares a parametersome_string
which is of typeString
. This means it expects an owner to aString
value. -
When we call
println!("{}", some_string);
, it will print the value of theString
to the console. -
After the
take_ownership
function finishes executing,some_string
goes out of scope, and Rust automatically frees up the memorysome_string
occupies.
One important feature to note is that println!
is a macro, not a function. This means passing a variable into println!
does not transfer ownership.
Rust1fn main() { 2 let x = 5; 3 make_copy(x); // x is a Copy type. make_copy takes ownership of a new x value 4 // Here, x is still valid as it's a Copy type 5} 6fn make_copy(some_integer: i32) { 7 println!("{}", some_integer); 8}
Here's a step-by-step of what's happening:
-
let x = 5;
- This line creates an integer variablex
and gives it a value of 5. Integers in Rust have theCopy
trait, which means that when they are used as function arguments, what is actually passed is a copy of the data, not the original data itself. -
make_copy(x);
- Themake_copy
function is called withx
as an argument. Becausex
is a type that implements theCopy
trait, it is copied when passed to the function. This means the function gets its own version ofx
's value to work with, and the originalx
inmain
is unaffected by whatever happens to this copy inside the function. After this line,x
is still perfectly valid and accessible in themain
function scope. -
some_integer: i32
- The function takes one parameter,some_integer
, which is of typei32
Likex
, this is also aCopy
type. -
println!("{}", some_integer);
- The function prints the value ofsome_integer
to the console. Ifmake_copy
changedsome_integer
in any way (which it doesn't in this example), it would only change its copy, notx
inmain
. -
After
make_copy
finishes executing,some_integer
goes out of scope, and Rust automatically frees up the memorysome_integer
occupies.
Rust1fn main() { 2 let s = give_ownership(); // s becomes owner of value returned by gives_ownership 3 // s is valid 4} 5fn give_ownership() -> String { 6 let s = String::from("Hello World!"); 7 s 8}
let s = give_ownership();
- In themain
function, a new variables
is declared. It is set to the value returned bygive_ownership()
. This means thats
becomes the owner of theString
value thatgive_ownership()
returns.fn give_ownership() -> String
- This function signature tells us thatgive_ownership
will return aString
value when it's called.let s = String::from("Hello World!");
- Inside the function, we declare a newString
variables
and initialize it with the value"Hello World!"
. TheString
variable is owned bys
inside thegive_ownership
functions
- The function returns the value ofs
, giving ownership of theString
tos
in themain
function.- After the call to
give_ownership()
,s
is a validString
in themain
function's scope. You can uses
just like any other validString
in Rust.
Rust1fn main() { 2 let s = String::from("Hello World!"); 3 let s = take_and_give(s); 4} 5 6fn take_and_give(some_string: String) { 7 some_string 8}
This Rust code demonstrates ownership transfer to and from a function. Here is a breakdown of what happens:
let s = String::from("Hello World!");
In themain
function, we declare a variables
and initialize it with aString
containing the text "Hello World!". TheString
is owned bys
in themain
functionlet s = take_and_give(s);
- Callingtake_and_give
transfers ownership of the value ins
to this new function.take_and_give
then returns theString
ands
takes ownership of theString
once again.
Understanding these concepts will equip you to write efficient and safe code in Rust. Practice this knowledge through hands-on exercises for effective learning. The upcoming session will present problems for you to tackle, further enhancing your understanding of Rust's variable ownership rules. Happy coding!