DEV Community

Cover image for What Rust Does Differently: A Beginner's Perspective
Younes Merzouka
Younes Merzouka

Posted on

What Rust Does Differently: A Beginner's Perspective

Rust makes a lot of interesting decisions about how things work — what you can and cannot do, and how you do certain things. Coming from a mostly web development background, three concepts in particular caught my attention as a beginner: Mutability, Ownership, and Handling Null.


Mutability

Rust variables are immutable by default. So code like this in JavaScript:

let a = 0;
a += 1;
Enter fullscreen mode Exit fullscreen mode

...wouldn't work in Rust:

let a = 0;
a += 1; // compiler error
Enter fullscreen mode Exit fullscreen mode

To mutate a variable, you need to add the mut keyword:

let mut a = 0;
a += 1;
Enter fullscreen mode Exit fullscreen mode

The reason is safety. This mechanism forces you to explicitly opt into mutability, which makes your code more deterministic and avoids problems — especially with concurrency, where two threads might otherwise modify the same variable without you realising it.

Mutable References

One thing that tripped me up early on was mutable references. Suppose we want to read from stdin:

let mut s = String::new();
io::stdin().read_line(&s); // passing a reference to s
Enter fullscreen mode Exit fullscreen mode

This looks reasonable — similar to how you'd do it in C. But it actually results in a compiler error telling you that read_line expects a mutable reference. The correct version is:

let mut s = String::new();
io::stdin().read_line(&mut s);
Enter fullscreen mode Exit fullscreen mode

You have to explicitly tell the compiler that the reference itself is mutable. Why this matters will become clearer once we talk about ownership.


Ownership

Managing allocated memory is a well-known challenge in systems programming, and there are a few common approaches:

  1. Garbage collection — many high-level languages manage memory for you through a GC. This simplifies things for developers but can impact performance through "GC pauses", where the entire application halts while unused memory is freed. Not ideal for systems software.
  2. Manual memory management — the approach C takes. You allocate memory and you're responsible for freeing it. This has historically led to many bugs and security vulnerabilities: null pointer dereferences, buffer overflows, use-after-free, and so on.

Rust takes a different approach through ownership.

Consider this example:

fn add_suffix(mut s: String, suf: &str) {
    s.push_str(suf);
    drop(s); // frees the heap memory
}

fn main() {
    let mut s = String::from("Hello, ");
    add_suffix(s, "World");
    println!("{}", s); // compiler error
}
Enter fullscreen mode Exit fullscreen mode

The function add_suffix receives a String allocated on the heap, drops it (equivalent to free in C), and then main tries to access that freed memory — a classic use-after-free bug. Rust won't let this compile.

You might think the fix is simply to remove the drop:

fn add_suffix(mut s: String, suf: &str) {
    s.push_str(suf);
}

fn main() {
    let mut s = String::from("Hello, ");
    add_suffix(s, "World");
    println!("{}", s); // still a compiler error
}
Enter fullscreen mode Exit fullscreen mode

But this still doesn't work. Passing s to add_suffix as an argument transfers ownership of s to that function.

So what does transferring ownership actually mean? In Rust, each heap-allocated value is owned by the scope in which it was created. When that scope ends, the memory is automatically freed. So when add_suffix returns, s is freed — and trying to use it in println! afterwards is accessing a dead memory region.

Here's another example of the same idea:

let p: &String;
{
    let s = String::from("Hello");
    p = &s;
} // s is dropped here, memory is freed
println!("{}", *p); // compiler error: p points to freed memory
Enter fullscreen mode Exit fullscreen mode

The correct way to achieve what we originally wanted is through borrowing:

fn add_suffix(s: &mut String, suf: &str) { // takes a mutable reference
    s.push_str(suf);
}

fn main() {
    let mut s = String::from("Hello, ");
    add_suffix(&mut s, "World"); // lends s to the function
    println!("{}", s); // s is still valid here
}
Enter fullscreen mode Exit fullscreen mode

By passing a reference (&mut s), main remains the owner of the memory. The function simply borrows it temporarily. When the function returns, the borrow ends — but the memory isn't freed, because main still owns it.

This same principle applies even without mutation. If you don't need to mutate the value, you use & instead of &mut. Now the reason you need &mut for mutable references (as we saw with read_line) should make a lot more sense.


Option

Rust doesn't have null. But there are situations where you genuinely want to express: "this function either returns a result, or nothing — not because of an error, but because there's simply nothing to return."

Consider this function:

fn get_slice_by_char(s: &str, needle: char) -> &str {
    for (i, c) in s.char_indices() {
        if c == needle {
            return &s[i..];
        }
    }
    return ?; // what do we return if the character isn't found?
}
Enter fullscreen mode Exit fullscreen mode

null might seem useful here. For exactly these cases, Rust has Option<T>:

fn get_slice_by_char(s: &str, needle: char) -> Option<&str> {
    for (i, c) in s.char_indices() {
        if c == needle {
            return Some(&s[i..]);
        }
    }
    None
}
Enter fullscreen mode Exit fullscreen mode

What makes Option<T> special is that it addresses the classic problem with null: forgetting to handle it. Option<T> forces you to explicitly deal with both cases. For example, this won't compile:

fn main() {
    let s: &str = get_slice_by_char("hello, there", 't');
    // compiler error: expected &str, found Option<&str>
}
Enter fullscreen mode Exit fullscreen mode

The correct approach is to use match (similar to a switch statement) to handle both possibilities:

fn main() {
    let s: &str = match get_slice_by_char("hello, there", 't') {
        Some(slice) => slice,
        None => panic!("Character not found"),
    };
}
Enter fullscreen mode Exit fullscreen mode

The compiler won't let you use the value inside an Option without first unpacking it — which means you can never accidentally forget to handle the "nothing" case.


These three concepts — mutability, ownership, and Option — felt strange to me at first coming from other languages. But once they click, you start to see how they all work together to make Rust code safe without needing a garbage collector. Pretty elegant, honestly.

Top comments (0)