Skip to main content

Command Palette

Search for a command to run...

Rust Week 2: Diving Deep into Ownership, Collections, and Error Handling

Updated
16 min read
Rust Week 2: Diving Deep into Ownership, Collections, and Error Handling
N
🚀 Greetings World! 🌐 Meet a dynamic Frontend Developer, UI/UX Designer, and avid explorer of Cloud & DevOps realms! Uncover the journey of a professional deeply passionate about crafting seamless user experiences, designing visually stunning interfaces, and navigating the cloud with a DevOps mindset. 🔧 Skills Snapshot: - Frontend Mastery: HTML, CSS, and JavaScript expert, specializing in React, Angular, and Vue.js. - Design Wizardry: Proficient in wireframing, prototyping, and Adobe Creative Suite and Figma for captivating designs. - Cloud Maestro: Fluent in AWS, Azure, and Google Cloud Platform, adept at architecting scalable solutions. - DevOps Guru: Skilled in Docker, Kubernetes, Jenkins, and Git, contributing to efficient development workflows. 🔗 Let's Connect: Open to collaborating on exciting projects and sharing industry insights, I invite connections for networking or discussions. Reach out for potential collaborations. 📧 Contact Me: -Portfolio:[https://www.nehalingole.in/] - GitHub: [GitHub Profile](https://github.com/Ingole712521) - Email: [nehalingole2001@gmail.com](mailto:nehalingole2001@gmail.com) - Mobile: 7397966719 - Figma: [Figma Profile](https://www.figma.com/@nehalingole) - Twitter: [Twitter Profile](https://twitter.com/IngoleNehal) - HashNode: [HashNode Profile](https://hashnode.com/@Nehal71) - LinkedIn : [LinkedIn Profile](https://www.linkedin.com/in/nehal-ingole/)

My journey through the core concepts that make Rust unique and powerful


Introduction

Welcome back to my Rust learning journey. After getting comfortable with the basics in Week 1, Week 2 was where things got really interesting. This week, I dove headfirst into what makes Rust truly special - its ownership model, borrowing system, and the powerful ways it handles data without a garbage collector.

If Week 1 was learning to walk, Week 2 was learning to run while juggling. Let me share everything I learned.


1. Ownership - Rust's Superpower

What is Ownership?

Ownership is Rust's most unique feature. It enables Rust to make memory safety guarantees without needing a garbage collector. In simple terms, ownership is a set of rules that the compiler checks at compile time.

Think of ownership like being the owner of a house. When you own a house, you are responsible for it. When you move out, you clean up after yourself. Rust works the same way with memory - each value has a single owner, and when that owner goes out of scope, Rust automatically cleans up the memory.

fn main() {
    let house = String::from("My House");
}

Stack vs Heap

Understanding stack and heap is crucial for grasping ownership.

The stack stores values in the order it gets them and removes them in the opposite order. This is called last in, first out. All data stored on the stack must have a known, fixed size.

The heap is less organized. When you put data on the heap, you request a certain amount of space. The operating system finds an empty spot somewhere on the heap, marks it as being in use, and returns a pointer to that location.

Imagine the stack as a tidy desk where you keep your notebook, pen, and coffee mug. Everything has its place and size, and you can grab things quickly. The heap is like a warehouse where you store boxes of various sizes - you need to find space, put things away, and remember where you put them.

let x = 5;
let s = String::from("hello");

Move vs Copy

Some types in Rust are like original documents - they cannot be copied, only moved. Others are like photocopies - they can be duplicated freely.

When you assign an integer to another variable, a copy is created. Both variables can be used independently.

When you assign a String to another variable, the ownership moves. The original variable can no longer be used because the ownership of the heap memory has been transferred.

let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);

let s1 = String::from("hello");
let s2 = s1;
println!("{}", s2);

Types like integers, booleans, and floating-point numbers implement the Copy trait. Types like String, vectors, and other heap-allocated types do not.


2. Borrowing and References

What is Borrowing?

When a function wants to use a value but not take ownership, it can borrow the value using references. Borrowing allows you to access data without taking ownership.

Think of it like borrowing a book from the library. You can read it, but you don't own it, and you must return it.

