DEV Community

Quinterabok
Quinterabok

Posted on

Understanding Rust Concepts

What Are Data Structures?

Think of Data Structures Like Containers in Real Life

Imagine your kitchen:

  • Spice rack = Array/Vector (items in order, numbered shelves)
  • Dictionary = HashMap (look up word → definition)
  • Chain of paper clips = LinkedList (each connects to the next)

Why Do We Need Data Structures?

// Without data structures - messy!
let student1_name = "Alice";
let student1_grade = 85;
let student2_name = "Bob";
let student2_grade = 92;
// This gets crazy with 100 students!

// With data structures - organized!
let mut students = HashMap::new();
students.insert("Alice", 85);
students.insert("Bob", 92);
// Easy to add 100 more!
Enter fullscreen mode Exit fullscreen mode

Ownership - The Heart of Rust

What Is Ownership? (Simple Analogy)

Think of ownership like owning a car:

  • Only ONE person can own a car at a time
  • When you sell your car, you can't drive it anymore
  • You can lend your car (borrow) but you're still the owner
  • When the owner dies, the car is destroyed

The Three Rules of Ownership

// Rule 1: Each value has exactly ONE owner
let x = String::from("Hello");  // x owns the string "Hello"

// Rule 2: When owner goes out of scope, value is dropped
{
    let y = String::from("World"); // y owns "World"
} // y goes out of scope here, "World" is destroyed

// Rule 3: There can only be one owner at a time
let a = String::from("Test");
let b = a;  // Ownership moves from 'a' to 'b'
// println!("{}", a); // ERROR! 'a' no longer owns anything
println!("{}", b); // This works, 'b' is the owner
Enter fullscreen mode Exit fullscreen mode

Why Does Rust Do This?

Problem in other languages:

// C code - dangerous!
char* ptr1 = malloc(100); //memory allocation(malloc)
char* ptr2 = ptr1;  // Both point to same memory
free(ptr1);         // Memory freed
printf("%s", ptr2); // CRASH! Using freed memory
Enter fullscreen mode Exit fullscreen mode

Rust solution:

// Rust prevents this at compile time!
let s1 = String::from("Hello");
let s2 = s1;  // s1 can no longer be used
// println!("{}", s1); // Compile error - prevents crash!
Enter fullscreen mode Exit fullscreen mode

Understanding Move vs Copy

Some types are "Copy" (cheap to duplicate):

let x = 5;      // integers are Copy
let y = x;      // x is copied to y
println!("{} {}", x, y); // Both work! 5 5
Enter fullscreen mode Exit fullscreen mode

Some types are "Move" (expensive to duplicate):

let s1 = String::from("Hello");  // String is not Copy
let s2 = s1;                     // s1 moves to s2
// println!("{}", s1);           // ERROR! s1 no longer valid
println!("{}", s2);              // OK! s2 owns the string
Enter fullscreen mode Exit fullscreen mode

When Does Ownership Transfer?

fn take_ownership(s: String) {
    println!("I now own: {}", s);
} // s goes out of scope and is dropped

fn main() {
    let my_string = String::from("Hello");
    take_ownership(my_string);  // Ownership moves to function
    // println!("{}", my_string); // ERROR! No longer own it
}
Enter fullscreen mode Exit fullscreen mode

Borrowing - Using Without Owning

What Is Borrowing? (Real Life Analogy)

Like borrowing a friend's book:

  • You can read it, but you don't own it
  • You must return it when done
  • The owner can't throw it away while you're reading
  • Multiple people can read it, but only one can write in it

Immutable Borrowing (Reading Only)

fn read_string(s: &String) {  // &String means "borrow a String"
    println!("I'm reading: {}", s);
    // I can read but not modify
} // Borrow ends here, ownership returns to caller

fn main() {
    let my_string = String::from("Hello");
    read_string(&my_string);  // Lend the string
    println!("I still own: {}", my_string); // Still have it!
}
Enter fullscreen mode Exit fullscreen mode

Mutable Borrowing (Reading and Writing)

fn modify_string(s: &mut String) {  // &mut = mutable borrow
    s.push_str(" World");  // I can modify it
}

fn main() {
    let mut my_string = String::from("Hello");
    modify_string(&mut my_string);  // Lend it mutably
    println!("Modified: {}", my_string); // "Hello World"
}
Enter fullscreen mode Exit fullscreen mode

The Borrowing Rules

// Rule 1: Many immutable borrows are OK
let s = String::from("Hello");
let r1 = &s;  // OK
let r2 = &s;  // OK
let r3 = &s;  // OK - multiple readers
println!("{} {} {}", r1, r2, r3);

