4.2Integer types
An integer is a whole number: no fractional part, possibly negative. Rust gives you ten integer types, and despite the apparent abundance, the system fits in one sentence: pick a size (8, 16, 32, 64, or 128 bits) and pick whether negatives are allowed (i for signed, u for unsigned).
| Type | Size | Range |
|---|---|---|
i8 | 1 byte | −128 to 127 |
i16 | 2 bytes | −32,768 to 32,767 |
i32 | 4 bytes | −2,147,483,648 to 2,147,483,647 |
i64 | 8 bytes | −9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 |
i128 | 16 bytes | about −1.7 × 10³⁸ to 1.7 × 10³⁸ |
u8 | 1 byte | 0 to 255 |
u16 | 2 bytes | 0 to 65,535 |
u32 | 4 bytes | 0 to 4,294,967,295 |
u64 | 8 bytes | 0 to 18,446,744,073,709,551,615 |
u128 | 16 bytes | 0 to about 3.4 × 10³⁸ |
The ranges follow from lesson 4.1's arithmetic: n bits make 2ⁿ patterns. An unsigned (u) type spends all its patterns on 0 upward, so u8 covers 0 through 255. A signed (i) type spends half its patterns on negatives, so i8 covers −128 through 127: same 256 patterns, different neighborhood. Need the exact bounds in code? Every type carries them as constants:
fn main() {
println!("i32 spans {} to {}", i32::MIN, i32::MAX);
println!("u16 spans {} to {}", u16::MIN, u16::MAX);
}i32 spans -2147483648 to 2147483647
u16 spans 0 to 65535Which one?
The honest answer is that one of them is the default and the rest are for reasons:
Best practice
Use i32 unless you have a specific reason not to. It's Rust's default for integer literals, it's comfortably large for most quantities, and it's fast on every mainstream processor. Reach for i64/u64 when values can exceed two billion in real use (file sizes, timestamps, global counters), u8 when you're working with raw bytes (chapter 18 will), and the others when a measured reason appears. The 128-bit types are for cryptography-and-similar territory; you'll know.
What about unsigned for values that "can't be negative," like an age or a count? Tempting, sometimes right, and worth a moment of caution: subtraction is where unsigned types bite. A count of 3 minus a count of 5 is −2, and −2 does not exist in u32's world; exactly what happens then is the next lesson's whole subject (4.4), but "nothing good by accident" is a fair preview. Prefer signed for quantities you'll do arithmetic on, and treat unsigned as a statement that negative values are impossible, not merely unexpected. (One unsigned type is unavoidable and everywhere, usize, and it gets the next lesson, 4.3.)
The classic disaster that doesn't compile here
If you ask a C++ veteran about unsigned types, you'll hear war stories, and the worst ones come from mixing: compare a signed and an unsigned value there, and the signed one is silently converted, so -1 < 1u evaluates to false (the −1 becomes 4,294,967,295 on the way in). Programs make absurd decisions, nothing crashes, and someone debugs it on a weekend.
Here's that bug, attempted in Rust:
fn main() {
let signed: i32 = -1;
let unsigned: u32 = 1;
if signed < unsigned {
println!("sanity prevails");
}
}error[E0308]: mismatched types
--> src/main.rs:4:17
|
4 | if signed < unsigned {
| ------ ^^^^^^^^ expected `i32`, found `u32`
| |
| expected because this is `i32`
Rust has no implicit numeric conversions, none, not even the "safe-looking" ones. Different integer types don't compare, don't add, and don't pass for each other without an explicit conversion that you write and therefore think about (lesson 4.10). The entire silent-mixing genre of bug is not rare in Rust; it's unrepresentable.
Key insight
This is the chapter 0 sales pitch keeping its shape at small scale: where C++ trusts you with a silent conversion and learncpp.com needs a lesson titled "why to avoid unsigned," Rust converts the trap into a compile error and unsigned types back into ordinary tools. The strictness that nags you ("just compare them!") is the same strictness that deleted the weekend debugging session. You'll be choosing types on their merits, not on their blast radius.
Quiz time
Question #1
Without the table: what's the largest value of u16, and how do you derive it from the size?
Show solution
65,535. Sixteen bits make 2¹⁶ = 65,536 patterns; spent on 0 upward, the largest is 2¹⁶ − 1.
Question #2
Pick a sensible type for each, with one phrase of why: (a) a person's age in years, (b) the byte count of a possibly-huge file, (c) a temperature change, which might be negative, (d) one byte read from a network packet.
Show solution
(a) i32: the default; nothing about ages needs anything else (resist u8 cleverness; ages get subtracted).
(b) u64: real files exceed i32/u32 territory, and a negative byte count is meaningless.
(c) i32: it's signed by nature.
(d) u8: it is a byte.
Reasonable people defend other choices; the marks are for "default to i32, deviate with a reason."
Question #3
Predict the compiler's reaction:
fn main() {
let total: i64 = 100;
let count: i32 = 4;
println!("{}", total + count);
}Show solution
E0308, mismatched types: i64 and i32 don't add, even though both are signed integers and the math is obviously harmless. No implicit conversions means no implicit conversions; lesson 4.10 shows the explicit fix.
Two integer types remain, the odd ones whose size depends on your machine, and they come bundled with the rest of the literal syntax.