Immutable References

An immutable reference allows you to read data but not modify it. You can have multiple immutable references to the same data at the same time.

fn main() {
    let book = String::from("Rust Programming");
    
    let reader1 = &book;
    let reader2 = &book;
    println!("Readers see: {} and {}", reader1, reader2);
}

Mutable References

A mutable reference allows you to both read and modify data. However, you can only have one mutable reference to a particular piece of data at a time.

fn main() {
    let mut book = String::from("Rust Programming");
    
    let editor = &mut book;
    editor.push_str(" - 2nd Edition");
    
    println!("Edited book: {}", book);
}

The Rules of Borrowing

  1. You can have either one mutable reference or any number of immutable references, but not both at the same time.

  2. References must always be valid - no dangling references.

These rules prevent data races at compile time. A data race happens when:

  • Two or more pointers access the same data at the same time

  • At least one of them is writing to the data

  • There's no mechanism to synchronize access


3. Slices

What are Slices?

Slices let you reference a contiguous sequence of elements in a collection rather than the whole collection. A slice does not take ownership - it's a view into a portion of data.

Think of slices like looking through a window at a specific part of a landscape. You don't own the land, but you can see a portion of it.

fn main() {
    let text = String::from("Hello, Rust!");
    
    let hello = &text[0..5];
    let rust = &text[7..11];
    
    println!("First word: {}", hello);
    println!("Second word: {}", rust);
}

String Slices

A string slice is a reference to part of a String. The syntax &text[0..5] creates a slice starting at index 0 and continuing up to, but not including, index 5.

String slices have the type &str.

Array Slices

Slices work with arrays too. They give you a view into a portion of an array.

let numbers = [1, 2, 3, 4, 5];
let slice = &numbers[1..4];

String vs &str

  • String is an owned type that stores its data on the heap. You can grow, shrink, and modify it.

  • &str is a borrowed reference to a string. It's a view into a string that you don't own.

When you need to own string data, use String. When you just need to read string data, use &str.


4. Structs

What are Structs?

A struct is a custom data type that lets you name and package together multiple related values. Think of it as creating a blueprint for a concept in your program.

struct User {
    username: String,
    email: String,
    active: bool,
    age: u32,
}

Creating Instances

To use a struct, you create an instance by providing concrete values for each field.

fn main() {
    let user1 = User {
        username: String::from("rustacean"),
        email: String::from("rust@example.com"),
        active: true,
        age: 25,
    };
}

Accessing and Modifying Fields

You access struct fields using dot notation. To modify a field, the entire instance must be mutable.

fn main() {
    let mut user1 = User {
        username: String::from("rustacean"),
        email: String::from("rust@example.com"),
        active: true,
        age: 25,
    };
    
    println!("User: {}", user1.username);
    user1.email = String::from("newemail@example.com");
}

Field Init Shorthand

When variable names match field names, you can use a shorthand syntax.

fn build_user(username: String, email: String) -> User {
    User {
        username,
        email,
        active: true,
        age: 30,
    }
}

5. impl and Methods

What are Methods?

Methods are functions attached to structs. They define the behavior of your structs. The first parameter of a method is always self, which represents the instance the method is being called on.

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
    
    fn double_width(&mut self) {
        self.width *= 2;
    }
}

Types of Method Parameters

  • &self - borrows the instance immutably, allowing read-only access

  • &mut self - borrows the instance mutably, allowing modification

  • self - takes ownership of the instance, rarely used

Associated Functions

Functions defined in an impl block that don't take self as a parameter are called associated functions. They're often used as constructors.

impl Rectangle {
    fn square(size: u32) -> Rectangle {
        Rectangle {
            width: size,
            height: size,
        }
    }
}

fn main() {
    let square = Rectangle::square(15);
}

Associated functions are called using double colons: Rectangle::square(15).


6. Enums

What are Enums?

Enums allow you to define a type by enumerating its possible variants. They're perfect for situations where a value can be one of several possibilities.

Think of it like ordering coffee - it can be espresso, latte, or cappuccino, each with different properties.

enum Coffee {
    Espresso,
    Latte { milk_type: String, shots: u8 },
    Cappuccino(u8),
}

