All the Rust Features

In this article, we’ll explore all (or nearly all) of the key features that make the Rust programming language unique.

If you want to understand what sets Rust apart, dive into each feature and see how it helps make Rust an efficient, safe, and powerful language.

Fun fact: Rust’s name wasn’t inspired by iron oxidation but by a resilient fungus that thrives in extreme conditions!

If you prefer a video version:

You can find me here: https://francescociulla.com

1. Memory Safety without Garbage Collection

Rust’s memory management is one of its standout features. Unlike languages that rely on a garbage collector, Rust ensures memory safety at compile time. This means there’s no runtime cost for managing memory. Instead, Rust uses a powerful system of ownership and borrowing that checks for potential issues before your program even runs.

Example

fn main() {
    let data = String::from("Hello, Rust!");
    println!("{}", data);
} // `data` is dropped here automatically, freeing memory

Rust's compiler guarantees that memory is freed without a garbage collector, resulting in more efficient memory usage.

2. Ownership System

The ownership model is fundamental to Rust’s safety. Each piece of data in Rust has a single owner, and when the owner goes out of scope, the data is automatically cleaned up. This prevents issues like dangling pointers and memory leaks.

Example

fn main() {
    let s1 = String::from("Hello");
    let s2 = s1; // `s1` is moved to `s2` and is no longer accessible
    println!("{}", s2);
}

In Rust, once data is "moved," the original variable is no longer valid, ensuring no double frees or memory leaks.

3. Borrowing and References

When you need multiple parts of your program to access the same data without transferring ownership, borrowing allows this. Rust’s references let you share data safely and lifetimes ensure that borrowed data doesn’t outlive its owner.

Example

fn main() {
    let s = String::from("Hello");
    print_length(&s);
}

fn print_length(s: &String) {
    println!("Length: {}", s.len());
} // `s` is borrowed, so ownership isn’t transferred

Borrowing keeps your program safe by controlling who can access what, preventing bugs and runtime errors.

4. Pattern Matching and Enums

Rust’s pattern matching and enums provide a powerful way to write clear, exhaustive logic that handles all possible cases. This is especially useful in error handling, where enums like Result and Option allow you to handle outcomes explicitly.

Example

enum Direction {
    North,
    South,
    East,
    West,
}

fn move_character(dir: Direction) {
    match dir {
        Direction::North => println!("Moving North"),
        Direction::South => println!("Moving South"),
        Direction::East => println!("Moving East"),
        Direction::West => println!("Moving West"),
    }
}

This ensures that every possible case is handled, making your code safe and predictable.

5. Error Handling with Result and Option

Rust avoids exceptions by making error handling a first-class citizen through Result and Option. These enums enforce safe and predictable handling, so you always consider both successful and error scenarios.

Example

fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err(String::from("Cannot divide by zero"))
    } else {
        Ok(a / b)
    }
}

With Result, the caller must handle both the happy path (Ok) and the potential error (Err), promoting robust error handling.

6. Type Inference

Although Rust is a statically typed language, you don’t need to specify types everywhere. Rust’s type inference system is smart, allowing for clean and readable code without sacrificing safety.

###Example

fn main() {
    let x = 10; // Rust infers `x` as an `i32`
    let y = 20.5; // Rust infers `y` as an `f64`
    println!("x: {}, y: {}", x, y);
}
Rust’s type inference provides both flexibility and clarity.

7. Traits and Trait Objects

Rust doesn’t use traditional inheritance. Instead, it has traits to define shared behavior, enabling polymorphism without complex inheritance chains. Trait objects, like dyn Trait, provide dynamic dispatch for flexibility.

###Example

trait Greet {
    fn say_hello(&self);
}

struct Person;
impl Greet for Person {
    fn say_hello(&self) {
        println!("Hello!");
    }
}

fn greet(g: &dyn Greet) {
    g.say_hello();
}

fn main() {
    let p = Person;
    greet(&p);
}

Traits allow shared behavior without needing inheritance, making Rust’s approach flexible and safe.

8. Concurrency without Data Races

Rust’s concurrency model prevents data races. By enforcing Send and Sync traits, Rust enables safe concurrency, letting you write highly concurrent code without bugs from unexpected data races.

Example

use std::thread;

fn main() {
    let handle = thread::spawn(|| {
        println!("Hello from a thread!");
    });

    handle.join().unwrap();
}

Rust’s compiler guarantees thread safety, making concurrent programming safer.

9. Macros for Code Generation

Rust macros support metaprogramming with two types: declarative macros for pattern matching and procedural macros for generating code. Macros help reduce boilerplate and increase code reuse.

Example

macro_rules! say_hello {
    () => {
        println!("Hello, macro!");
    };
}

fn main() {
    say_hello!();
}

Macros make code concise and reusable, keeping your Rust codebase efficient.

10. Lifetimes and Borrow Checking

Lifetimes prevent dangling references by ensuring references are valid for as long as necessary. They’re key to Rust’s safety guarantees.

Example

fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() > s2.len() {
        s1
    } else {
        s2
    }
}

fn main() {
    let result = longest("apple", "banana");
    println!("Longest: {}", result);
}

Rust’s borrow checker ensures that lifetimes are always safe and references are valid.

11. Generics and Type System

Rust’s generic system allows you to write flexible, reusable, and type-safe code. With trait bounds, you can define flexible functions and structures without sacrificing safety.

###Example

fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

Generics make it easy to write safe, reusable code without duplicating functionality.

12. Unsafe Code

Rust is safe by default, but in cases where performance or low-level access is needed, unsafe code lets you bypass Rust’s safety checks. Use it sparingly.

Example

fn main() {
    let x: *const i32 = &10;
    unsafe {
        println!("Value at x: {}", *x);
    }
}
Unsafe code is powerful but requires caution, as it bypasses Rust’s guarantees.

13. Async/Await for Concurrency

With async/await, Rust provides non-blocking concurrency, useful for high-performance I/O operations.

Example

use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    println!("Starting...");
    sleep(Duration::from_secs(1)).await;
    println!("Done!");
}

Async programming in Rust allows efficient and scalable applications.

14. Cargo for Dependency and Package Management

Cargo is Rust’s build system and package manager, helping manage dependencies, build projects, and run tests.

Example

cargo new my_project
cd my_project
cargo build

Cargo simplifies project management, and the extensive ecosystem of crates makes development in Rust easy.

If you prefer a video version:

You can find me here: https://francescociulla.

1
Subscribe to my newsletter

Read articles from Francesco Ciulla directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Francesco Ciulla
Francesco Ciulla

👋 Hi, I Am Francesco I am a Computer Scientist interested in Web3 and DevRel. I worked from 2017 to 2020 on the Copernicus project for the ESA European Space Agency as a Fullstack Developer. Docker Captain I have interviewed 195+ Developers on my YouTube Channel I am a Developer Advocate at daily.dev I have founded 4C, a community focused on Content Creation.