Rust is a systems programming language that emphasizes safety, concurrency, and performance. It achieves memory safety without garbage collection through its unique ownership system, while still maintaining high performance comparable to C/C++. This guide focuses on practical knowledge to get you productive quickly.
Core Philosophy
Rust's design principles make it unique among programming languages:
Memory Safety: Prevents common programming errors like null pointer dereferencing, dangling pointers, and data races. This is achieved through the compiler's strict checking rather than runtime checks.
Zero-Cost Abstractions: High-level abstractions compile to efficient machine code. You can write expressive code without performance penalties.
Fearless Concurrency: Safe concurrent programming without data races. The compiler prevents common concurrency bugs at compile time.
No Runtime: No garbage collector or runtime, making it suitable for embedded systems and performance-critical applications.
Basic Syntax and Data Types
Variables and Mutability
Rust's variable system is designed to prevent bugs and make code more predictable:
// Variables are immutable by default - this is a key safety feature// Once a value is bound to a name, it cannot be changedletx=5;// x = 6; // This would cause a compile error// To make a variable mutable, use the 'mut' keyword// This explicitly shows your intent to change the valueletmuty=5;y=6;// This is allowed because of 'mut'// Constants are always immutable and must have type annotation// They can be declared in any scope, including globalconstMAX_POINTS:u32=100_000;// Shadowing allows you to reuse a variable name// This creates a new variable, effectively hiding the previous one// Useful when you want to transform a value but keep it immutableletx=5;letx=x+1;// x is now 6, but still immutableletx=x*2;// x is now 12, still immutable
Basic Types
Rust is a statically typed language, which means the compiler must know the type of every variable. However, it can often infer types, so you don't always need to write them:
// Scalar Types - represent a single valueletinteger:i32=42;// Signed integer (can be negative)letunsigned:u32=42;// Unsigned integer (only positive)letfloat:f64=3.14;// Floating point (64-bit precision)letboolean:bool=true;// Boolean (true or false)letcharacter:char='A';// Character (Unicode scalar value)// Compound Types - group multiple values// Tuples: fixed-length collection of values of different typeslettuple:(i32,f64,char)=(1,2.0,'a');let(x,y,z)=tuple;// Destructuring a tuple// Arrays: fixed-length collection of values of the same type// Size must be known at compile timeletarray:[i32;5]=[1,2,3,4,5];// Type is [i32; 5]letfirst=array[0];// Accessing elements (zero-based)// Slices: reference to a portion of an array or string// They don't have ownership, just a view into the dataletslice:&[i32]=&array[1..3];// References elements 1 and 2
Ownership and Borrowing
Ownership is Rust's most unique feature and the key to its memory safety guarantees. It's a set of rules that the compiler checks at compile time.
Ownership Rules
Each value has an owner (a variable that owns it)
Only one owner at a time
When owner goes out of scope, value is dropped (memory is freed)
// Ownership transfer (move semantics)lets1=String::from("hello");// s1 owns the stringlets2=s1;// Ownership moves from s1 to s2// println!("{}", s1); // This would fail - s1 no longer owns the string// This prevents double-free errors that can occur in other languages// Clone for deep copylets1=String::from("hello");lets2=s1.clone();// Creates a complete copy of the dataprintln!("{} {}",s1,s2);// Both valid because they own different data// Note: clone can be expensive for large data// Stack-only data (like integers) implements Copy traitletx=5;lety=x;// x is still valid because integers are copied, not movedprintln!("{} {}",x,y);// Both valid
Borrowing
Borrowing allows you to access data without taking ownership. This is crucial for performance and flexibility:
// Immutable borrows (multiple allowed)lets=String::from("hello");letr1=&s;// r1 borrows sletr2=&s;// r2 also borrows sprintln!("{} {}",r1,r2);// Both references are valid// The data can't be modified through these references// Mutable borrows (only one allowed)letmuts=String::from("hello");letr1=&muts;// r1 mutably borrows s// let r2 = &mut s; // This would fail - can't have multiple mutable borrows// let r3 = &s; // This would fail - can't have immutable borrow while mutable exists// These rules prevent data races at compile time// Borrowing rules in practicefnmain(){letmuts=String::from("hello");{letr1=&s;// First borrowletr2=&s;// Second borrowprintln!("{} {}",r1,r2);}// r1 and r2 go out of scope hereletr3=&muts;// Now we can mutably borrowr3.push_str(" world");}
Pattern Matching and Control Flow
Rust's pattern matching is powerful and exhaustive, ensuring you handle all possible cases:
// Match is like a switch statement but more powerful// It must be exhaustive (cover all possible cases)letnumber=13;matchnumber{1=>println!("One!"),// Match exact value2|3|5|7|11=>println!("Prime!"),// Match multiple values13..=19=>println!("Teen!"),// Match range_=>println!("Something else"),// Catch-all pattern}// Pattern matching with destructuring// This is useful for extracting values from complex typesletpoint=(3,4);matchpoint{(0,0)=>println!("Origin"),// Match exact tuple(0,y)=>println!("Y-axis at {}",y),// Extract y value(x,0)=>println!("X-axis at {}",x),// Extract x value(x,y)=>println!("Point at ({}, {})",x,y),// Extract both values}// Match with guards (additional conditions)matchnumber{nifn<0=>println!("Negative"),nifn>0=>println!("Positive"),_=>println!("Zero"),}
If Let and While Let
These are convenient syntax for when you only care about one pattern:
// if let for single pattern matching// More concise than match when you only care about one caseletsome_value=Some(3);ifletSome(x)=some_value{println!("Got: {}",x);}else{println!("Got nothing");}// while let for pattern matching in loops// Useful for iterating until a pattern doesn't matchletmutstack=Vec::new();stack.push(1);stack.push(2);whileletSome(top)=stack.pop(){// Continues while pop returns Someprintln!("{}",top);}
Error Handling
Rust uses the type system for error handling, making it explicit and forcing you to handle errors:
// Option<T> represents a value that might not exist// This replaces null/undefined from other languagesfnfind_user(id:u32)->Option<String>{ifid==1{Some(String::from("John"))// Value exists}else{None// No value}}// Result<T, E> represents an operation that might fail// T is the success type, E is the error typefndivide(a:f64,b:f64)->Result<f64,String>{ifb==0.0{Err(String::from("Division by zero"))// Operation failed}else{Ok(a/b)// Operation succeeded}}// Using ? operator for error propagation// This is a shorthand for handling errorsfnprocess_division()->Result<f64,String>{letresult=divide(10.0,2.0)?;// Returns early if ErrOk(result*2.0)}// Common pattern for handling Option/Resultmatchfind_user(1){Some(name)=>println!("Found user: {}",name),None=>println!("User not found"),}// Using if let for cleaner code when you only care about successifletSome(name)=find_user(1){println!("Found user: {}",name);}
// Structs are custom data types that group related datastructUser{name:String,// Field with typeage:u32,// Another fieldactive:bool,// And another}// Tuple structs are useful for simple groupingsstructPoint(i32,i32);// Similar to a tuple but with a name// Enums can hold different types of data// This is more powerful than enums in other languagesenumMessage{Quit,// No dataMove{x:i32,y:i32},// Struct-likeWrite(String),// Tuple-likeChangeColor(i32,i32,i32),// Tuple-like with multiple values}// Implementing methods for typesimplUser{// Constructor (convention in Rust)fnnew(name:String,age:u32)->User{User{name,age,active:true,}}// Method that takes &self (reference to the instance)fnis_adult(&self)->bool{self.age>=18}// Method that takes &mut self (mutable reference)fndeactivate(&mutself){self.active=false;}}
Traits
Traits are similar to interfaces in other languages but more powerful:
// Defining a trait (interface)traitDrawable{fndraw(&self);// Method that implementors must definefnarea(&self)->f64;// Another required method}// Implementing a trait for a typestructCircle{radius:f64,}implDrawableforCircle{fndraw(&self){println!("Drawing circle with radius {}",self.radius);}fnarea(&self)->f64{std::f64::consts::PI*self.radius*self.radius}}// Using trait bounds to constrain generic typesfndraw_shape<T:Drawable>(shape:T){shape.draw();println!("Area: {}",shape.area());}// Default implementations in traitstraitPrintable{fnprint(&self){println!("Default implementation");}}
usestd::thread;usestd::time::Duration;usestd::sync::{Arc,Mutex,mpsc};// Basic thread creationfnbasic_threading(){// Spawn a new threadlethandle=thread::spawn(||{foriin1..=5{println!("Thread: count {}",i);thread::sleep(Duration::from_millis(100));}});// Main thread continues executingforiin1..=3{println!("Main: count {}",i);thread::sleep(Duration::from_millis(200));}// Wait for the spawned thread to finishhandle.join().unwrap();}// Sharing data between threads using Arc (Atomic Reference Counting)fnshared_state_threading(){// Arc allows multiple ownership across threadsletcounter=Arc::new(Mutex::new(0));letmuthandles=vec![];foriin0..3{// Clone the Arc to create a new referenceletcounter=Arc::clone(&counter);// Spawn a new threadlethandle=thread::spawn(move||{// Lock the mutex to access the dataletmutnum=counter.lock().unwrap();*num+=1;println!("Thread {}: counter is now {}",i,*num);});handles.push(handle);}// Wait for all threads to completeforhandleinhandles{handle.join().unwrap();}println!("Final counter value: {}",*counter.lock().unwrap());}// Message passing between threads using channelsfnmessage_passing(){// Create a channel (multiple producer, single consumer)let(tx,rx)=mpsc::channel();// Spawn a thread that sends messagesthread::spawn(move||{letmessages=vec!["Hello","from","the","thread"];formsginmessages{tx.send(msg).unwrap();thread::sleep(Duration::from_millis(100));}});// Receive messages in the main threadforreceivedinrx{println!("Got: {}",received);}}// Example of thread-safe data structuresfnthread_safe_collections(){usestd::sync::RwLock;usestd::collections::HashMap;// RwLock allows multiple readers or a single writerletmap=Arc::new(RwLock::new(HashMap::new()));letmuthandles=vec![];// Spawn writer threadsforiin0..3{letmap=Arc::clone(&map);handles.push(thread::spawn(move||{letmutmap=map.write().unwrap();map.insert(i,i*i);println!("Thread {} wrote to map",i);}));}// Spawn reader threadsforiin0..3{letmap=Arc::clone(&map);handles.push(thread::spawn(move||{letmap=map.read().unwrap();println!("Thread {} reading map: {:?}",i,*map);}));}forhandleinhandles{handle.join().unwrap();}}
Async/Await
Rust's async/await syntax provides a more efficient way to handle concurrent operations, especially I/O-bound tasks:
usestd::time::Duration;usetokio;// Popular async runtime// Basic async functionasyncfnsay_hello(){println!("Hello");// Simulate some async worktokio::time::sleep(Duration::from_secs(1)).await;println!("World");}// Async function that returns a valueasyncfnfetch_data(id:u32)->String{// Simulate network requesttokio::time::sleep(Duration::from_millis(100)).await;format!("Data for id {}",id)}// Running multiple async tasks concurrentlyasyncfnconcurrent_tasks(){// Join multiple futureslet(result1,result2)=tokio::join!(fetch_data(1),fetch_data(2));println!("Results: {}, {}",result1,result2);// Spawn multiple tasksletmuthandles=vec![];foriin0..3{lethandle=tokio::spawn(asyncmove{letresult=fetch_data(i).await;println!("Task {}: {}",i,result);});handles.push(handle);}// Wait for all tasks to completeforhandleinhandles{handle.await.unwrap();}}// Async streamsasyncfnprocess_stream(){usetokio_stream::StreamExt;usefutures::stream;// Create a stream of numbersletmutstream=stream::iter(1..=5);// Process stream itemswhileletSome(num)=stream.next().await{println!("Processing number: {}",num);tokio::time::sleep(Duration::from_millis(100)).await;}}// Error handling in async codeasyncfnasync_error_handling()->Result<String,Box<dynstd::error::Error>>{// Simulate a fallible operationletresult=tokio::time::timeout(Duration::from_secs(1),fetch_data(1)).await??;// Note the double ? for both timeout and fetch_data errorsOk(result)}// Example of async main#[tokio::main]asyncfnmain(){// Run basic async functionsay_hello().await;// Run concurrent tasksconcurrent_tasks().await;// Process streamprocess_stream().await;// Handle errorsmatchasync_error_handling().await{Ok(data)=>println!("Success: {}",data),Err(e)=>println!("Error: {}",e),}}
Choosing Between Threads and Async
Use threads when:
You have CPU-bound tasks
You need to run blocking operations
You want to utilize multiple CPU cores
You need to run code that can't be made async
Use async when:
You have I/O-bound tasks (network, file operations)
You need to handle many concurrent operations
You want to minimize resource usage
You're building a web server or other I/O-heavy application
userayon::prelude::*;// Popular parallel iterator libraryfnparallel_processing(){letnumbers=vec![1,2,3,4,5];// Process items in parallelletsum:i32=numbers.par_iter().map(|&n|n*n).sum();println!("Sum of squares: {}",sum);}
usetokio::sync::Mutex;asyncfnasync_shared_state(){letcounter=Arc::new(Mutex::new(0));letmuthandles=vec![];foriin0..3{letcounter=Arc::clone(&counter);handles.push(tokio::spawn(asyncmove{letmutlock=counter.lock().await;*lock+=1;println!("Task {}: counter is now {}",i,*lock);}));}forhandleinhandles{handle.await.unwrap();}}