Understanding Ownership in Rust with Examples

Understanding Ownership in Rust with Examples

The Rust programming language offers a unique approach to memory management, combining aspects of both automatic and manual memory management systems. This is achieved through an " ownership " system with rules that the compiler checks at compile time. This article will introduce you to the concept of ownership in Rust with detailed examples.

What is Ownership?

In Rust, the ownership concept is a set of rules that applies to all values. These rules dictate that each value in Rust has the following:

  1. A variable called its "owner".
  2. Only one owner at a time.
  3. When the owner goes out of scope, the value will be dropped.

This system exists primarily to make memory safe, eliminating common bugs such as null pointer dereferencing, double-free errors, and dangling pointers.

The Rules of Ownership

Variable Scope

The first key concept in Rust ownership is "scope." A scope is a range within a program for which a variable is valid. Here's an example:

{
    let s = "Hello, world!";
    // s is valid here
} // s is no longer valid past this point        

In this case, s is valid from the point at which it's declared until the closing brace of its scope.

The String Type

For a more complex example, let's use the String type:

{
    let mut s = String::from("Hello, world!");
    s.push_str(", nice to meet you.");
    // s is valid and has changed
} // s is no longer valid past this point        

This case works similarly to the previous example, but we're also able to modify s. This results from the String type stored on the heap and can have a dynamic size.

Memory and Allocation

Regarding handling the String type, Rust automatically takes care of memory allocation and deallocation. In the example above, when s goes out of scope, Rust automatically calls the drop function, which returns the memory taken s back to the operating system.

Ownership and Functions

The ownership rules apply when interacting with functions as well. When a variable is passed to a function, the ownership of that variable is moved to the function (known as a "move"). After the move, the original variable can no longer be used.

fn main() {
    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
    //println!("{}", s);             // this line would lead to a compile error
}

fn takes_ownership(some_string: String) { // some_string comes into scope
    println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called. The memory is freed.        

In the example above, the println! line after the takes_ownership function call would result in a compile error because the ownership of s was moved to the takes_ownership function.

Borrowing and References

To allow access to data without taking ownership, Rust uses a concept called "borrowing." Instead of passing objects directly, we pass references to them.

fn main() {
    let s = String::from("hello"); // s comes into scope

    does_not_take_ownership(&s); // s's value is referenced here
                                 // s is still valid here
    println!("{}", s);           // this line will compile and print "hello"
}

fn does_not_take_ownership(some_string: &String) { // some_string is a reference
    println!("{}", some_string);
} // Here, some_string goes out of scope. But because it does not have ownership, nothing happens.        

In this case, &s creates a reference to the value of s but does not own it. Because it does not have ownership, the value it points to will not be dropped when the reference goes out of scope.

Note, however, that references are immutable by default. If you want to modify the borrowed value, you need to use a mutable reference with the mut keyword.

fn main() {
    let mut s = String::from("hello");
    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}        

The Slice Type

Another aspect of ownership in Rust involves the "slice" type. A slice is a reference to a contiguous sequence within a collection rather than the whole collection. Here's an example with a string slice:

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

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

In this example, hello will be a slice that references the first five bytes of s, and world will be a slice that references the following five bytes.

Rust's ownership model is a powerful tool for managing memory safety without a garbage collector. This ownership system with rules for borrowing and slicing allows for fine-grained control over memory allocation and deallocation, all while keeping the code safe from memory bugs and maintaining high performance.

Deep and Shallow Copying

Understanding the idea of deep and shallow copying is essential in understanding ownership in Rust.

Let's start with a scenario. If we have a simple type, such as an integer, and we assign its value to a new variable, the value is copied, as seen here:

let x = 5;
let y = x;

println!("x = {}, y = {}", x, y);  // Outputs: "x = 5, y = 5"        

This is because integers are simple values with a known, fixed size and are stored on the stack. Therefore, the number is copied into the new variable. This type of copying is known as "deep copying."

However, things get more complex when we deal with data stored on the heap, like a String:

let s1 = String::from("hello");
let s2 = s1;

// println!("{}, world!", s1); // This line will cause an error        

This will throw a compile error because Rust prevents you from using s1 after transferring its ownership to s2. This default behaviour is known as "shallow copying" or "moving." If a deep copy is needed, you need to call the clone method:

let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2);  // Outputs: "s1 = hello, s2 = hello"        

Now, s1 and s2 are two separate strings with the same value, "hello".

The Copy Trait

Rust has a special trait called Copy for handling cases where you want a value to be able to make a copy of itself. Simple data types like integers, booleans, floating point numbers, and character types have this trait. However, any type that requires allocation or is some form of resource, like String, does not have this trait.

If we have a type and we want to make it Copy, we can do so by adding an annotation to the type definition:

#[derive(Copy, Clone)]
struct Simple {
    a: i32,
    b: i32,
}

let s = Simple { a: 5, b: 6 };
let _t = s;

println!("{}", s.a);  // Outputs: "5"        

Conclusions

Ownership is a central feature of Rust, aiming to make memory management safe and efficient. This unique approach provides the best of both worlds: memory safety without needing a garbage collector. It enforces rules at compile-time, preventing a wide range of common programming errors related to memory use. However, it also requires a slightly different mindset when designing your Rust programs since you must always be mindful of who owns data at any time.

Stay tuned, and happy coding!

Visit my Blog for more articles, news, and software engineering stuff!

Follow me on Medium, LinkedIn, and Twitter.

Check out my most recent book — Application Security: A Quick Reference to the Building Blocks of Secure Software.

All the best,

Luis Soares

CTO | Head of Engineering | Blockchain Engineer | Solidity | Rust | Smart Contracts | Web3 | Cyber Security

#blockchain #rust #memory #safety #solana #smartcontracts #network #datastructures #data #smartcontracts #web3 #security #privacy #confidentiality #cryptography #softwareengineering #softwaredevelopment #coding #software

To view or add a comment, sign in

Insights from the community

Others also viewed

Explore topics