Skip to content
DevNursery.com - New Web Developer Docs
GitHub

Rust

Introduction to Rust Programming Language Rust is a modern, systems programming language that focuses on safety, concurrency, and performance. Developed by Mozilla, Rust is designed to be a low-level language like C or C++ but with memory safety guarantees and modern language features. In this section, we’ll cover the basics of Rust’s syntax to get you started.

Hello, World!

Let’s start with a classic “Hello, World!” program in Rust:

fn main() {
    println!("Hello, World!");
}

Here’s what’s happening in this code:

fn main() { ... }: This is the entry point of the program. All Rust programs start execution from the main function.

println!("Hello, World!");: This line prints “Hello, World!” to the standard output. The println! macro is used for formatted printing.

Rust uses a unique syntax, and you’ll notice some differences compared to other languages.

Variables and Data Types

In Rust, you must declare a variable’s type explicitly, or the compiler will infer it for you. Here are some common data types:

Integer types: i8, i16, i32, i64, i128, u8, u16, u32, u64, u128, and isize/usize for machine-specific sizes.

Floating-point types: f32 and f64.

Booleans: bool.

Characters: char.

Tuples: Ordered, fixed-size collections of values with different types.

Arrays: Fixed-size arrays with elements of the same type.

Slices: Dynamically-sized views into a contiguous sequence.

Strings: UTF-8 encoded text.

Variable Declaration and Assignment

Here’s how you declare and assign variables in Rust:

let name: &str = "Alice"; // Immutable variable
let mut age: u32 = 30;    // Mutable variable
let: Keyword used for variable binding.

name: &str: Variable name and type annotation.

= "Alice": Initialization.

mut: Keyword for mutable variables.

Functions

Defining functions in Rust is straightforward:

fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn main() {
    let sum = add(5, 3);
    println!("Sum: {}", sum);
}

fn add(a: i32, b: i32) -> i32 { ... }: Defines a function named add that takes two i32 parameters and returns an i32 value.

-> i32: Specifies the return type.

Control Flow

Rust offers common control flow constructs like if, else, loop, while, and for. Here’s an example of an if statement:

fn main() {
    let number = 42;

    if number < 0 {
        println!("Negative");
    } else if number == 0 {
        println!("Zero");
    } else {
        println!("Positive");
    }
}

Ownership and Borrowing

One of Rust’s unique features is its ownership system, which ensures memory safety without a garbage collector. Rust uses ownership, borrowing, and lifetimes to manage memory efficiently.

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // Moves ownership from s1 to s2
    // println!("{}", s1); // This line would cause a compilation error

    let s3 = s2.clone(); // Creates a deep copy
    println!("s2: {}, s3: {}", s2, s3);
}

Ownership: Each value in Rust has a variable that is its “owner.”

Borrowing: Multiple references (“borrowings”) to a value can exist, but only one can be mutable at a time.

Lifetimes: These annotations ensure references are valid.

Rust CLI

The Rust CLI (Command-Line Interface) is an essential tool for working with Rust programming language. In this overview, we’ll cover some of the most common Rust CLI commands for running single Rust files, creating Cargo projects, installing dependencies, running Cargo projects, and compiling Cargo projects.

Running Single Rust Files

To run a single Rust file, follow these steps:

Create a new Rust file, e.g., my_program.rs.

Open your terminal.

Use the rustc command followed by the filename to compile and run the Rust program:

rustc my_program.rs
./my_program

This will compile the my_program.rs file and produce an executable named my_program. You can run it with ./my_program.

Creating Cargo Projects

Cargo is Rust’s official package manager and build tool. It simplifies project management and dependency handling. To create a new Cargo project, use the following command:

cargo new my_project

This command creates a new directory called my_project containing the necessary project files and directory structure. You can replace my_project with your desired project name.

Installing Dependencies

To add dependencies to your Cargo project, you’ll typically edit the Cargo.toml file and specify the dependencies there. For example, to add the popular rand crate as a dependency, open the Cargo.toml file and add the following:

[dependencies]
rand = "0.8"

After adding dependencies, run the following command to download and install them:

cargo build

Cargo will fetch and build all the specified dependencies and place them in the project’s target directory.

Running Cargo Projects

To run a Cargo project, use the following command from your project’s root directory (where the Cargo.toml file is located):

cargo run