// Rule 2: Only ONE mutable borrow at a time
let mut s = String::from("Hello");
let r1 = &mut s;  // OK
// let r2 = &mut s;  // ERROR! Can't have two mutable borrows
r1.push_str(" World");

// Rule 3: Can't mix mutable and immutable borrows
let mut s = String::from("Hello");
let r1 = &s;      // Immutable borrow
// let r2 = &mut s;  // ERROR! Can't borrow mutably while immutably borrowed
println!("{}", r1);
Enter fullscreen mode Exit fullscreen mode

Why These Rules?

// Without rules, this could happen:
let mut vec = vec![1, 2, 3];
let first = &vec[0];        // Borrow first element
vec.clear();                // This would invalidate 'first'!
// println!("{}", first);   // CRASH! Reading invalid memory

// Rust prevents this:
let mut vec = vec![1, 2, 3];
let first = &vec[0];        // Immutable borrow
// vec.clear();             // ERROR! Can't modify while borrowed
println!("{}", first);      // Safe!
Enter fullscreen mode Exit fullscreen mode

Lifetimes - How Long Do References Live?

What Are Lifetimes? (Simple Analogy)

Think of lifetimes like library book due dates:

  • Every borrowed book has a return date
  • You can't keep the book longer than allowed
  • The library (compiler) checks you return on time

Basic Lifetime Example

fn main() {
    let r;                // Declare a reference
    {
        let x = 5;        // x lives in this scope
        r = &x;           // r borrows x
    }                     // x dies here
    // println!("{}", r); // ERROR! r points to dead value
}
Enter fullscreen mode Exit fullscreen mode

Why Do We Need Lifetime Annotations?

// This function is confusing to Rust:
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x  // Return might come from x
    } else {
        y  // Or from y
    }
}
// Rust asks: "How long will the returned reference live?"
Enter fullscreen mode Exit fullscreen mode

Adding Lifetime Annotations

// 'a is a lifetime parameter - like a generic type but for lifetimes
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
// This says: "The returned reference lives as long as the shorter of x or y"

fn main() {
    let string1 = String::from("long string is long");
    {
        let string2 = String::from("xyz");
        let result = longest(&string1, &string2);
        println!("The longest is {}", result); // OK - both live here
    }
    // result can't be used here because string2 is dead
}
Enter fullscreen mode Exit fullscreen mode

Lifetime in Structs

// This struct holds a reference
struct ImportantExcerpt<'a> {
    part: &'a str,  // This reference must live as long as the struct
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("Could not find a '.'");

    let i = ImportantExcerpt {
        part: first_sentence,  // Borrow from novel
    };

    println!("Excerpt: {}", i.part);
    // novel must live as long as 'i' exists
}
Enter fullscreen mode Exit fullscreen mode

Data Structures

Vectors - Dynamic Arrays

Think of Vec like a expandable shelf:

  • Items are numbered (indexed)
  • Can add more items at the end
  • All items are the same type
// Creating vectors
let mut numbers = Vec::new();           // Empty vector
let mut fruits = vec!["apple", "banana"]; // With initial values

// Adding items
numbers.push(1);
numbers.push(2);
fruits.push("orange");

// Accessing items - two ways:
// 1. Panics if index doesn't exist
let first = numbers[0];  // Gets 1, panics if empty

// 2. Safe access - returns Option
match numbers.get(0) {
    Some(value) => println!("First: {}", value),
    None => println!("Vector is empty"),
}

// Iterating
for number in &numbers {
    println!("Number: {}", number);
}

// Ownership with vectors
let mut vec = vec![1, 2, 3];
let first = vec.remove(0);  // Takes ownership of element
println!("Removed: {}", first);
println!("Remaining: {:?}", vec); // [2, 3]
Enter fullscreen mode Exit fullscreen mode

HashMap - Key-Value Storage

Think of HashMap like a phone book:

  • Look up by name (key) to get phone number (value)
  • Very fast lookups
  • No particular order
use std::collections::HashMap;

// Creating
let mut scores = HashMap::new();

// Adding data
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Red"), 50);

// Looking up - safe way
match scores.get("Blue") {
    Some(score) => println!("Blue team: {}", score),
    None => println!("Team not found"),
}

// Updating values - the entry API
let text = "hello world wonderful world";
let mut word_count = HashMap::new();

for word in text.split_whitespace() {
    let count = word_count.entry(word).or_insert(0);
    *count += 1;
}
println!("{:?}", word_count); // {"hello": 1, "world": 2, "wonderful": 1}

