2. Types, mutability and expressions
Full example: examples/l02_types.rs — run it with cargo run --example l02_types.
Immutable by default
Section titled “Immutable by default”let answer = 42; // inferred as i32, cannot changelet mut counter: u32 = 0; // explicitly mutablecounter += 1;In C# terms, every let is like a local you can never reassign; in Java, like final var. You opt into mutability with mut:
let count = 0;count += 1;error[E0384]: cannot assign twice to immutable variable `count` --> e_immutable.rs:3:5 |2 | let count = 0; | ----- first assignment to `count`3 | count += 1; | ^^^^^^^^^^ cannot assign twice to immutable variable |help: consider making this binding mutable |2 | let mut count = 0; | +++Shadowing
Section titled “Shadowing”You can declare a new variable with the same name, even with a different type. Handy for “parse and replace”:
let input = " 7 ";let input: i32 = input.trim().parse().expect("not a number");println!("input + 1 = {}", input + 1); // input + 1 = 8This is not mutation: the first input (a &str) still exists, it is just hidden. C# and Java forbid redeclaring a local in the same scope.
Scalar types
Section titled “Scalar types”| Rust | C# | Java | Notes |
|---|---|---|---|
i8 / u8 |
sbyte / byte |
byte (signed) / — |
Java has no unsigned integers |
i16 / u16 |
short / ushort |
short / — |
|
i32 / u32 |
int / uint |
int / — |
i32 is the default integer |
i64 / u64 |
long / ulong |
long / — |
|
i128 / u128 |
Int128 / UInt128 |
— | |
isize / usize |
nint / nuint |
— | pointer-sized; used for indexes and lengths |
f32 / f64 |
float / double |
float / double |
f64 is the default float |
bool |
bool |
boolean |
|
char |
Rune |
int code point |
4 bytes, a Unicode scalar value — not a UTF-16 unit like C#/Java char |
let note = '♪';println!("{note} is {} bytes in UTF-8, size_of::<char>() = {}", note.len_utf8(), std::mem::size_of::<char>());// ♪ is 3 bytes in UTF-8, size_of::<char>() = 4No implicit conversions
Section titled “No implicit conversions”C# and Java silently widen an int to a long. Rust never converts numbers for you:
let small: i32 = 10;let big: i64 = 20;let total = small + big;error[E0308]: mismatched types --> e_mismatch.rs:4:25 |4 | let total = small + big; | ^^^ expected `i32`, found `i64`Convert explicitly with as (or i64::from(small), which only exists for lossless conversions):
let total = small as i64 + big; // 30Overflow is a bug, not a feature
Section titled “Overflow is a bug, not a feature”| C# | Java | Rust debug build | Rust release build | |
|---|---|---|---|---|
255u8 + 1 |
wraps (unless checked) |
wraps | panics | wraps |
In a debug build, Rust stops the program:
thread 'main' panicked at e_overflow.rs:3:16:attempt to add with overflowWhen wrapping or failure is the intended behaviour, say so explicitly:
let max = u8::MAX;println!("checked: {:?}, wrapping: {}", max.checked_add(1), max.wrapping_add(1));// checked: None, wrapping: 0Tuples and arrays
Section titled “Tuples and arrays”let point: (f64, f64) = (1.5, -2.0);let (x, y) = point; // destructuring, like C# tuple deconstructionlet primes = [2, 3, 5, 7, 11]; // fixed-size array: [i32; 5]println!("x = {x}, y = {y}, first prime = {}, count = {}", primes[0], primes.len());// x = 1.5, y = -2, first prime = 2, count = 5A growable list is Vec<T> (like List<T> / ArrayList<T>) — lesson 8 covers collections.
Everything is an expression
Section titled “Everything is an expression”if returns a value, so there is no ternary operator:
let parity = if answer % 2 == 0 { "even" } else { "odd" };A block { … } evaluates to its last expression — without a semicolon:
let area = { let width = 3; let height = 4; width * height // no `;` → this is the block's value};Functions work the same way; return is only needed for early exits:
fn square(x: i32) -> i32 { x * x}// loop + break with a valuelet mut n = 1;let first_power_over_100 = loop { n *= 2; if n > 100 { break n; }}; // 128
// for over ranges: 0..3 is end-exclusive, 1..=10 is inclusivefor i in 0..3 { print!("{i} "); // 0 1 2}let sum: i32 = (1..=10).sum(); // 55There is no C-style for (int i = 0; i < n; i++): use a range. while condition { … } also exists.
Key takeaways
Section titled “Key takeaways”letis immutable; addmutonly when you need it.- Numeric types are explicit, conversions are explicit, overflow panics in debug.
charis a Unicode scalar value (4 bytes), not UTF-16.if, blocks,loopand functions are expressions; the last expression without;is the value.
Exercises
Section titled “Exercises”- Write a function
clamp_percent(value: i32) -> u8that returns0for negative values,100for values above 100, and the value otherwise — usingifas an expression.
Solution
fn clamp_percent(value: i32) -> u8 { if value < 0 { 0 } else if value > 100 { 100 } else { value as u8 }}The as u8 is safe here because the value is known to be within 0..=100. Rust also provides value.clamp(0, 100) as u8.
- Why does this function fail to compile, and what is the one-character fix?
fn double(x: i32) -> i32 { x * 2;}Solution
The trailing semicolon turns x * 2 into a statement, so the body evaluates to () instead of i32 (error E0308: mismatched types). Remove the ;.
- In C#,
byte b = 255; b++;gives0. What happens in Rust withlet mut b: u8 = 255; b += 1;?
Solution
In a debug build the program panics with attempt to add with overflow. In a release build it wraps to 0. If wrapping is what you want, write b = b.wrapping_add(1);; if you want to detect it, use b.checked_add(1), which returns an Option<u8>.