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!
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
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
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!
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
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
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
}
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!
}
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"
}
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);
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!
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
}
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?"
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
}
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
}
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]
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
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
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());
}
}
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
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
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
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
}
Key Takeaways for Your Talk
- Ownership prevents bugs - No more use-after-free or double-free
- Borrowing is safe sharing - Read-only or exclusive write access
- Lifetimes ensure safety - References can't outlive their data
- Data structures work with the system - They respect ownership rules
- Compile-time checking - Catch errors before they cause crashes
Top comments (0)