😎Common Programming Concepts

Common programming concepts in Rust

Variable

Declare a variable

💡 Rust will infer the type of a variable based on the value we provide

fn main() {
    // Create a variable
    let a = 5;
    println!("a: {}", a);
}

Immutability

💡 In Rust, variables are immutable by default. If we want to make a variable mutable, we have to use mut keyword before the variable.

fn main() {
    // Mutability
    // let b = 10; => error
    let mut b = 10;
    b = 20;

    println!("b: {}", b);
}

Shadowing

💡 When the variables have the same name, and they’re within the same scope, the second variable will shadow the first variable.

fn main() {
    // Shadowing
    let c = 10;
    let c = 20;

    println!("c: {}", c);
}

The different between mutability and shadowing is that with mutability, you’re modifying a single variable, while with shadowing, you’re create two separate variables, and one of them is shadowing the other.

Scopes

💡 In Rust, variables live within a scope, which is the area between an opening and closing curly braces. The inner scope has access to the variables defined in the outer scope.

fn main() {
    // Scope
    let age = 10;

    {
        let age = 20;
        println!("age in the inner scope: {}", age);
    }

    println!("age in the outer scope: {}", age);
}

Because the outer scope can’t access the variable in the inner scope, therefore, we’ll get an error when we’re trying to access the variable in the inner scope from the outer scope.

fn main() {
    // Scope
    let outer_age = 10;

    {
        let inner_age = 20;
        println!("inner_age: {}", inner_age);
    }
    
		println!("inner_age: {}", inner_age); // Error
    println!("outer_age: {}", outer_age);
}
error[E0425]: cannot find value `inner_age` in this scope
  --> src/main.rs:36:27
   |
36 |     println!("inner: {}", inner_age); // Error
   |                           ^^^^^ not found in this scope

Data Types

💡 Every value in Rust is of a certain data type, which tells Rust what kind of data is being specified so it knows how to work with that data.

💡 Rust is a statically typed language, which means that it must know the types of all variables at compile time. In cases when many types are possible, we must add a type annotation.

CategoryData TypeDescriptionSize (on 64-bit systems)

Scalar Types

i8, i16, i32, i64, i128

Signed integers

1, 2, 4, 8, 16 bytes

u8, u16, u32, u64, u128

Unsigned integers

1, 2, 4, 8, 16 bytes

f32, f64

Floating-point numbers

4, 8 bytes

char

Character (Unicode scalar value)

4 bytes

bool

Boolean value (true or false)

1 byte

Compound Types

Array

Fixed-size collection of elements of the same type

Varies depending on element type and size

Slice

Dynamically sized view into an underlying array

Same as underlying array

Tuple

Fixed-size collection of elements of potentially different types

Varies depending on element types

Struct

User-defined composite data type with named fields

Varies depending on field types

Enum

User-defined type representing a set of variants

Varies depending on variant types

Reference Types

&T

Reference to a value of type T

8 bytes

&mut T

Mutable reference to a value of type T

8 bytes

Other Types

Option<T>

Represents an optional value of type T, can be Some(value) or None

8 bytes (includes 1 byte for discriminant)

Result<T, E>

Represents the outcome of an operation, either Ok(value) or Err(error)

16 bytes (includes 1 byte for discriminant)

Notes:

  • The size of some data types may vary depending on the target architecture (32-bit vs. 64-bit).

  • Rust has additional data types like raw pointers, functions, and closures, which are more advanced and not covered in this basic table.

Scalar Types

💡 A scalar type represents a single value. Rust has 4 scalar types: integers, floating-point numbers, Booleans, and characters.

pub fn data_types() {
    // Scalar Data Types
    // Boolean
    let is_bool: bool = true;
    let is_bool: bool = false;

    // Unsigned integers - must be positive numbers
    let u8: u8 = 8;
    let u16: u16 = 16;
    let u32: u32 = 32;
    let u64: u64 = 64;
    let u128: u128 = 128;

    // Signed integers - could be positive or negative numbers
    let i8: i8 = 8;
    let i16: i16 = 16;
    let i32: i32 = -32;
    let i64: i64 = -64;
    let i128: i128 = 128;

    // Floating point numbers - numbers with decimal places
    let f32: f32 = 3.2;
    let f64: f64 = 6.4;

    // Platform specific integers
    let usize: usize = 1;
    let isize: isize = 1;

    // Characters, &str, and String
    let char: char = 'c'; // single character represents with the char keyword
    let hello: &str = "hello"; // string slice - all string literals are string slices
    let hello2: String = String::from("hello");
}