This command will build and execute your Rust project. If your project contains multiple binaries, you can specify which one to run using the -p flag followed by the target name defined in the Cargo.toml file.

Compiling Cargo Projects

If you want to build your Cargo project without running it immediately, you can use the following command:

cargo build

This command compiles your project’s source code and produces executable files in the target/debug directory. You can run the resulting executables directly from there.

For a release build with optimizations, use:

cargo build --release

This creates optimized binaries in the target/release directory.

These are some of the fundamental Rust CLI commands you’ll use when working with Rust projects. The Rust CLI, combined with Cargo, provides a robust and efficient development environment for Rust programming.

Rust Tricks

Arrays

TrickPurposeSyntax/ExampleLibraries
1. Create an arrayCreate a fixed-size arraylet arr: [i32; 5] = [1, 2, 3, 4, 5];Standard Lib
2. Initialize with a default valueInitialize array elementslet arr = [0; 10];Standard Lib
3. Access elements by indexAccess array elementslet val = arr[2];Standard Lib
4. Iterate through elementsIterate over array elementsrust for &num in &arr { println!("{}", num); } Standard Lib
5. Update array elementModify an array elementarr[1] = 42;Standard Lib
6. Get array lengthRetrieve the length of an arraylet len = arr.len();Standard Lib
7. Compare arraysCheck if two arrays are equallet equal = arr1 == arr2;Standard Lib
8. Slice an arrayCreate a subarray from an arraylet subarr = &arr[1..4];Standard Lib
9. Iterate with indicesEnumerate elements with indicesrust for (i, &num) in arr.iter().enumerate() { println!("Index {}: {}", i, num); } Standard Lib
10. Find max/minFind the maximum or minimum valuelet max = arr.iter().max();Standard Lib
11. Search for an elementCheck if an element existslet exists = arr.contains(&value);Standard Lib
12. Sort an arraySort array elementsarr.sort(); or `arr.sort_by(a, b
13. Reverse an arrayReverse array elementsarr.reverse();Standard Lib
14. Concatenate arraysMerge two arraysrust let combined = [&arr1[..], &arr2[..]].concat(); Standard Lib
15. Clone an arrayCreate a deep copy of an arraylet cloned = arr.to_vec(); or let cloned = arr.clone();Standard Lib
16. Filter elementsCreate a new array with filters```rust let filtered: Vec<_> = arr.iter().filter(&&x
17. Map elementsTransform elements using a function```rust let mapped: Vec<_> = arr.iter().map(&x
18. Zip two arraysPair elements from two arraysrust let zipped: Vec<_> = arr1.iter().zip(arr2.iter()).collect(); Standard Lib
19. Reduce elementsCompute a single value from array```rust let sum = arr.iter().fold(0,acc, &x
20. Remove duplicatesRemove duplicate elementsrust let unique: Vec<_> = arr.iter().cloned().collect(); unique.dedup(); Standard Lib
21. Chunk arraySplit array into chunks```rust let chunks: Vec<_> = arr.chunks(3).map(chunk
22. Iterate in chunksProcess array elements in chunksrust for chunk in arr.chunks(3) { println!("{:?}", chunk); } Standard Lib
23. Compare arrays lexicographicallyCompare arrays lexicographicallylet cmp = arr1.cmp(&arr2);Standard Lib
24. Shuffle arrayShuffle array elements randomlyrust use rand::seq::SliceRandom; let mut rng = rand::thread_rng(); arr.shuffle(&mut rng); rand crate
25. Binary searchSearch for an element efficientlyrust let result = arr.binary_search(&target); Standard Lib

Vectors

TrickPurposeSyntax/ExampleLibraries
1. Create an empty vectorInitialize an empty vectorlet empty_vec: Vec<i32> = Vec::new();Standard Lib
2. Create a vector with valuesInitialize a vector with valueslet vec = vec![1, 2, 3, 4, 5];Standard Lib
3. Add an element to the endAppend an element to the vectorvec.push(6);Standard Lib
4. Add multiple elementsAppend multiple elementsrust vec.extend(vec![7, 8, 9]); Standard Lib
5. Remove an element by valueRemove an element by its value```rust vec.retain(&x
6. Remove an element by indexRemove an element by indexrust vec.remove(2); Standard Lib
7. Get the first elementAccess the first elementlet first = vec.first();Standard Lib
8. Get the last elementAccess the last elementlet last = vec.last();Standard Lib
9. Get and remove the first elementExtract and remove the first elementlet first = vec.remove(0);Standard Lib
10. Get and remove the last elementExtract and remove the last elementlet last = vec.pop();Standard Lib
11. Check if the vector is emptyDetermine if the vector is emptylet is_empty = vec.is_empty();Standard Lib
12. Get vector lengthRetrieve the length of a vectorlet len = vec.len();Standard Lib
13. Clone a vectorCreate a deep copy of a vectorlet cloned = vec.clone();Standard Lib
14. Clear all elementsRemove all elements from a vectorvec.clear();Standard Lib
15. Check for an elementCheck if an element existslet exists = vec.contains(&element);Standard Lib
16. Find index of an elementFind the index of an element`let index = vec.iter().position(&x
17. Find element by indexAccess an element by indexlet element = vec.get(index);Standard Lib
18. Iterate with indicesEnumerate elements with indicesrust for (i, &num) in vec.iter().enumerate() { println!("Index {}: {}", i, num); } Standard Lib
19. Split into partsSplit a vector into smaller partsrust let (left, right) = vec.split_at(index); Standard Lib
20. Clone and extendClone and extend a vectorrust let new_vec = vec.clone(); new_vec.extend(iterable); Standard Lib
21. Sort vectorSort vector elementsvec.sort(); or `vec.sort_by(a, b
22. Reverse vectorReverse vector elementsvec.reverse();Standard Lib
23. Filter elementsCreate a new vector with filters```rust let filtered: Vec<_> = vec.iter().filter(&&x
24. Map elementsTransform elements using a function```rust let mapped: Vec<_> = vec.iter().map(&x
25. Copy from sliceCopy a slice of a vector to a new vectorrust let copied: Vec<_> = vec[1..4].to_vec(); Standard Lib

Strings

TrickPurposeSyntax/ExampleLibraries
1. Create an empty stringInitialize an empty stringlet empty_string: String = String::new();Standard Lib
2. Create a string with valueInitialize a string with valuelet my_string = String::from("Hello, Rust!");Standard Lib
3. Concatenate two stringsCombine two stringslet combined = string1 + &string2;Standard Lib
4. Append to a stringAdd more content to a stringstring.push_str(" World!");Standard Lib
5. String lengthGet the length of a stringlet length = my_string.len();Standard Lib
6. Check if a string is emptyDetermine if a string is emptylet is_empty = my_string.is_empty();Standard Lib
7. Iterate over charactersIterate over characters in a stringrust for c in my_string.chars() { println!("{}", c); } Standard Lib
8. Iterate over bytesIterate over bytes in a stringrust for b in my_string.bytes() { println!("{}", b); } Standard Lib
9. Convert to uppercaseChange the case to uppercaselet upper = my_string.to_uppercase();Standard Lib
10. Convert to lowercaseChange the case to lowercaselet lower = my_string.to_lowercase();Standard Lib
11. Check for substringCheck if a string contains a substringlet contains = my_string.contains("Rust");Standard Lib
12. Find the index of substringFind the index of a substringlet index = my_string.find("Rust");Standard Lib
13. Replace substringReplace a substring in a stringlet replaced = my_string.replace("Rust", "RUST");Standard Lib
14. Trim whitespaceRemove leading and trailing whitespaceslet trimmed = my_string.trim();Standard Lib
15. Convert to integerParse a string into an integerlet num: i32 = my_string.parse().unwrap();Standard Lib
16. String slicingGet a portion of the stringlet slice = &my_string[0..5];Standard Lib
17. String formattingFormat strings with placeholdersrust let formatted = format!("Hello, {}!", name);Standard Lib
18. Repeat a stringCreate a new string by repeatinglet repeated = "abc".repeat(3);Standard Lib
19. Convert to bytesConvert a string to a byte arraylet bytes = my_string.as_bytes();Standard Lib
20. Convert from bytesConvert a byte array to a stringlet from_bytes = String::from_utf8(bytes).unwrap();Standard Lib
21. Split a stringSplit a string into partsrust let parts: Vec<&str> = my_string.split(",").collect(); Standard Lib
22. Join stringsJoin a collection of stringslet joined = parts.join(", ");Standard Lib
23. Remove charactersRemove characters from a stringrust let removed = my_string.replace(" ", "");Standard Lib
24. String comparisonCompare two stringslet equal = string1 == string2;Standard Lib
25. String cloningCreate a deep copy of a stringlet cloned = my_string.clone();Standard Lib

Rust Pattern Matching

Pattern matching is a powerful feature in Rust that allows you to destructure and match values against patterns to control the flow of your code. It’s similar to switch or case statements in other programming languages but more flexible and expressive. Pattern matching is often used with enums, structs, tuples, and more. Let’s explore pattern matching in Rust.

Basic Syntax

In Rust, you can use the match keyword to perform pattern matching. The basic syntax of a match expression looks like this:

match value_to_match {
    pattern1 => {
        // Code to execute when pattern1 matches
    },
    pattern2 if condition => {
        // Code to execute when pattern2 matches and condition is true
    },
    _ => {
        // Code to execute for any other case
    }
}
  • value_to_match: The value you want to match against patterns.
  • pattern1, pattern2: Patterns to compare against value_to_match.
  • condition: An optional condition that can be added to patterns.

Enum Matching

One common use of pattern matching is with enums. Enums are often used to represent different states or options, and pattern matching allows you to handle each case differently.

enum TrafficLight {
    Red,
    Yellow,
    Green,
}

fn main() {
    let light = TrafficLight::Red;

    match light {
        TrafficLight::Red => println!("Stop!"),
        TrafficLight::Yellow => println!("Slow down!"),
        TrafficLight::Green => println!("Go!"),
    }
}

Destructuring Tuples and Structs

You can destructure tuples and structs in pattern matching to access their elements or fields.

fn main() {
    let point = (3, 4);

    match point {
        (0, 0) => println!("Origin"),
        (x, 0) => println!("On x-axis at {}", x),
        (0, y) => println!("On y-axis at {}", y),
        (x, y) => println!("At ({}, {})", x, y),
    }
}

Pattern Matching with Guards

You can add guards to patterns to further refine when a pattern should match.

fn main() {
    let value = 42;

    match value {
        x if x < 0 => println!("Negative"),
        x if x > 0 && x <= 100 => println!("Positive and within range"),
        _ => println!("Other"),
    }
}

Matching Option and Result

Pattern matching is often used to handle Option and Result types, which represent the presence or absence of a value and the success or failure of an operation.

fn main() {
    let maybe_value: Option<i32> = Some(42);

    match maybe_value {
        Some(value) => println!("Found value: {}", value),
        None => println!("No value"),
    }

    let result: Result<i32, &str> = Ok(42);

    match result {
        Ok(value) => println!("Success: {}", value),
        Err(error) => println!("Error: {}", error),
    }
}

Advanced Pattern Matching

Rust’s pattern matching can handle more complex scenarios, including nested patterns, ranges, and more. It’s a versatile tool for writing expressive and safe code.

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];

    for number in numbers {
        match number {
            1..=3 => println!("Small number: {}", number),
            4 | 5 => println!("Large number: {}", number),
            _ => println!("Other: {}", number),
        }
    }
}

Pattern matching is a fundamental feature of Rust that helps you write concise, readable, and safe code. It’s especially useful when dealing with complex data structures and error handling.

Option, Future & Result

In Rust, working with Option, Future, and Result types is essential for handling potentially missing values, asynchronous computations, and error-prone operations. These types are fundamental for writing safe and robust Rust code.

Option

The Option type represents either a value of type T (wrapped in Some) or the absence of a value (represented by None). It is commonly used for cases where a function might not always return a valid result.

Creating and Using Option

fn divide(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 {
        None
    } else {
        Some(a / b)
    }
}

fn main() {
    let result = divide(10.0, 2.0);

    match result {
        Some(value) => println!("Result: {}", value),
        None => println!("Cannot divide by zero"),
    }
}

Result<T, E>

The Result<T, E> type represents either a success value of type T (wrapped in Ok) or an error value of type E (wrapped in Err). It is commonly used for functions that may fail and need to provide an error explanation.

Creating and Using Result

fn divide(a: f64, b: f64) -> Result<f64, &'static str> {
    if b == 0.0 {
        Err("Division by zero")
    } else {
        Ok(a / b)
    }
}

fn main() {
    let result = divide(10.0, 0.0);

    match result {
        Ok(value) => println!("Result: {}", value),
        Err(error) => println!("Error: {}", error),
    }
}

Future

The Future type represents a value that may become available asynchronously in the future. It’s used extensively in asynchronous programming to perform non-blocking operations.

Creating and Using Future

use futures::Future;

fn main() {
    let future = async_function();

    tokio::runtime::Runtime::new()
        .unwrap()
        .block_on(async {
            let result = future.await;
            match result {
                Ok(value) => println!("Async result: {}", value),
                Err(error) => println!("Async error: {}", error),
            }
        });
}

async fn async_function() -> Result<i32, &'static str> {
    // Simulate an async operation
    tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
    Ok(42)
}

In this example, we use the futures crate and the Tokio runtime to work with async functions and await their results.

Combining Option, Result, and Future

It’s common to combine these types to handle complex scenarios in Rust programs. For example, you might have an async function that returns a Result<Option>:

use std::io;

async fn read_file() -> Result<Option<String>, io::Error> {
    // Async file reading logic
    Ok(Some("File content".to_string()))
}

In such cases, you can use nested match statements to handle each layer of the type:

fn main() {
    tokio::runtime::Runtime::new()
        .unwrap()
        .block_on(async {
            let result = read_file().await;
            match result {
                Ok(Some(content)) => println!("File content: {}", content),
                Ok(None) => println!("File not found"),
                Err(error) => println!("Error: {}", error),
            }
        });
}

Working with Option, Result, and Future is crucial for writing robust and error-handling code in Rust, especially in scenarios involving asynchronous programming and dealing with potential errors and missing values.

Structs and Traits

Structs

A struct in Rust is used to create custom data types that can hold multiple values with different data types. You can think of a struct as a blueprint for creating objects that share common attributes.

Here’s a basic example of defining a struct and creating instances of it:

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let origin = Point { x: 0, y: 0 };
    let point = Point { x: 5, y: 10 };

    println!("Origin: ({}, {})", origin.x, origin.y);
    println!("Point: ({}, {})", point.x, point.y);
}

