Skip to content

2. Types, mutability and expressions

Full example: examples/l02_types.rs — run it with cargo run --example l02_types.

let answer = 42; // inferred as i32, cannot change
let mut counter: u32 = 0; // explicitly mutable
counter += 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;
| +++

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 = 8

This 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.

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>() = 4

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; // 30
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 overflow

When 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: 0
let point: (f64, f64) = (1.5, -2.0);
let (x, y) = point; // destructuring, like C# tuple deconstruction
let 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 = 5

A growable list is Vec<T> (like List<T> / ArrayList<T>) — lesson 8 covers collections.

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 value
let 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 inclusive
for i in 0..3 {
print!("{i} "); // 0 1 2
}
let sum: i32 = (1..=10).sum(); // 55

There is no C-style for (int i = 0; i < n; i++): use a range. while condition { … } also exists.

  • let is immutable; add mut only when you need it.
  • Numeric types are explicit, conversions are explicit, overflow panics in debug.
  • char is a Unicode scalar value (4 bytes), not UTF-16.
  • if, blocks, loop and functions are expressions; the last expression without ; is the value.
  1. Write a function clamp_percent(value: i32) -> u8 that returns 0 for negative values, 100 for values above 100, and the value otherwise — using if as 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.

  1. 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 ;.

  1. In C#, byte b = 255; b++; gives 0. What happens in Rust with let 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>.