Compound Types

💡 Compound types can group multiple values into one type. Rust has two primitive compound types: tuples and arrays.

pub fn data_types() {
    // Arrays - holds multiple values of the same type
    let numbers: [i32; 5] = [1, 2, 3, 4, 5];
    let n5 = numbers[4];
    println!("The value at index 4: {}", n5);

    // Tuples - holds multiple values with the different types
    let tuple_1 = (1, 2, 3);
    let tuple_2 = (1, 5.0, "5");

    let t1 = tuple_1.2;
    let (i1, f1, s1) = tuple_2;
    println!("The value at index 2 of tuple_1: {}", t1);
    println!("i1: {i1}, f1: {f1}, s1: {s1}")

    // Empty tuple will be called a unit type
    // It's usually returned implicitly when no other meaningful value could be returned
    // For example: functions that don't return a value implicitly return the unit type
    let unit = ();

    // Type aliasing - a new name for existing type
    type Age = u8;
    let thomas_age: Age = 25;
}

Constants

💡 In Rust, constants provide a mechanism to define fixed values during compilation. These values are immutable, meaning they cannot be changed after initialization. This immutability ensures program predictability and avoids accidental modifications.

const DAYS_IN_WEEK: u8 = 7; // Immutable, known at compile time
const USD_TO_EUR_RATE: f64 = 0.92; // Immutable, known at compile time

Key Characteristics of Constants:

  • Immutability: The defining characteristic of constants is their unchangeable nature. Once assigned a value, a constant cannot be modified throughout the program's execution.

  • Explicit Type Annotation: Unlike variables where the compiler can infer the type based on the assigned value, constants require explicit type declaration. This improves code clarity and avoids potential type mismatches.

  • Scope: Constants can be declared within various scopes, including the global scope, allowing for program-wide access if necessary. However, it's generally recommended to keep constants within the appropriate scope for better organization and encapsulation.

  • Constant Expressions: The value assigned to a constant must be a constant expression. This means the expression can be evaluated entirely at compile time, ensuring the value is known before the program runs.

  • Naming Convention: Rust adheres to the screaming snake case convention for constants. This means all letters are uppercase, and underscores separate words (e.g., MAX_VALUE).

Static Variables

💡 In Rust, global variables are called static variables. Static variables are declared using the static keyword, and its naming convention and type annotation are the same as the constants. Static variables could be declared in any scope including the global scope.

static HELLO_WORLD: &str = "Hello, world!";

fn main() {
    println!("value is: {HELLO_WORLD}");
}

💡 Static variables can only store references with the 'static lifetime, which means the Rust compiler can figure out the lifetime and we aren’t required to annotate it explicitly. Accessing an immutable static variable is safe.

Key Characteristics of Constants:

  • Immutability: Unlike the constants, static variables can be marked as mutable using mut keyword. However, accessing or modifying a mutable static variable is unsafe, therefore, those operations must be done within an unsafe block.

  • Memory management: When using constant variables, the value of the constant will be inline. The constants do not occupy a specific location in memory. Constants are allowed to duplicate their data whenever they’re used.

  • On the other hand, static variables do occupy a specific location in memory, which means there’s only one instance of the value. It means, a static variable has a fixed address in memory, therefore, using that value will always access the same data.

static mut CONVERSION_CACHE: [f64; 10] = [0.0; 10]; // Mutable, single memory location

fn update_conversion_rate(new_rate: f64) {
    unsafe { CONVERSION_CACHE[0] = new_rate; } // Mutation requires unsafe block
}

fn get_conversion_rate_for_usd(amount: f64) -> f64 {
    unsafe { amount * CONVERSION_CACHE[0] } // Access requires unsafe block
}