In this example, we define a Point struct with two integer fields, x and y. We then create instances of the struct and access their fields using dot notation.

Traits

A trait in Rust is similar to an interface in other programming languages. It defines a set of methods that can be implemented by types (structs or enums) to provide specific behavior. Traits allow for code reuse and defining generic functionality.

Here’s an example of defining a trait and implementing it for a struct:

trait Drawable {
    fn draw(&self);
}

struct Circle {
    radius: f64,
}

impl Drawable for Circle {
    fn draw(&self) {
        println!("Drawing a circle with radius {}", self.radius);
    }
}

fn main() {
    let circle = Circle { radius: 5.0 };
    circle.draw();
}

In this example, we define a Drawable trait with a draw method. We then implement the Drawable trait for the Circle struct, providing a specific implementation for the draw method.

Implementing Methods on Structs

You can implement methods directly on structs without using traits. These are called “associated methods” because they are associated with the struct itself, rather than an instance of the struct.

Here’s an example of implementing methods on a struct:

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn is_square(&self) -> bool {
        self.width == self.height
    }
}

fn main() {
    let rect1 = Rectangle { width: 5, height: 10 };
    let rect2 = Rectangle { width: 7, height: 7 };

    println!("Rectangle 1: Area={}, Is square={}", rect1.area(), rect1.is_square());
    println!("Rectangle 2: Area={}, Is square={}", rect2.area(), rect2.is_square());
}

In this example, we define the Rectangle struct and implement two methods, area and is_square, directly on the struct itself. We can then call these methods on instances of the struct.

Implementing Traits for Structs

You can also implement traits for structs to provide behavior defined by the trait. This is useful for defining shared functionality across multiple types.

Here’s an example of implementing a trait for a struct:

trait Printable {
    fn print(&self);
}

struct Book {
    title: String,
    author: String,
}

impl Printable for Book {
    fn print(&self) {
        println!("Book: {} by {}", self.title, self.author);
    }
}

fn main() {
    let book = Book {
        title: "The Rust Programming Language".to_string(),
        author: "Steve Klabnik and Carol Nichols".to_string(),
    };

    book.print();
}

In this example, we define a Printable trait with a print method and implement it for the Book struct. We can then call the print method on a Book instance.

Structs and traits are powerful tools in Rust for creating custom data types and defining behavior for those types. They enable you to write clean, reusable, and organized code.