Ch 3. Common programming concepts
Ch 3.1 Variables and Immutability
-
Default: Variables in Rust are immutable by default.
-
Immutability Example (does not compile):
fn main() { let x = 5; println!("The value of x is: {x}"); x = 6; // Error: cannot assign twice to an immutable variable println!("The value of x is: {x}"); }
Mutability
-
Why Immutability Matters:
- Immutability helps avoid bugs caused by unexpected changes to values, making programs safer and easier to reason through.
- Rust enforces immutability at compile time, preventing runtime errors from value changes.
-
Making Variables Mutable: Use
mutto allow changes to a variable’s value.fn main() { let mut x = 5; println!("The value of x is: {x}"); x = 6; // Now allowed with `mut` println!("The value of x is: {x}"); } -
Use Case: Mutability is useful when a variable’s value needs to change throughout the program, and using
mutconveys this intent to others reading the code.
Constants
-
Naming Convention: Constants use uppercase with underscores.
-
Declaration: Use
constfor constants, which are always immutable and require type annotations.const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3; -
Key Differences:
- Constants cannot be made mutable and must be assigned a value that can be evaluated at compile time.
- They can be declared in any scope (including global) and are valid for the entire program duration.
Shadowing
-
Shadowing Variables: Reuse a variable name by redeclaring it with
let, allowing transformations or changes in type.fn main() { let x = 5; let x = x + 1; // Shadowing the first `x` { let x = x * 2; // Shadowing within an inner scope println!("The value of x in the inner scope is: {x}"); // 12 } println!("The value of x is: {x}"); // 6 } -
Differences from
mut:- Shadowing allows changing both the value and type of a variable, whereas
mutonly allows changing the value without changing the type. - Example of type change with shadowing:
let spaces = " "; let spaces = spaces.len(); // spaces is now an integer (usize)- Shadowing prevents accidental reassignment errors, enforcing immutability after transformations.
- Shadowing allows changing both the value and type of a variable, whereas
-
Example Error when using
mutto change types:let mut spaces = " "; spaces = spaces.len(); // Error: expected `&str`, found `usize`
3.2 Data Types
Rust is a statically typed language, meaning all types must be known at compile time. Rust often infers types, but sometimes you need to specify them explicitly, like when parsing strings into numbers:
let guess: u32 = "42".parse().expect("Not a number!");Scalar Types
A scalar type represents a single value. Rust has four primary scalar types:
-
Integer Types:
- Signed (
i) and Unsigned (u) types of various sizes (8-bit to 128-bit, andisize/usizefor architecture-dependent sizes). - Default integer type:
i32. - Integer Overflow: In debug mode, Rust panics on overflow. In release mode, Rust uses two’s complement wrapping.
- Integer literals can be written in decimal, hexadecimal, octal, binary, and byte notation.
Example:
let x: u8 = 255; // Unsigned 8-bit integer - Signed (
-
Floating-Point Types:
- Types:
f32(single precision) andf64(double precision, default). - Example:
let x = 2.0; // f64 let y: f32 = 3.0; // f32 - Types:
-
Boolean Type (
bool):- Two possible values:
trueandfalse. - Example:
let t = true; let f: bool = false; - Two possible values:
-
Character Type (
char):- Represents a Unicode scalar value (4 bytes).
- Can store more than just ASCII characters, including emojis.
- Example:
let c = 'z'; let z: char = 'ℤ'; let heart = '❤️';
Compound Types
Compound types can group multiple values into one type. Rust has two primitive compound types:
-
Tuples:
- Group multiple values with different types into one compound type.
- Tuples have a fixed length and can be destructured.
Example:
let tup: (i32, f64, u8) = (500, 6.4, 1); let (x, y, z) = tup; // Destructuring println!("y: {y}"); // Accessing elements- Access individual elements by index:
let x = tup.0; let y = tup.1; -
Arrays:
- Fixed-size collection of values of the same type.
- Arrays are stored on the stack, unlike vectors, which can change in size and are stored on the heap.
Example:
let a = [1, 2, 3, 4, 5]; let months = ["Jan", "Feb", "Mar"];- Array initialization with the same value:
let a = [3; 5]; // Equivalent to [3, 3, 3, 3, 3]- Access elements via index:
let first = a[0];
Handling Invalid Array Indexes
- If you access an invalid array index, Rust will panic at runtime, protecting against unsafe memory access.
Example of invalid index causing a panic:
let a = [1, 2, 3, 4, 5];
let element = a[10]; // Panic: index out of bounds3.3 Functions
-
Function Declaration: Use the
fnkeyword, followed by the function name (snake case), and parentheses().fn main() { println!("Hello, world!"); another_function(); } fn another_function() { println!("Another function."); } -
Calling Functions: Call a function by its name followed by parentheses. The order of function definition does not matter, as long as the function is in scope.
Parameters
-
Functions can take parameters (input values) defined in the function signature. You must declare the type of each parameter.
fn another_function(x: i32) { println!("The value of x is: {x}"); } fn main() { another_function(5); // Passing 5 as an argument }-
Multiple Parameters: Separate parameters with commas.
fn print_labeled_measurement(value: i32, unit_label: char) { println!("The measurement is: {value}{unit_label}"); }
-
Statements and Expressions
-
Statements: Perform actions but do not return values (e.g., variable declarations).
fn main() { let y = 6; // Statement }-
You cannot assign a statement to a variable because it doesn’t return a value.
let x = (let y = 6); // Error: expected expression, found statement
-
-
Expressions: Evaluate to a value. Most operations in Rust (e.g., arithmetic, function calls, blocks) are expressions.
fn main() { let y = { let x = 3; x + 1 // Expression: returns 4 }; println!("The value of y is: {y}"); // Prints "The value of y is: 4" }- Semicolon in Expressions: Adding a semicolon turns an expression into a statement, which will not return a value.
Function Return Values
-
Return Values: Functions can return values using an arrow (
->) to specify the return type.fn five() -> i32 { 5 // Return value (no semicolon) } fn main() { let x = five(); println!("The value of x is: {x}"); // Prints "The value of x is: 5" } -
Implicit Return: The last expression in a function is implicitly returned if it does not end with a semicolon. For example:
fn plus_one(x: i32) -> i32 { x + 1 // No semicolon, so this value is returned } -
Explicit Return: You can also use the
returnkeyword to return early.fn early_return(x: i32) -> i32 { if x > 5 { return x; // Return early } x + 1 // Otherwise, return x + 1 }
Common Errors
-
Mismatched Return Types: A semicolon at the end of an expression turns it into a statement, leading to errors when a value is expected:
fn plus_one(x: i32) -> i32 { x + 1; // Error: this is now a statement, so it returns `()`, not `i32` }- Fix by removing the semicolon:
fn plus_one(x: i32) -> i32 { x + 1 // Now returns the value }
3.4 Comments
Single-Line Comments
-
Use two slashes (
//) to write comments. Everything after//is ignored by the compiler.// This is a comment
Multi-Line Comments
-
For longer comments, use
//on each line:// This is a longer comment that // spans multiple lines.
Inline Comments
-
You can also place comments at the end of code lines:
let lucky_number = 7; // I’m feeling lucky today
Idiomatic Comment Placement
-
Typically, comments are placed above the code they annotate for better readability:
// I’m feeling lucky today let lucky_number = 7;
3.5 Control flow
if Expressions
let number = 6;
if number % 4 == 0 {
println!("divisible by 4");
} else if number % 3 == 0 {
println!("divisible by 3");
} else if number % 2 == 0 {
println!("divisible by 2");
} else {
println!("not divisible by 4, 3, or 2");
}-
Using
ifinletStatements: You can assign the result of anifexpression to a variable.let condition = true; let number = if condition { 5 } else { 6 }; println!("The value of number is: {number}"); -
Type Consistency in
if: The result of both theifandelsearms must have the same type.let number = if condition { 5 } else { "six" }; // Error: mismatched types
loop (Infinite Loop)
-
Repeats a block of code forever unless explicitly stopped with
break.loop { println!("again!"); } -
Breaking from a
loop: Usebreakto exit a loop.let result = loop { counter += 1; if counter == 10 { break counter * 2; // return value } }; // result is 20, e.g. the returned value of counter * 2 println!("The result is {result}");- You might also need to pass the result of that operation out of the loop to the rest of your code. To do this, you can add the value you want returned after the break expression you use to stop the loop; that value will be returned out of the loop so you can use i
-
Loop Labels: Used to specify which loop to break from in nested loops.
'outer: loop { loop { break 'outer; // Breaks the outer loop } }
while Loops
-
Executes code while a condition is true.
let mut number = 3; while number != 0 { println!("{number}!"); number -= 1; } println!("LIFTOFF!!!");
for Loops
-
Used to iterate over a collection or a range.
let a = [10, 20, 30, 40, 50]; for element in a { println!("the value is: {element}"); } -
Iterating over a Range: You can use ranges to loop through numbers.
for number in (1..4).rev() { println!("{number}!"); } println!("LIFTOFF!!!");