// Ownership considerations
let mut map = HashMap::new();
let key = String::from("color");
let value = String::from("blue");

map.insert(key, value);
// key and value are now owned by the map!
// println!("{}", key); // ERROR! No longer own key
Enter fullscreen mode Exit fullscreen mode

LinkedList - Chain of Items

Think of LinkedList like a chain:

  • Each link points to the next
  • Easy to insert/remove in middle
  • Harder to find specific items
use std::collections::LinkedList;

let mut list = LinkedList::new();

// Adding items
list.push_back(1);    // Add to end
list.push_front(0);   // Add to beginning
list.push_back(2);

// Result: 0 -> 1 -> 2

// Iterating
for item in &list {
    println!("Item: {}", item);
}

// When to use LinkedList vs Vec:
// Vec: Almost always better (fast access, cache-friendly)
// LinkedList: Only when you frequently insert/remove in middle
//             AND you have pointers to the nodes
Enter fullscreen mode Exit fullscreen mode

Putting It All Together - Practical Examples

Example 1: Student Grade Manager

use std::collections::HashMap;

struct Student {
    grades: Vec<f32>,
}

impl Student {
    fn new() -> Student {
        Student {
            grades: Vec::new(),
        }
    }

    fn add_grade(&mut self, grade: f32) {
        self.grades.push(grade);
    }

    fn average(&self) -> f32 {  // Note: &self (borrowing)
        if self.grades.is_empty() {
            0.0
        } else {
            self.grades.iter().sum::<f32>() / self.grades.len() as f32
        }
    }
}

fn main() {
    let mut class = HashMap::new();

    // Create students
    let mut alice = Student::new();
    alice.add_grade(85.0);
    alice.add_grade(92.0);

    let mut bob = Student::new();
    bob.add_grade(78.0);
    bob.add_grade(88.0);

    // Add to class
    class.insert("Alice", alice);
    class.insert("Bob", bob);

    // Calculate averages
    for (name, student) in &class {  // Borrow the HashMap
        println!("{}: {:.1}", name, student.average());
    }
}
Enter fullscreen mode Exit fullscreen mode

Example 2: Understanding Ownership Flow

fn demonstrate_ownership_flow() {
    // Step 1: Create owned data
    let mut words = vec![
        String::from("hello"),
        String::from("world"),
        String::from("rust")
    ];

    // Step 2: Borrow for reading
    print_words(&words);  // words is borrowed, not moved

    // Step 3: Borrow for modification
    add_word(&mut words, String::from("programming"));

    // Step 4: Transfer ownership
    let processed = process_words(words);  // words moves into function

    // words is no longer accessible here!
    println!("Processed: {:?}", processed);
}

fn print_words(words: &Vec<String>) {  // Immutable borrow
    for word in words {
        println!("Word: {}", word);
    }
}  // Borrow ends here

fn add_word(words: &mut Vec<String>, word: String) {  // Mutable borrow
    words.push(word);
}  // Borrow ends here

fn process_words(words: Vec<String>) -> Vec<String> {  // Takes ownership
    words.into_iter()
        .map(|word| word.to_uppercase())
        .collect()
}  // Original words is dropped here
Enter fullscreen mode Exit fullscreen mode

Common Mistakes and How to Fix Them

Mistake 1: Using After Move

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

// RIGHT:
let s1 = String::from("hello");
let s2 = s1.clone(); // Explicitly clone
println!("{} {}", s1, s2); // Both work
Enter fullscreen mode Exit fullscreen mode

Mistake 2: Multiple Mutable Borrows

// WRONG:
let mut vec = vec![1, 2, 3];
let r1 = &mut vec;
let r2 = &mut vec; // ERROR!

// RIGHT:
let mut vec = vec![1, 2, 3];
{
    let r1 = &mut vec;
    r1.push(4);
} // r1 scope ends
let r2 = &mut vec; // Now OK
Enter fullscreen mode Exit fullscreen mode

Mistake 3: Returning References to Local Variables

// WRONG:
fn create_string() -> &str {
    let s = String::from("hello");
    &s // ERROR! s dies when function ends
}

// RIGHT:
fn create_string() -> String {
    String::from("hello") // Return owned value
}
Enter fullscreen mode Exit fullscreen mode

Key Takeaways for Your Talk

  1. Ownership prevents bugs - No more use-after-free or double-free
  2. Borrowing is safe sharing - Read-only or exclusive write access
  3. Lifetimes ensure safety - References can't outlive their data
  4. Data structures work with the system - They respect ownership rules
  5. Compile-time checking - Catch errors before they cause crashes

Top comments (0)