Enum Variants

Enums can have different kinds of variants:

  • Unit variants - just the name, no associated data

  • Struct variants - named fields like a struct

  • Tuple variants - unnamed fields like a tuple

Pattern Matching with match

The match expression allows you to compare a value against a series of patterns and execute code based on which pattern matches.

fn describe_coffee(coffee: Coffee) {
    match coffee {
        Coffee::Espresso => println!("Strong and short"),
        Coffee::Latte { milk_type, shots } => {
            println!("Latte with {} milk, {} shots", milk_type, shots);
        }
        Coffee::Cappuccino(shots) => {
            println!("Cappuccino with {} shots", shots);
        }
    }
}

The Power of Enums

Enums in Rust are extremely powerful because they can hold data. This is different from enums in many other languages where they're just numbered constants. This feature makes error handling with Option and Result possible.


7. Vectors

What are Vectors?

Vectors (Vec<T>) are resizable arrays. They store multiple values of the same type in a single data structure, with all values placed next to each other in memory.

Think of them as a backpack where you can keep adding and removing items.

fn main() {
    let mut v1: Vec<i32> = Vec::new();
    let mut v2 = vec![1, 2, 3];
}

Creating Vectors

You can create vectors in two ways:

  • Using Vec::new() - creates an empty vector

  • Using the vec! macro - creates a vector with initial values

Adding and Removing Elements

  • push adds an element to the end

  • pop removes the last element and returns it as Option<T>

v1.push(10);
v1.push(20);
let last = v1.pop();

Accessing Elements

You can access vector elements by index using either &v[index] or the get method.

let third = &v2[2];
match v1.get(1) {
    Some(value) => println!("Value: {}", value),
    None => println!("No value"),
}

The indexing syntax panics if the index is out of bounds. The get method returns an Option that you can handle safely.

Iterating Over Vectors

There are three ways to iterate over vectors, each with different ownership implications:

let v = vec![1, 2, 3];

for i in &v {
    println!("{}", i);
}

let mut v_mut = vec![1, 2, 3];
for i in &mut v_mut {
    *i *= 2;
}

for i in v {
    println!("{}", i);
}
  • for i in &v - borrows immutably, v remains usable

  • for i in &mut v - borrows mutably, v remains usable

  • for i in v - takes ownership, v is consumed


8. HashMap

What is HashMap?

A HashMap<K, V> stores a mapping of keys of type K to values of type V. It does this using a hashing function that determines how to place these keys and values in memory.

Think of it like a dictionary - you look up words to find their definitions.

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    
    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);
}

Accessing Values

You can get values from a HashMap using the get method, which returns an Option<&V>.

let team_name = String::from("Blue");
match scores.get(&team_name) {
    Some(score) => println!("Score: {}", score),
    None => println!("Team not found"),
}

Iterating

You can iterate over key-value pairs in a HashMap.

for (key, value) in &scores {
    println!("{}: {}", key, value);
}

Updating a HashMap

When you want to insert a value only if the key has no value, you can use the entry API.

scores.entry(String::from("Blue")).or_insert(25);
scores.entry(String::from("Red")).or_insert(30);

The or_insert method returns a mutable reference to the value for this key. If the key exists, it returns a reference to the existing value. If not, it inserts the parameter as the new value and returns a reference to that.

Updating Based on Old Value

A common pattern is to look up a key's value and then update it based on the old value.

let text = "hello world wonderful world";
let mut word_counts = HashMap::new();

for word in text.split_whitespace() {
    let count = word_counts.entry(word).or_insert(0);
    *count += 1;
}

This counts the occurrences of each word in the text.


9. Option

What is Option?

Option<T> is an enum that represents the concept of a value being present or absent. It's Rust's way of handling null values without the problems of null references.

enum Option<T> {
    Some(T),
    None,
}

Think of it like a box that either contains something or is empty.

fn divide(numerator: f64, denominator: f64) -> Option<f64> {
    if denominator == 0.0 {
        None
    } else {
        Some(numerator / denominator)
    }
}

Why Option Instead of Null?

