Basics: Variables, Types, and Operations
Variables and Mutability
Immutable by Default
In Rust, variables are immutable by default - the opposite of Python!
fn main() {
let x = 5;
println!("x = {}", x);
// x = 6; // ERROR: cannot assign twice to immutable variable
}
Python comparison:
# Python: variables are always mutable
x = 5
x = 6 # OK in Python
Mutable Variables
fn main() {
let mut x = 5;
println!("x = {}", x);
x = 6; // OK
println!("x = {}", x);
}
Constants
const MAX_POINTS: u32 = 100_000;
const PI: f64 = 3.14159265359;
fn main() {
println!("Max points: {}", MAX_POINTS);
}
Constants vs Immutable Variables:
| Feature | const | let |
|---|---|---|
| Mutability | Never | With mut |
| Type annotation | Required | Optional |
| Scope | Global or local | Local |
| Computed at | Compile time | Runtime |
| Naming | SCREAMING_SNAKE_CASE | snake_case |
Shadowing
Rust allows you to declare a new variable with the same name, which "shadows" the previous one.
fn main() {
let x = 5;
let x = x + 1; // Shadows previous x
{
let x = x * 2; // Shadows in inner scope
println!("Inner x: {}", x); // 12
}
println!("Outer x: {}", x); // 6
}
Python comparison:
# Python just reassigns the variable
x = 5
x = x + 1 # x is now 6 (reassignment, not shadowing)
# Python scoping - to demonstrate shadowing, use different scopes
x = 5
print(f"Outer x: {x}") # Prints: 5
def inner():
x = 12 # New local x shadows outer x
print(f"Inner x: {x}") # Prints: 12
inner()
print(f"Outer x still: {x}") # Prints: 5 (unchanged)
Shadowing vs Mutation:
// Shadowing allows type changes
let spaces = " ";
let spaces = spaces.len(); // OK: now it's a number
// Mutation doesn't
let mut spaces = " ";
// spaces = spaces.len(); // ERROR: type mismatch
Data Types
Rust is statically typed - all types must be known at compile time. Python is dynamically typed.
Python:
# Type is determined at runtime
x = 5 # x is int
x = "hello" # x is now str (allowed in Python)
Rust:
// Type is fixed at compile time
let x = 5; // x is i32 forever
// let x = "hello"; // ERROR: can't change type
Scalar Types
Integers
Rust has multiple integer types with explicit sizes:
| Length | Signed | Unsigned |
|---|---|---|
| 8-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| 128-bit | i128 | u128 |
| arch | isize | usize |
Python comparison:
# Python has one int type (arbitrary precision)
x = 5
y = 999999999999999999999 # Python handles this fine
fn main() {
let a: i32 = -42; // Signed 32-bit
let b: u32 = 42; // Unsigned 32-bit
let c = 98_222; // Default: i32
let d = 0xff; // Hexadecimal
let e = 0o77; // Octal
let f = 0b1111_0000; // Binary
let g = b'A'; // Byte (u8)
}
Integer Overflow:
- Debug mode: panics
- Release mode: wraps around
let x: u8 = 255;
// let y = x + 1; // Panic in debug, wraps to 0 in release
// Explicit wrapping
let y = x.wrapping_add(1); // Always wraps to 0
let z = x.checked_add(1); // Returns None if overflow
Floating-Point
fn main() {
let x = 2.0; // f64 (default)
let y: f32 = 3.0; // f32
}
f32: 32-bit, single precisionf64: 64-bit, double precision (default, generally faster)
Boolean
fn main() {
let t = true;
let f: bool = false;
if t {
println!("It's true!");
}
}
Character
fn main() {
let c = 'z';
let z: char = 'ℤ';
let emoji = '😻';
// char is 4 bytes (Unicode scalar)
println!("Size: {} bytes", std::mem::size_of::<char>()); // 4
}
Note: char is Unicode, not ASCII. It represents a Unicode scalar value.
Compound Types
Tuples
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
// Destructuring
let (x, y, z) = tup;
println!("y = {}", y);
// Direct access
println!("First: {}", tup.0);
println!("Second: {}", tup.1);
}
Unit Type: Empty tuple () represents no value.
fn main() {
let unit = ();
// Functions with no return value return ()
}
Arrays
Fixed-size, same type:
fn main() {
let arr = [1, 2, 3, 4, 5];
let first = arr[0];
// With type annotation
let arr: [i32; 5] = [1, 2, 3, 4, 5];
// Initialize with same value
let arr = [3; 5]; // [3, 3, 3, 3, 3]
// Length
println!("Length: {}", arr.len());
}
Array vs Vector:
| Feature | Array | Vector |
|---|---|---|
| Size | Fixed | Dynamic |
| Allocation | Stack | Heap |
| Type | [T; N] | Vec<T> |
Strings
Two main types in Rust (vs one in Python):
fn main() {
// String slice (immutable, fixed size)
let s1: &str = "hello";
// String (mutable, growable)
let mut s2: String = String::from("hello");
s2.push_str(", world!");
println!("{}", s1);
println!("{}", s2);
}
Python comparison:
# Python has one str type (immutable)
s1 = "hello"
s2 = "hello" + ", world!" # Creates new string
String vs &str:
| Feature | String | &str |
|---|---|---|
| Ownership | Owned | Borrowed |
| Mutability | Can grow | Fixed size |
| Allocation | Heap | Usually stack (or heap) |
Type Inference and Annotations
fn main() {
// Type inferred
let x = 5; // i32
let y = 2.0; // f64
let z = true; // bool
// Explicit annotation
let a: i32 = 5;
let b: f64 = 2.0;
let c: bool = true;
// Sometimes required
let guess: u32 = "42".parse().expect("Not a number!");
}
Operators
Arithmetic
fn main() {
let sum = 5 + 10;
let difference = 95.5 - 4.3;
let product = 4 * 30;
let quotient = 56.7 / 32.2;
let floored = 5 / 2; // Integer division: 2
let remainder = 43 % 5;
println!("{} {} {} {} {} {}",
sum, difference, product, quotient, floored, remainder);
}
Comparison
fn main() {
println!("5 == 5: {}", 5 == 5); // true
println!("5 != 6: {}", 5 != 6); // true
println!("5 > 3: {}", 5 > 3); // true
println!("5 < 3: {}", 5 < 3); // false
println!("5 >= 5: {}", 5 >= 5); // true
println!("5 <= 4: {}", 5 <= 4); // false
}
Logical
fn main() {
let t = true;
let f = false;
println!("AND: {}", t && f); // false
println!("OR: {}", t || f); // true
println!("NOT: {}", !t); // false
}
Bitwise
fn main() {
let a = 0b1010;
let b = 0b1100;
println!("AND: {:b}", a & b); // 1000
println!("OR: {:b}", a | b); // 1110
println!("XOR: {:b}", a ^ b); // 0110
println!("NOT: {:b}", !a); // ...11110101
println!("Left shift: {:b}", a << 1); // 10100
println!("Right shift: {:b}", a >> 1); // 101
}
Compound Assignment
fn main() {
let mut x = 5;
x += 1; // x = x + 1
x -= 1; // x = x - 1
x *= 2; // x = x * 2
x /= 2; // x = x / 2
x %= 3; // x = x % 3
}
Type Casting
fn main() {
let x = 5;
let y = x as f64; // i32 -> f64
let a = 65u8;
let b = a as char; // u8 -> char ('A')
let c = 1000;
let d = c as u8; // Truncates to 232
println!("{} {} {} {}", y, b, c, d);
}
Warning: Casting can truncate or wrap. Use checked conversions when safety matters:
fn main() {
let x: u32 = 1000;
// Safe conversion
match u8::try_from(x) {
Ok(val) => println!("Converted: {}", val),
Err(e) => println!("Conversion failed: {}", e),
}
}
String Operations
Creating Strings
fn main() {
let s1 = String::from("hello");
let s2 = "world".to_string();
let s3 = String::new();
}
Concatenation
fn main() {
let s1 = String::from("Hello");
let s2 = String::from(" world");
// Using +
let s3 = s1 + &s2; // s1 is moved, can't use it anymore
// Using format! macro
let s1 = String::from("Hello");
let s4 = format!("{} {}", s1, s2); // s1 and s2 still valid
println!("{}", s4);
}
String Methods
fn main() {
let mut s = String::from("hello");
// Append
s.push_str(" world");
s.push('!');
// Length
println!("Length: {}", s.len());
// Contains
println!("Contains 'world': {}", s.contains("world"));
// Replace
let s2 = s.replace("world", "Rust");
// Split
for word in s.split_whitespace() {
println!("{}", word);
}
// Trim
let padded = " hello ";
println!("'{}'", padded.trim());
}
String Slicing
fn main() {
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
let hello2 = &s[..5]; // Same as [0..5]
let world2 = &s[6..]; // From 6 to end
let all = &s[..]; // Entire string
println!("{} {}", hello, world);
}
Warning: String slicing uses byte indices, not character indices!
fn main() {
let s = "😻";
// let c = &s[0..1]; // PANIC! Invalid UTF-8
let c = &s[0..4]; // OK: 😻 is 4 bytes
}
Printing and Formatting
println! and format!
fn main() {
let x = 42;
let y = 3.14;
let name = "Alice";
// Basic
println!("x = {}", x);
// Multiple values
println!("x = {}, y = {}", x, y);
// Positional
println!("{0} {1} {0}", "A", "B"); // A B A
// Named
println!("{name} is {age} years old", name="Bob", age=30);
// Debug output
let arr = [1, 2, 3];
println!("{:?}", arr);
// Pretty debug
println!("{:#?}", arr);
}
Format Specifiers
fn main() {
let x = 255;
println!("Decimal: {}", x);
println!("Hex: {:x}", x);
println!("Hex (upper): {:X}", x);
println!("Binary: {:b}", x);
println!("Octal: {:o}", x);
let y = 3.14159;
println!("Float: {}", y);
println!("2 decimals: {:.2}", y);
println!("Scientific: {:e}", y);
let s = "hello";
println!("Right-align: {:>10}", s);
println!("Left-align: {:10}", s);
println!("Center: {:^10}", s);
println!("Padded: {:0>10}", s);
}
Comments
// Single-line comment
/*
Multi-line comment
Can span multiple lines
*/
/// Documentation comment for the next item
/// These appear in generated docs
fn documented_function() {}
//! Documentation comment for the containing item
//! Used at the top of modules or crates
fn main() {
// Comments are ignored by compiler
let x = 5; // inline comment
}
Practice Exercises
Exercise 1: Temperature Converter
Write a program that:
- Converts Fahrenheit to Celsius
- Formula: C = (F - 32) × 5/9
fn main() {
let fahrenheit = 98.6;
// Your code here
}
Exercise 2: String Manipulation
Create a program that:
- Takes a sentence
- Counts the number of words
- Reverses each word
- Prints the result
Exercise 3: Type Exploration
Write code that:
- Declares variables of each scalar type
- Prints their sizes using
std::mem::size_of::<Type>() - Shows min and max values for integer types
Hint: Use i32::MIN and i32::MAX
Common Mistakes
1. Forgetting mut
let x = 5;
x = 6; // ERROR
Fix: let mut x = 5;
2. Type Mismatch
let x: i32 = 5;
let y: i64 = x; // ERROR
Fix: let y: i64 = x as i64;
3. String Concatenation
let s1 = "hello";
let s2 = " world";
let s3 = s1 + s2; // ERROR: can't add &str
Fix: let s3 = s1.to_string() + s2; or format!("{}{}", s1, s2)
4. Integer Division
let x = 5 / 2; // Result: 2, not 2.5
Fix: let x = 5.0 / 2.0; or let x = 5 as f64 / 2.0;
Key Takeaways
- Variables are immutable by default - use
mutfor mutability - Rust has strong static typing with type inference
- Shadowing allows rebinding and type changes
- Constants must have type annotations and are computed at compile time
- Strings come in two flavors:
String(owned) and&str(borrowed) - Use
asfor type casting, but be aware of truncation - The
println!macro supports rich formatting options
Next Steps
In the next chapter, we'll explore Rust's most distinctive feature: the ownership system. This is what makes Rust special and enables memory safety without garbage collection.
Quick Reference
// Variables
let x = 5; // Immutable
let mut y = 5; // Mutable
const MAX: u32 = 100; // Constant
// Types
let a: i32 = 5; // 32-bit integer
let b: f64 = 3.14; // 64-bit float
let c: bool = true; // Boolean
let d: char = '😻'; // Unicode character
let e = (1, 2.0); // Tuple
let f = [1, 2, 3]; // Array
// Strings
let s1: &str = "hello"; // String slice
let s2: String = String::from("hello"); // String
// Casting
let x = 5;
let y = x as f64;
// Printing
println!("x = {}", x);
println!("Debug: {:?}", arr);