π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.
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
, andclosures
, 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 theif
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