Rust's memory safety guarantees without garbage collection

Rust is a modern systems programming language known for its safety, speed, and concurrency features. It offers a unique blend of performance and memory safety, making it ideal for building fast and reliable software systems. With its expressive syntax and powerful type system, Rust empowers developers to write efficient and robust code for various applications, from low-level systems programming to high-level web development. One of Rust's key strengths is its ability to enforce memory safety at compile time, preventing common bugs such as null pointer dereferences and buffer overflows without using any garbage collections but ensuring other rules. We'll take a look at those rules in this Answer.

Ownership

We need to familiarize ourselves with how ownership works in Rust. These rules are enforced by the Rust compiler.

  • Each value in a Rust program has a single owner. The owner is a variable that stores the values.

  • There can only be one owner for a value at a time. Values can be the same for different owners.

  • The value is removed from memory when the owner goes out of scope.

For example, let's consider the example given below:

fn main(){
let x = String::from("Hello");
let y = x;
println!("{}", x); // line throws error
}

In the code above,

  • Line 2: Here, we created a variable x and assigned it a string "Hello". Now, x is the owner of "Hello".

  • Line 3: Then, we moved the value of x to y. This means that the ownership of x was transferred to y. and that x can no longer be used for accessing its former value.

  • Line 5: Finally, if we try to access the value of x which, now y owns, we'll be thrown an error.

Note: This error can be fixed by commenting out line 5, where we're trying to access the value of x that was transferred. If you're interested in transferring ownership, check out our answer on Moving Ownership in Rust.

This method of catching error earlier during compilation can prevent memory leaks during the program's runtime and hence even eliminate the need for a garbage collector.

References and borrowing

In Rust, if we want to move values between owners, we can employ either of the two available methods.

Immutable borrowing

Immutable referencing allows us to create references that cannot be modified by the variable referencing it. Let's have a look at how we can create immutable references.

fn main(){
let s1: String = String::from("hello");
print_string(&s1);
println!("{}", s1)
}
fn print_string(s: &String){
println!("{}", s);
}
  • Lines 1–7: Inside our main() function:

    • Line 2: We're creating a string, s1. This contains the value "hello".

    • Line 4: Here, we're calling the method that we created. Notice that we're passing it the reference using the ampersand symbol (&). This creates the immutable reference that the print_string() method will use.

  • Lines 9–11: This method, that we called print_string() takes in a string reference (&String) as a parameter.

    • Line 10: Finally, it simply prints the string reference using the println! macro.

Mutable borrowing

In contrast to immutable references, mutable ones can be mutated. Here's how we can create mutable references. Let's use the same example where our reference is being passed into a different function but this time as a mutable reference.

fn main(){
let mut s: String = String::from("hello");
append_word(&mut s);
println!("{}", s);
}
fn append_word(s: &mut String) {
s.push_str(", world!");
}

Let's have a look at the explanation of the code shown above,

  • Lines 1–7: Inside our main() function:

    • Line 2: We're creating a mutable string, s1. This contains the value, "hello".

    • Line 4: Here, we're calling the append_word() method that we created. This method takes in a mutable reference to the string being passed. Notice the &mut keyword before the string name.

  • Lines 9–11: The append_word() method, takes in a mutable string reference using the &mut keyword.

    • Line 10: Finally, this method uses the push_str() method to append another string to the one being passed.

Lifetimes

The Rust compiler automatically checks, very carefully, for variables that go out of scope and might possibly leave a dangling value. The compiler throws an error whenever it encounters such a situation, which is related to the concept of lifetimes, as given below:

fn main() {
let x: &i32;
{
let y: i32 = 5;
x = &y; // throws an error
}
println!("{}", x);
}

The code shown above, throws an error. Let's take a look at why it does that.

  • Line 3: Here, we initialized a variable, x, but didn't assign it a value. It can store references to variables of type i32.

  • Lines 4–7: We're using curly brackets to create a scope. Variables initialized inside this scope are not valid outside it.

    • Line 5: Here, we created a variable, y,

    • Line 6: We assigned x, a reference to y.

  • Line 9: If we try to use x, the compiler will throw an error

Conclusion

Rust ensures memory safety without garbage collection by enforcing ownership, borrowing, and lifetimes at compile time. This approach prevents common bugs like null pointer dereferences and buffer overflows, enhancing code reliability and performance. Understanding these principles is essential for effective Rust programming across various applications.

Free Resources

Copyright ©2025 Educative, Inc. All rights reserved