In languages with null, every variable can potentially be null, leading to null pointer exceptions. In Rust, you must explicitly handle the possibility of a missing value by using Option<T>. This makes your code safer because you can't forget to handle the None case.

Using Option with match

The primary way to work with Option is using match to handle both cases.

fn main() {
    let result1 = divide(10.0, 2.0);
    
    match result1 {
        Some(value) => println!("Result: {}", value),
        None => println!("Cannot divide by zero"),
    }
}

if let Syntax

When you only care about the Some case, you can use if let for a more concise syntax.

if let Some(value) = result1 {
    println!("Value: {}", value);
}

Common Option Methods

Option<T> has many useful methods:

  • unwrap() - returns the value if Some, panics if None (use cautiously)

  • expect("message") - similar to unwrap but with a custom panic message

  • unwrap_or(default) - returns the value if Some, otherwise returns default

  • map() - applies a function to the contained value if Some


10. Result<T, E>

What is Result?

Result<T, E> is an enum for operations that can succeed or fail. It's similar to Option but includes error information when something goes wrong.

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Think of it as a receipt from a vending machine - either you get your snack or an error message.

use std::fs::File;
use std::io::Read;

fn read_username_from_file() -> Result<String, std::io::Error> {
    let mut file = match File::open("hello.txt") {
        Ok(file) => file,
        Err(e) => return Err(e),
    };
    
    let mut username = String::new();
    match file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => Err(e),
    }
}

Handling Results with match

You use match with Result just like with Option.

fn main() {
    match read_username_from_file() {
        Ok(name) => println!("Username: {}", name),
        Err(error) => println!("Error reading file: {}", error),
    }
}

The ? Operator

The ? operator is a shorthand for the common pattern of returning early on error. It does almost exactly what the match statement does.

fn read_username_concise() -> Result<String, std::io::Error> {
    let mut file = File::open("hello.txt")?;
    let mut username = String::new();
    file.read_to_string(&mut username)?;
    Ok(username)
}

If the value is Ok, the ? operator returns the value inside. If the value is Err, it returns the error from the function early.

When to Use Result

Use Result when an operation can fail in ways you need to communicate to the caller. File I/O, network requests, and data parsing are common examples.


Key Takeaways from Week 2

  1. Ownership is liberating, not restricting - Once it clicks, you realize it prevents entire classes of bugs. The compiler ensures memory safety without a garbage collector.

  2. Borrowing rules feel strict but prevent data races - No more wondering who modified what. The rules guarantee that data races cannot happen at compile time.

  3. Enums with match make code expressive - Pattern matching forces you to handle all cases, making your code more robust and self-documenting.

  4. Option and Result force you to handle edge cases - No more null pointer exceptions. The type system ensures you handle both success and failure cases.

  5. Vectors and HashMaps require ownership awareness - Always think about whether you are borrowing or moving. Understanding this prevents subtle bugs.

  6. Structs and impl blocks organize code - They help you group related data and behavior together, making your code more maintainable.

  7. Slices provide safe views into data - They let you work with parts of collections without copying or taking ownership.


What's Next

Week 3 will dive into more advanced topics:

  • Error handling mastery with the question mark operator and custom error types

  • Generics for writing flexible, reusable code

  • Traits for defining shared behavior

  • Lifetimes for ensuring references are always valid

  • Writing tests to verify your code works correctly


Final Thoughts

Rust's learning curve is steep, but Week 2 showed me why it is worth it. The ownership model, which seemed so restrictive at first, actually gives me confidence that my code will work as intended. No more unexpected nulls, no more data races, no more memory leaks - just clean, predictable behavior.

The type system in Rust doesn't just catch errors - it guides you toward better design. When you struggle with the borrow checker, it's often because your code has a fundamental design issue that would have caused problems in other languages anyway.

The best analogy I have found for Rust is that it is like having a strict but helpful mentor who catches your mistakes before they become problems. Yes, you have to follow the rules, but those rules make you a better programmer.

As I continue this journey, I'm excited to see what Week 3 brings. The foundation from Week 2 is solid, and I feel ready to tackle more advanced concepts.

Happy coding, and remember: the Rust compiler is your friend, not your enemy. It's there to help you write better, safer code.

Connect with :