1. Installation & Setup
What Is Installation & Setup?
Setting up Rust involves installing the Rust compiler, package manager, and tools to start developing
Rust applications. This ensures you have the necessary environment to write, build, and run Rust
programs.
Installing Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
This command installs Rust using the official Rustup installer, which manages Rust versions and tools.
Verifying Installation
rustc --version
cargo --version
These commands verify that the Rust compiler (rustc
) and package manager
(cargo
) are installed correctly.
Ways of Implementation
- Creating a New Project: Use
cargo new
to scaffold a new Rust project.
cargo new project_name
cd project_name
Building & Running: Use cargo build
and cargo run
to
compile and execute your Rust code.
# Build only
cargo build
# Build with optimizations for release
cargo build --release
# Build and run
cargo run
# Check for errors without building
cargo check
2. Language Basics
What Are Language Basics?
Language basics in Rust introduce fundamental concepts like defining the entry point, working with
variables, and adding comments. These are essential for writing any Rust program.
Main Function
fn main() {
println!("Hello, world!");
}
The main
function is the entry point of every Rust program.
Variables & Mutability
// Immutable by default
let x = 5;
// x = 6; // Error!
// Mutable variable
let mut y = 5;
y = 6; // Works!
// Constants
const MAX_POINTS: u32 = 100_000;
// Shadowing
let z = 5;
let z = z + 1; // Creates a new variable, shadows previous z
Rust variables are immutable by default. Use mut
for mutability and const
for
constants.
Comments
// Line comment
/* Block comment
Can span multiple lines */
/// Documentation comment (generates docs)
/// # Examples
/// ```
/// let five = 5;
/// ```
Comments in Rust include line comments, block comments, and documentation comments for generating API
docs.
3. Data Types
Scalar Types
Type |
Examples |
Description |
Integer |
i8, i16, i32, i64, i128, isize u8, u16, u32, u64, u128, usize |
Signed and unsigned integers of various sizes |
Floating-point |
f32, f64 |
IEEE-754 floating point numbers |
Boolean |
bool |
true or false |
Character |
char |
Unicode scalar value (4 bytes) |
Integer Examples
let a = 98_222; // Decimal
let b = 0xff; // Hex
let c = 0o77; // Octal
let d = 0b1111_0000; // Binary
let e = b'A'; // Byte (u8 only)
Compound Types
Tuples
let tup: (i32, f64, char) = (500, 6.4, 'A');
// Destructuring
let (x, y, z) = tup;
println!("y is: {}", y);
// Accessing by index
let five_hundred = tup.0;
let six_point_four = tup.1;
Arrays
// Fixed-length array
let arr: [i32; 5] = [1, 2, 3, 4, 5];
// Accessing elements
let first = arr[0];
// Array with same value repeated
let zeros = [0; 10]; // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
4. String Handling
String Types
Type |
Description |
&str |
String slice, immutable reference to UTF-8 encoded string data |
String |
Owned string type, growable, mutable, heap-allocated |
String Literals (&str)
let s = "Hello, world!"; // s has type &str
String Type
// Creating a new String
let s = String::from("Hello");
let s = "Hello".to_string();
// Appending
let mut s = String::from("Hello");
s.push_str(", world!"); // s is now "Hello, world!"
s.push('!'); // Add a single character
Converting Between Types
// &str to String
let s1: &str = "hello";
let s2: String = s1.to_string();
let s3: String = s1.to_owned();
let s4: String = String::from(s1);
// String to &str
let s1: String = String::from("hello");
let s2: &str = &s1;
let s3: &str = s1.as_str();
String Operations
// Concatenation
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // Note: s1 is moved here
// Format macro
let s1 = String::from("Hello");
let s2 = String::from("world");
let s3 = format!("{}, {}!", s1, s2); // s1 and s2 are still valid
// Slicing
let hello = "Hello, world!";
let s = &hello[0..5]; // s = "Hello"
5. Ownership & Borrowing
What Is Ownership & Borrowing?
Ownership and borrowing are core concepts in Rust that ensure memory safety without a garbage collector.
Ownership defines how memory is managed, while borrowing allows temporary access to data without
transferring ownership.
Ownership Rules
- Each value in Rust has an owner
- There can only be one owner at a time
- When the owner goes out of scope, the value is dropped
Move Semantics
let s1 = String::from("hello");
let s2 = s1; // s1 is moved to s2 and is no longer valid
// println!("{}", s1); // Error!
// Clone for deep copy
let s1 = String::from("hello");
let s2 = s1.clone(); // s1 is still valid
Primitive types implement the Copy
trait and are copied rather than moved:
let x = 5; let y = x; // Both x and y are valid
Functions and Ownership
fn main() {
let s = String::from("hello");
takes_ownership(s); // s is moved and is no longer valid here
let x = 5;
makes_copy(x); // x is copied, still valid here
}
fn takes_ownership(s: String) {
println!("{}", s);
} // s goes out of scope and is dropped
fn makes_copy(x: i32) {
println!("{}", x);
} // x goes out of scope, nothing special happens
Borrowing with References
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // passing a reference
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope, but it doesn't own the value, so nothing happens
Mutable References
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}", s); // prints "hello, world"
}
fn change(s: &mut String) {
s.push_str(", world");
}
Reference Rules
- You can have either:
- One mutable reference
- Any number of immutable references
- References must always be valid (no dangling references)
let mut s = String::from("hello");
// This works
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// r1 and r2 are no longer used after this point
// This also works
let r3 = &mut s;
println!("{}", r3);
// This would ERROR - can't have immutable and mutable refs at same time
// let r1 = &s;
// let r3 = &mut s;
// println!("{} and {}", r1, r3);
6. Lifetimes
What Are Lifetimes?
Lifetimes in Rust ensure that references are valid for as long as they are used, preventing dangling
references and ensuring memory safety.
Lifetime Annotations
// Lifetime parameter 'a
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
Lifetime annotations explicitly specify the scope of references to ensure they are valid.
Lifetimes in Structs
// Struct with reference that needs a lifetime
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let i = ImportantExcerpt {
part: first_sentence,
};
}
Structs with references require lifetime annotations to ensure the references remain valid.
Ways of Implementation
- Lifetime Elision Rules: Rust infers lifetimes in simple cases, reducing the need
for explicit annotations.
// Example of elision rules
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
Static Lifetime: Indicates that a reference can live for the entire program.
// 'static means the reference can live for the entire program
let s: &'static str = "I have a static lifetime.";
7. Control Flow
What Is Control Flow?
Control flow structures in Rust allow decision-making and iteration, enabling programs to execute
different code paths based on conditions.
if Expressions
// Basic if
let number = 6;
if number % 4 == 0 {
println!("number is divisible by 4");
} else if number % 3 == 0 {
println!("number is divisible by 3");
} else if number % 2 == 0 {
println!("number is divisible by 2");
} else {
println!("number is not divisible by 4, 3, or 2");
}
// if in a let statement
let condition = true;
let number = if condition { 5 } else { 6 };
Ways of Implementation
- Loops: Rust provides
loop
, while
, and for
loops for iteration.
// Infinite loop with break
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // Return value from loop
}
};
println!("The result is {}", result);
Pattern Matching: Use match
for exhaustive pattern matching.
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(), // Default case
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}
8. Functions & Closures
What Are Functions & Closures?
Functions and closures are reusable blocks of code in Rust. Functions are named and can take parameters,
while closures are anonymous functions that can capture variables from their environment.
Function Definition
// Basic function
fn greet(name: &str) {
println!("Hello, {}!", name);
}
// Function with return value
fn add(a: i32, b: i32) -> i32 {
a + b // No semicolon means this is an expression that returns a value
}
// Early return with explicit return statement
fn abs(x: i32) -> i32 {
if x >= 0 {
return x;
}
-x
}
Closures
// Basic closure syntax
let add_one = |x| x + 1;
let sum = add_one(5); // 6
// Closure with type annotations
let add_two = |x: i32| -> i32 { x + 2 };
// Closure that captures environment
let x = 4;
let equal_to_x = |z| z == x;
let result = equal_to_x(4); // true
Ways of Implementation
- Function Pointers: Functions can be assigned to variables and passed as arguments.
fn add(a: i32, b: i32) -> i32 {
a + b
}
// Function pointer
let f: fn(i32, i32) -> i32 = add;
let result = f(1, 2); // 3
Higher-order Functions: Functions that take other functions or closures as
arguments.
let numbers = vec![1, 2, 3, 4, 5];
// Using a closure with map
let plus_one: Vec = numbers.iter()
.map(|x| x + 1)
.collect();
// [2, 3, 4, 5, 6]
// Using a named function with map
fn double(x: &i32) -> i32 {
x * 2
}
let doubled: Vec = numbers.iter()
.map(double)
.collect();
// [2, 4, 6, 8, 10]
9. Structs
Understanding Structs in Rust
Structs (structures) are custom data types that let you package and name multiple related values that make up a meaningful group. They are similar to objects in other languages but designed with Rust's ownership rules in mind.
Types of Structs
Rust provides three types of structures:
// 1. Classic Struct with named fields
struct Employee {
name: String,
position: String,
salary: u32,
active: bool,
}
// 2. Tuple Struct (unnamed fields)
struct Color(u8, u8, u8); // RGB color
// 3. Unit Struct (no fields)
struct UnitStruct; // Used for trait implementations
Creating and Using Structs
Structs can be instantiated and modified in several ways:
// Creating instances
let mut employee = Employee {
name: String::from("John Doe"),
position: String::from("Developer"),
salary: 50000,
active: true,
};
// Field shorthand when variables have same names as fields
let name = String::from("Jane Doe");
let position = String::from("Manager");
let employee2 = Employee {
name, // same as name: name
position, // same as position: position
salary: 60000,
active: true,
};
// Updating based on another instance
let employee3 = Employee {
salary: 55000,
..employee2 // copy other fields from employee2
};
Methods and Associated Functions
Structs can have methods (functions associated with the type):
struct Circle {
radius: f64,
center: Point,
}
impl Circle {
// Associated function (constructor)
fn new(x: f64, y: f64, radius: f64) -> Circle {
Circle {
center: Point { x, y },
radius,
}
}
// Method that calculates area
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
// Method that modifies the circle
fn scale(&mut self, factor: f64) {
self.radius *= factor;
}
// Method that checks if a point is inside the circle
fn contains(&self, point: &Point) -> bool {
let dx = self.center.x - point.x;
let dy = self.center.y - point.y;
dx * dx + dy * dy <= self.radius * self.radius
}
}
// Usage example
fn main() {
let mut circle = Circle::new(0.0, 0.0, 5.0);
println!("Area: {}", circle.area());
circle.scale(2.0);
println!("New area: {}", circle.area());
let point = Point { x: 3.0, y: 4.0 };
println!("Contains point: {}", circle.contains(&point));
}
Advanced Features
Structs can implement various traits for added functionality:
#[derive(Debug, Clone, PartialEq)]
struct Product {
id: u32,
name: String,
price: f64,
}
impl Default for Product {
fn default() -> Self {
Product {
id: 0,
name: String::from("Unnamed"),
price: 0.0,
}
}
}
impl std::fmt::Display for Product {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}: {} (${:.2})", self.id, self.name, self.price)
}
}
// Usage with derived and implemented traits
fn main() {
let product1 = Product {
id: 1,
name: String::from("Widget"),
price: 19.99,
};
// Using Debug trait
println!("Debug: {:?}", product1);
// Using Display trait
println!("Display: {}", product1);
// Using Default trait
let product2 = Product::default();
// Using PartialEq trait
if product1 != product2 {
println!("Products are different!");
}
}
Best Practices
- Use meaningful names for structs and their fields
- Implement relevant traits for your use case
- Consider privacy (pub/private) for fields and methods
- Use documentation comments to explain complex structs
- Prefer methods over external functions when working with struct data
10. Enums & Pattern Matching
What Are Enums & Pattern Matching?
Enums (enumerations) are a powerful feature in Rust that allow you to define a type by listing all of its possible variants. They are particularly useful when you have a value that could be one of several predefined possibilities. Pattern matching provides a mechanism to handle these variants in a clear and exhaustive way.
Basic Enums
At their simplest, enums are a way to represent a fixed set of options:
// Define an enum for compass directions
enum Direction {
North,
South,
East,
West,
}
// Using the enum
let player_direction = Direction::North;
// Using with match
match player_direction {
Direction::North => println!("Moving up"),
Direction::South => println!("Moving down"),
Direction::East => println!("Moving right"),
Direction::West => println!("Moving left"),
}
Enums with Data
Enums can hold data within their variants, making them more powerful:
// Enum variants can hold different types of data
enum GameEvent {
Score(u32), // Tuple variant
Move { x: i32, y: i32 }, // Struct variant
TeamName(String), // Single value variant
Quit, // Unit variant
}
// Using enum with data
let events = vec![
GameEvent::Score(10),
GameEvent::Move { x: 2, y: -1 },
GameEvent::TeamName(String::from("Blue Team")),
GameEvent::Quit,
];
// Pattern matching with data extraction
for event in events {
match event {
GameEvent::Score(points) => println!("Scored {} points!", points),
GameEvent::Move { x, y } => println!("Moved to ({}, {})", x, y),
GameEvent::TeamName(name) => println!("Team name: {}", name),
GameEvent::Quit => println!("Game Over!"),
}
}
Option and Result Enums
Rust's standard library includes two very important enums for handling optional values and errors:
// Option enum for optional values
fn divide(numerator: f64, denominator: f64) -> Option {
if denominator == 0.0 {
None
} else {
Some(numerator / denominator)
}
}
// Result enum for error handling
#[derive(Debug)]
enum MathError {
DivisionByZero,
NegativeSquareRoot,
}
fn safe_operations(x: f64, y: f64) -> Result {
if y == 0.0 {
return Err(MathError::DivisionByZero);
}
let result = x / y;
if result < 0.0 {
return Err(MathError::NegativeSquareRoot);
}
Ok(result.sqrt())
}
Advanced Pattern Matching
Pattern matching in Rust is powerful and supports multiple patterns and guards:
enum Temperature {
Celsius(f64),
Fahrenheit(f64),
}
let temp = Temperature::Celsius(23.0);
match temp {
Temperature::Celsius(c) if c > 30.0 => println!("Too hot!"),
Temperature::Celsius(c) if c < 10.0 => println!("Too cold!"),
Temperature::Celsius(c) => println!("Nice temperature: {}°C", c),
Temperature::Fahrenheit(f) if f > 86.0 => println!("Too hot!"),
Temperature::Fahrenheit(f) if f < 50.0 => println!("Too cold!"),
Temperature::Fahrenheit(f) => println!("Nice temperature: {}°F", f),
}
// Multiple patterns using |
let number = 7;
match number {
1 | 2 | 3 => println!("Low number"),
4 | 5 | 6 => println!("Medium number"),
_ => println!("High number"),
}
Best Practices
- Use enums when you have a fixed set of possible values
- Leverage pattern matching for exhaustive handling of variants
- Consider using
Option
instead of null values
- Use
Result
for error handling
- Implement methods on enums when they represent domain logic
11. Error Handling
What Is Error Handling?
Error handling in Rust ensures robust and predictable programs. It is a core feature of the language,
designed to help developers write safe and reliable code by addressing potential issues at compile time
or runtime.
Result Enum
// Result is defined in the standard library:
// enum Result {
// Ok(T),
// Err(E),
// }
fn divide(numerator: f64, denominator: f64) -> Result {
if denominator == 0.0 {
Err(String::from("Cannot divide by zero"))
} else {
Ok(numerator / denominator)
}
}
Handling Results with match
let result = divide(10.0, 2.0);
match result {
Ok(value) => println!("The result is {}", value),
Err(error) => println!("Error: {}", error),
}
Propagating Errors
fn read_username_from_file() -> Result {
let f = File::open("username.txt");
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
The ? Operator
// Same function using ? operator
fn read_username_from_file() -> Result {
let mut f = File::open("username.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
// Even shorter using method chaining
fn read_username_from_file() -> Result {
let mut s = String::new();
File::open("username.txt")?.read_to_string(&mut s)?;
Ok(s)
}
panic! Macro
// Causes program to crash with message
panic!("crash and burn");
// Unwrap will panic if Result is Err
let f = File::open("test.txt").unwrap();
// Expect provides a custom panic message
let f = File::open("test.txt")
.expect("Failed to open test.txt");
12. Common Collections
What Are Common Collections?
Common collections in Rust are data structures that allow efficient storage and manipulation of groups of
data. They include:
Vec
for dynamic arrays
HashMap
for key-value pairs
Vec<T>
// Creating a vector
let v: Vec = Vec::new();
let v = vec![1, 2, 3];
// Adding elements
let mut v = Vec::new();
v.push(5);
v.push(6);
// Reading elements
let third: &i32 = &v[2];
match v.get(2) {
Some(third) => println!("Third element is {}", third),
None => println!("No third element"),
}
// Iterating
for i in &v {
println!("{}", i);
}
// Mutably iterating
for i in &mut v {
*i += 50;
}
HashMap<K, V>
use std::collections::HashMap;
// Creating a new hash map
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
// Accessing values
let team_name = String::from("Blue");
let score = scores.get(&team_name);
// Updating values
scores.insert(String::from("Blue"), 25); // Overwrites
scores.entry(String::from("Red")).or_insert(50); // Only inserts if key has no value
// Updating based on old value
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}
13. Testing
What Is Testing?
Testing in Rust ensures code correctness and prevents regressions. It is a built-in feature that supports
unit and integration tests.
Unit Tests
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
#[test]
fn larger_can_hold_smaller() {
let larger = Rectangle { width: 8, height: 7 };
let smaller = Rectangle { width: 5, height: 1 };
assert!(larger.can_hold(&smaller));
}
#[test]
#[should_panic(expected = "Guess value must be less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
}
Integration Tests
// in tests/integration_test.rs
use my_crate;
#[test]
fn it_adds_two() {
assert_eq!(4, my_crate::add_two(2));
}
Running Tests
# Run all tests
cargo test
# Run specific test
cargo test test_name
# Run tests in parallel
cargo test -- --test-threads=1
# Show output even for passing tests
cargo test -- --nocapture
14. Modules & Packages
Understanding Modules & Packages in Rust
Modules and packages are Rust's building blocks for creating organized, reusable code structures. They provide a way to split code into logical units and manage visibility between components.
Module System Basics
Modules can be defined in separate files or in the same file. Here's how they work:
1. File-based Modules
// File: src/math.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub fn multiply(a: i32, b: i32) -> i32 {
a * b
}
pub mod advanced {
pub fn power(base: i32, exp: i32) -> i32 {
if exp == 0 { return 1; }
base * self::power(base, exp - 1)
}
}
// File: src/main.rs
mod math; // Declare the module first
fn main() {
// Using specific functions
use math::{add, multiply};
let sum = add(4, 5);
let product = multiply(4, 5);
// Or using the full path
let power = math::advanced::power(2, 3);
}
2. Module Tree Organization
// File: src/lib.rs
pub mod models; // tells Rust to look for src/models.rs or src/models/mod.rs
pub mod services; // tells Rust to look for src/services.rs or src/services/mod.rs
pub mod utils; // tells Rust to look for src/utils.rs or src/utils/mod.rs
// File: src/models/mod.rs
pub mod user; // tells Rust to look for src/models/user.rs
pub mod product; // tells Rust to look for src/models/product.rs
// File: src/models/user.rs
pub struct User {
pub id: u32,
pub username: String,
}
// File: src/main.rs
use crate::models::user::User; // when in the same crate
// or
use my_crate::models::user::User; // when using as external crate
Module Visibility and Access
// File: src/auth.rs
pub struct Credentials {
pub username: String,
password: String, // private by default
}
impl Credentials {
pub fn new(username: String, password: String) -> Self {
Self { username, password }
}
pub(crate) fn verify(&self) -> bool { // visible only within the current crate
self.password.len() >= 8
}
}
// File: src/main.rs
mod auth;
use auth::Credentials;
fn main() {
let creds = Credentials::new(
String::from("user"),
String::from("password123")
);
// Can access public fields
println!("Username: {}", creds.username);
// Can't access private fields
// println!("Password: {}", creds.password); // Error!
// Can call public methods
if creds.verify() {
println!("Valid credentials!");
}
}
Package Structure
A typical Rust project structure:
my_project/
├── Cargo.toml # Package manifest
├── src/
│ ├── main.rs # Binary crate root
│ ├── lib.rs # Library crate root (optional)
│ ├── models/ # Module directory
│ │ ├── mod.rs # Module entry point
│ │ ├── user.rs # User model module
│ │ └── product.rs # Product model module
│ └── utils/ # Another module directory
│ ├── mod.rs # Module entry point
│ └── helpers.rs # Helper functions module
├── tests/ # Integration tests
└── examples/ # Example code
Using External Crates
// In Cargo.toml
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.0", features = ["full"] }
// In src/main.rs or src/lib.rs
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
pub struct Config {
name: String,
version: String,
}
// In any other file that needs Config
use crate::Config; // if in the same crate
// or
use my_crate::Config; // if using as external crate
Best Practices
- Use separate files for logically distinct modules
- Follow the module tree structure for better organization
- Keep the module hierarchy shallow and intuitive
- Use clear naming conventions (snake_case for files and modules)
- Make items public only when needed by other modules
- Document public interfaces with rustdoc comments
- Use re-exports (pub use) to create more ergonomic public interfaces
15. Asynchronous Programming
What Is Asynchronous Programming?
Asynchronous programming in Rust allows you to write non-blocking code that can handle multiple tasks
concurrently. It is built on the async
and await
keywords, enabling efficient
and scalable applications.
// Using Tokio runtime
use tokio::time::{sleep, Duration};
async fn fetch_data() -> String {
println!("Fetching data...");
sleep(Duration::from_secs(2)).await; // Simulate network delay
"Data fetched!".to_string()
}
#[tokio::main]
async fn main() {
println!("Start");
let result = fetch_data().await;
println!("{}", result);
println!("End");
}
Ways of Implementation
Asynchronous programming can be implemented using:
- Futures: Represent computations that may complete in the future.
// Example of a Future
use std::future::Future;
fn example_future() -> impl Future
Async/Await: Simplifies writing asynchronous code by making it more readable and
maintainable.
// Example of Async/Await
use tokio::time::{sleep, Duration};
async fn async_task() {
println!("Task started...");
sleep(Duration::from_secs(1)).await;
println!("Task completed!");
}
#[tokio::main]
async fn main() {
async_task().await;
}
Runtimes: Executors like Tokio
or async-std
drive
asynchronous tasks to completion.
// Example using Tokio runtime
use tokio::task;
#[tokio::main]
async fn main() {
let handle = task::spawn(async {
println!("Task running in Tokio runtime...");
});
handle.await.unwrap();
}
// Example using async-std runtime
use async_std::task;
fn main() {
task::block_on(async {
println!("Task running in async-std runtime...");
});
}
16. Synchronous Programming
What Is Synchronous Programming?
Synchronous programming in Rust involves executing tasks sequentially, where each task must complete
before the next one starts. Rust works synchronously by default, making it straightforward to write and
reason about programs without introducing concurrency.
use std::io;
fn main() {
println!("Enter your name:");
let mut name = String::new();
io::stdin().read_line(&mut name).expect("Failed to read input");
println!("Hello, {}!", name.trim());
}
17. Concurrency
What Is Concurrency?
Concurrency in Rust allows multiple tasks to run simultaneously, improving performance and
responsiveness. It is particularly useful for applications that need to handle multiple independent
tasks, such as processing user requests, handling I/O operations, or performing background computations.
// Using Channels for Message Passing
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let message = String::from("Hello from the thread!");
tx.send(message).unwrap();
});
let received = rx.recv().unwrap();
println!("Received: {}", received);
}
// Using Async/Await
use tokio::time::{sleep, Duration};
async fn fetch_data() -> String {
println!("Fetching data...");
sleep(Duration::from_secs(2)).await; // Simulate network delay
"Data fetched!".to_string()
}
#[tokio::main]
async fn main() {
println!("Start");
let result = fetch_data().await;
println!("{}", result);
println!("End");
}
18. Threading
What Is Threading?
Threading in Rust allows you to run multiple threads of execution in parallel. Rust's ownership model
ensures memory safety and prevents data races, making it easier to write safe multithreaded programs.
// Spawning Threads
use std::thread;
fn main() {
let handle = thread::spawn(|| {
for i in 1..5 {
println!("Thread: {}", i);
}
});
for i in 1..5 {
println!("Main: {}", i);
}
handle.join().unwrap(); // Wait for the thread to finish
}
// Shared State with Mutex
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
19. Traits
What Are Traits?
Traits define shared behavior across types, similar to interfaces in other languages. They enable polymorphism and code reuse in Rust.
Defining and Implementing Traits
// Define a trait
trait Summary {
fn summarize(&self) -> String;
// Default implementation
fn default_summary(&self) -> String {
String::from("(Read more...)")
}
}
// Implement trait for a type
struct NewsArticle {
headline: String,
content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}", self.headline)
}
}
Trait Bounds
// Using trait bounds
fn notify(item: &T) {
println!("Breaking news! {}", item.summarize());
}
// Multiple trait bounds
fn complex_function(item: &T) {
// ...
}
// Where clause syntax
fn some_function(t: &T, u: &U) -> i32
where T: Display + Clone,
U: Clone + Debug
{
// ...
}
Trait Objects
// Using trait objects for dynamic dispatch
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec>,
}
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
20. Macros
What Are Macros?
Macros are a way of writing code that writes other code, known as metaprogramming. Rust provides a powerful macro system that allows for code generation and reduction of repetitive code.
Declarative Macros
// Basic macro definition
macro_rules! say_hello {
() => {
println!("Hello!");
};
}
// Macro with parameters
macro_rules! create_function {
($func_name:ident) => {
fn $func_name() {
println!("You called {:?}()", stringify!($func_name));
}
};
}
Procedural Macros
use proc_macro;
// Derive macro
#[proc_macro_derive(MyTrait)]
pub fn my_trait_derive(input: TokenStream) -> TokenStream {
// Implementation
}
// Attribute macro
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
// Implementation
}
// Function-like macro
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
// Implementation
}
Common Built-in Macros
// println! macro
println!("Hello, {}!", "world");
// vec! macro
let v = vec![1, 2, 3];
// assert! macro family
assert!(true);
assert_eq!(2 + 2, 4);
assert_ne!(2 + 2, 5);