😎Common Programming Concepts

Common programming concepts in Rust

Variable

Declare a variable

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

Immutability

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

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

Shadowing

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

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

Category
Data Type
Description
Size (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

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

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

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

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

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

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

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

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

let is_night = true;

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

else if

let grade = 85;

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

if let

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

let mut counter = 0;

while counter < 5 {
    println!("Count: {}", counter);
    counter += 1;
}
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 number in 1..5 { // Iterates from 1 (exclusive) to 5 (exclusive)
    println!("Number: {}", number);
}

Match Expressions

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