In general, it's best practice to use constants whenever possible. Static variables should be reserved for specific scenarios. These scenarios include:

  • Storing large amounts of data: Constants are limited in size by the type they represent. If you need to store a significant amount of data that cannot be easily divided into smaller pieces, a static variable might be necessary.

  • Needing a single, fixed memory location: Static variables have a unique memory address throughout the program's execution. This property can be useful in limited situations.

  • Using interior mutability: In rare cases, you might need a data structure that allows modification of specific parts while maintaining immutability for the overall structure. This advanced technique, known as interior mutability, often relies on static variables.

Functions

💡 Functions are defined using the fn keyword followed by the name of the function and then parentheses, which hold parameters.

Rust code uses snake case as the conventional style for function and variable names, in which all letters are lowercase and underscores separate words.

Rust functions use an arrow (>) followed by the type of value the function will return (e.g., > u32) to represent the return type of the function. If no return type is specified, the function returns the unit type (()), which essentially means it doesn't return a value.

// The greet function receives a name argument with type of string slice reference (&str) and return type is String
fn greet(name: &str) -> String { // Function to greet someone
    let greeting = format!("Hello, {}!", name); // Create greeting message
    greeting // Implicitly return the greeting string
}

fn main() {
    let user_name = "Alice";
    let message = greet(user_name); // Call the greet function
    println!("{}", message); // Print the returned greeting
}

Control Flows in Rust

💡 Control flow statements in Rust dictate the order in which your program executes code blocks. They allow you to conditionally execute code based on certain criteria or repeat code a specific number of times.

if Expressions

  • Used for simple conditional execution.

  • Checks a boolean expression.

  • If the expression is true, the code block following the if keyword is executed.

let age = 25;

if age >= 18 {
    println!("You are eligible to vote.");
}

if-else Statements

Similar to if expressions but provide an alternative code block to run if the condition is false.

let is_night = true;

if is_night {
    println!("Good night!");
} else {
    println!("Hello!");
}

else if

Allows for chaining multiple conditional checks within an if statement.

let grade = 85;

if grade >= 90 {
    println!("Excellent!");
} else if grade >= 80 {
    println!("Very good!");
} else {
    println!("Keep practicing!");
}

if let

💡 In Rust, if let is a concise way to combine conditional checks with pattern matching, specifically for handling values that match a single pattern. It offers a more readable and less verbose alternative to traditional if statements, especially when dealing with Option types or enums.

Structure:

if let <pattern> = <expression> {
    // Code to execute if the pattern matches
}
  • <pattern>: This defines the pattern you want to match against the value from the <expression>. It can be a variable name, a literal value, or a more complex structure like a tuple or struct.

  • <expression>: This is the value you want to check against the pattern.

Functionality:

  • If the pattern in <pattern> matches the value from <expression>, the code block within the curly braces is executed.

  • Any variables defined within the pattern are bound to the corresponding values from the expression, making them accessible within the code block.

  • If the pattern doesn't match, the if let block is skipped, and execution continues after it.

let result: Option<u32> = Some(42);

// Using if let for concise handling
if let Some(value) = result {
    println!("The value is: {}", value);
} else {
    println!("No value found!");
}

// Traditional approach with if statement
if result.is_some() {
    let value = result.unwrap();
    println!("The value is: {}", value);
} else {
    println!("No value found!");
}

Loops

💡 Used to repeat a block of code multiple times.

💡 while Loop: Executes a code block as long as a condition remains true.

let mut counter = 0;

while counter < 5 {
    println!("Count: {}", counter);
    counter += 1;
}

💡 loop: An infinite loop that continues until explicitly broken.

loop {
    println!("This will run forever!");
    // Add a break statement here to exit the loop
    break;
}
  • break and continue:

    • break: Exits a loop prematurely.

    • continue: Skips the current iteration of a loop and moves to the next.

💡 for Loop: Iterates over a collection or range of values.

for number in 1..5 { // Iterates from 1 (exclusive) to 5 (exclusive)
    println!("Number: {}", number);
}

Match Expressions

💡 Provide a cleaner way to handle different variants of a value compared to long if-else chains.

let weather = "sunny";

match weather {
    "sunny" => println!("Go for a walk!"),
    "rainy" => println!("Stay indoors with a good book"),
    _ => println!("Unknown weather condition"),
}

Last updated