Rust Introduction
Memory Safety Without Garbage Collection
Explanation
Why Rust?
Rust gives you C-level performance with memory safety guaranteed at compile time. No garbage collector, no runtime overhead, no null pointer exceptions. If it compiles, it works.
Key Concepts
- Ownership: Each value has one owner
- Borrowing: References without taking ownership
- Lifetimes: How long references are valid
- Zero-Cost Abstractions: High-level code, low-level performance
Rust vs JavaScript
// JavaScript - runtime errors possible
let numbers = [1, 2, 3];
let first = numbers[10]; // undefined, not an error
// Rust - compile-time safety
let numbers = vec![1, 2, 3];
// let first = numbers[10]; // Compile error!
let first = numbers.get(10); // Returns Option<&i32>
Demonstration
Example 1: Rust Basics
fn main() {
// Variables are immutable by default
let name = "Arthur";
let mut age = 30; // mut for mutable
// Type annotations (usually inferred)
let score: i32 = 100;
let pi: f64 = 3.14159;
let active: bool = true;
// Strings
let greeting = "Hello"; // &str (string slice)
let message = String::from("Hi"); // String (owned)
// String interpolation with format!
println!("Hello, {}! Age: {}", name, age);
// Arrays (fixed size)
let numbers: [i32; 5] = [1, 2, 3, 4, 5];
// Vectors (dynamic)
let mut fruits = vec!["apple", "banana"];
fruits.push("cherry");
// Tuples
let person = ("Arthur", 30, true);
let (n, a, _) = person; // Destructuring
// Option type (no null!)
let maybe_value: Option<i32> = Some(42);
let nothing: Option<i32> = None;
// Match expression
match maybe_value {
Some(v) => println!("Got: {}", v),
None => println!("Nothing"),
}
// If let (shorthand)
if let Some(v) = maybe_value {
println!("Value: {}", v);
}
// Result type (for errors)
let result: Result<i32, &str> = Ok(42);
// let error: Result<i32, &str> = Err("something went wrong");
}
Example 2: Ownership and Borrowing
fn main() {
// Ownership
let s1 = String::from("hello");
let s2 = s1; // s1 moved to s2, s1 no longer valid
// println!("{}", s1); // Error! s1 was moved
// Clone for deep copy
let s3 = String::from("hello");
let s4 = s3.clone(); // Both valid
println!("{} {}", s3, s4);
// Borrowing (references)
let s5 = String::from("hello");
let len = calculate_length(&s5); // Borrow s5
println!("{} has length {}", s5, len); // s5 still valid
// Mutable borrowing
let mut s6 = String::from("hello");
change(&mut s6);
println!("{}", s6); // "hello, world"
}
fn calculate_length(s: &String) -> usize {
s.len()
}
fn change(s: &mut String) {
s.push_str(", world");
}
// Rules:
// 1. One mutable reference OR any number of immutable references
// 2. References must always be valid
Example 3: Structs and Methods
// Struct definition
struct User {
id: u32,
name: String,
email: String,
active: bool,
}
impl User {
// Associated function (constructor)
fn new(name: String, email: String) -> Self {
Self {
id: 0,
name,
email,
active: true,
}
}
// Method (takes &self)
fn greet(&self) -> String {
format!("Hello, I'm {}!", self.name)
}
// Mutable method
fn deactivate(&mut self) {
self.active = false;
}
// Method that consumes self
fn into_name(self) -> String {
self.name
}
}
// Traits (like interfaces)
trait Greetable {
fn greet(&self) -> String;
}
impl Greetable for User {
fn greet(&self) -> String {
format!("Hi, I'm {}", self.name)
}
}
// Generic function
fn print_greeting<T: Greetable>(item: &T) {
println!("{}", item.greet());
}
fn main() {
let mut user = User::new(
String::from("Arthur"),
String::from("art@bpc.com")
);
println!("{}", user.greet());
user.deactivate();
print_greeting(&user);
}
Example 4: Error Handling
use std::fs::File;
use std::io::{self, Read};
// Custom error type
#[derive(Debug)]
enum AppError {
NotFound(String),
InvalidInput(String),
IoError(io::Error),
}
impl From<io::Error> for AppError {
fn from(err: io::Error) -> Self {
AppError::IoError(err)
}
}
// Function that can fail
fn read_username(path: &str) -> Result<String, AppError> {
let mut file = File::open(path)?; // ? propagates errors
let mut username = String::new();
file.read_to_string(&mut username)?;
if username.is_empty() {
return Err(AppError::InvalidInput("Username empty".into()));
}
Ok(username.trim().to_string())
}
// Using the function
fn main() {
match read_username("user.txt") {
Ok(name) => println!("User: {}", name),
Err(AppError::NotFound(path)) => println!("File not found: {}", path),
Err(AppError::InvalidInput(msg)) => println!("Invalid: {}", msg),
Err(AppError::IoError(e)) => println!("IO error: {}", e),
}
// Or use unwrap_or_default
let name = read_username("user.txt").unwrap_or_default();
// Or expect (panics with message)
// let name = read_username("user.txt").expect("Failed to read user");
}
Example 5: Iterators and Closures
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// Map
let doubled: Vec<i32> = numbers
.iter()
.map(|n| n * 2)
.collect();
// Filter
let evens: Vec<i32> = numbers
.iter()
.filter(|n| *n % 2 == 0)
.cloned()
.collect();
// Fold (reduce)
let sum: i32 = numbers
.iter()
.fold(0, |acc, n| acc + n);
// Chaining
let result: i32 = numbers
.iter()
.filter(|n| *n % 2 == 1) // Odd numbers
.map(|n| n * 2) // Double them
.sum(); // Sum up
// For loop
for num in &numbers {
println!("{}", num);
}
// Enumerate
for (i, num) in numbers.iter().enumerate() {
println!("{}: {}", i, num);
}
// Closures with captures
let multiplier = 3;
let multiply = |n: i32| n * multiplier;
println!("{}", multiply(5)); // 15
}
Key Takeaways:
- Variables are immutable by default
- Ownership prevents memory bugs
Optionreplaces null,Resulthandles errors- Traits are like interfaces
- Iterators are zero-cost abstractions
Imitation
Challenge 1: Create a Todo Struct
Task: Build a Todo struct with methods for completion and display.
Solution
#[derive(Debug)]
struct Todo {
id: u32,
text: String,
completed: bool,
}
impl Todo {
fn new(id: u32, text: String) -> Self {
Self {
id,
text,
completed: false,
}
}
fn toggle(&mut self) {
self.completed = !self.completed;
}
fn display(&self) -> String {
let status = if self.completed { "✓" } else { " " };
format!("[{}] {}: {}", status, self.id, self.text)
}
}
struct TodoList {
todos: Vec<Todo>,
next_id: u32,
}
impl TodoList {
fn new() -> Self {
Self { todos: vec![], next_id: 1 }
}
fn add(&mut self, text: String) -> &Todo {
let todo = Todo::new(self.next_id, text);
self.next_id += 1;
self.todos.push(todo);
self.todos.last().unwrap()
}
fn toggle(&mut self, id: u32) -> Option<&Todo> {
self.todos
.iter_mut()
.find(|t| t.id == id)
.map(|t| { t.toggle(); &*t })
}
fn list(&self) -> impl Iterator<Item = &Todo> {
self.todos.iter()
}
}
Challenge 2: Implement a Simple Stack
Task: Create a generic Stack with push, pop, and peek.
Solution
struct Stack<T> {
items: Vec<T>,
}
impl<T> Stack<T> {
fn new() -> Self {
Self { items: vec![] }
}
fn push(&mut self, item: T) {
self.items.push(item);
}
fn pop(&mut self) -> Option<T> {
self.items.pop()
}
fn peek(&self) -> Option<&T> {
self.items.last()
}
fn is_empty(&self) -> bool {
self.items.is_empty()
}
fn len(&self) -> usize {
self.items.len()
}
}
fn main() {
let mut stack: Stack<i32> = Stack::new();
stack.push(1);
stack.push(2);
stack.push(3);
assert_eq!(stack.peek(), Some(&3));
assert_eq!(stack.pop(), Some(3));
assert_eq!(stack.len(), 2);
}
Practice
Exercise 1: Temperature Converter
Difficulty: Beginner
Create functions to convert between Celsius and Fahrenheit:
- Handle invalid input with Result
- Support both directions
- Add a Temperature enum
Exercise 2: File Word Counter
Difficulty: Intermediate
Build a program that:
- Reads a file
- Counts words, lines, characters
- Uses proper error handling
- Outputs statistics
Summary
What you learned:
- Rust syntax and types
- Ownership and borrowing
- Structs and traits
- Error handling with Result
- Iterators and closures
Next Steps:
- Read: Rust Routing
- Practice: Complete Rustlings exercises
- Build: Create a CLI tool
