Primitive Types
Every value in Rust has a type, and the compiler knows all of them before the program runs. The simplest types are the primitives: integers, floating-point numbers, booleans and characters. They are built into the language, and everything else you will write is made out of them.
This lesson goes through each primitive type, the arithmetic you can do with
numbers, what happens when a number gets too big for its type, and how to
convert one numeric type into another with as.
Integers
An integer type is named by whether it can be negative and how many bits it
uses. Types starting with i are signed (they can hold negative numbers),
types starting with u are unsigned (zero and up):
| Bits | Signed | Unsigned | Unsigned range |
|---|---|---|---|
| 8 | i8 | u8 | 0 to 255 |
| 16 | i16 | u16 | 0 to 65,535 |
| 32 | i32 | u32 | 0 to about 4.3 billion |
| 64 | i64 | u64 | 0 to about 1.8 x 10^19 |
| 128 | i128 | u128 | 0 to about 3.4 x 10^38 |
| arch | isize | usize | same as 32 or 64 bits |
A signed type covers the same number of values, shifted so that zero sits in
the middle: i8 goes from -128 to 127. Every integer type has MIN and MAX
constants if you need the exact limits:
fn main() {
println!("u8: {} to {}", u8::MIN, u8::MAX);
println!("i8: {} to {}", i8::MIN, i8::MAX);
println!("i32: {} to {}", i32::MIN, i32::MAX);
}u8: 0 to 255
i8: -128 to 127
i32: -2147483648 to 2147483647usize and isize are as wide as a memory address on the machine you compile
for, which is 64 bits on almost every computer today. usize is the type Rust
uses for sizes and positions: the length of a collection and the index you use
to look something up in it are both usize.
When you write an integer without saying which type it is, and nothing else
forces a type, Rust picks i32. It is a good default: fast everywhere and big
enough for most counters and small quantities.
Integer literals
Rust gives you a few ways to write integers:
fn main() {
let population = 8_100_000_000_u64;
let small = 7u8;
let mask = 0xff;
let flags = 0b1010_0001;
let permissions = 0o755;
let letter = b'R';
println!("{population} {small} {mask} {flags} {permissions} {letter}");
}8100000000 7 255 161 493 82- Underscores are ignored, so
8_100_000_000is easier to read than8100000000. - A suffix picks the type:
7u8is au8,8_100_000_000_u64au64. 0x,0band0ostart hexadecimal, binary and octal numbers.b'R'is a byte literal: the ASCII code of a character, as au8.
Floating-point numbers
Rust has two floating-point types, f32 and f64, for 32 and 64 bits. The
default is f64: it is about as fast as f32 on modern CPUs and much more
precise. A float literal needs a decimal point or an exponent:
fn main() {
let price = 19.99;
let ratio: f32 = 0.75;
let distance = 1.5e3;
let whole = 4.0;
println!("{price} {ratio} {distance} {whole}");
}19.99 0.75 1500 44.0 is a float and 4 is an integer; they are different types. Floats follow
the IEEE 754 standard used by nearly every language, with its familiar
rounding surprises:
fn main() {
let sum = 0.1 + 0.2;
println!("{sum}");
println!("{}", sum == 0.3);
}0.30000000000000004
falseNeither 0.1 nor 0.2 can be stored exactly in binary, so the result is a tiny
bit off. Compare floats against a small tolerance rather than with ==, and
don't use them for money.
Booleans
bool has two values, true and false. You usually get one from a
comparison, and combine them with && (and), || (or) and ! (not):
fn main() {
let age = 17;
let has_permission = true;
let is_adult = age >= 18;
let can_enter = is_adult || has_permission;
let is_minor = !is_adult;
println!("adult: {is_adult}, can enter: {can_enter}, minor: {is_minor}");
}adult: false, can enter: true, minor: trueThe comparison operators are ==, !=, <, <=, > and >=. && and
|| short-circuit: the right side is only evaluated if it can still change the
answer. Rust never treats a number as a boolean, so if count { ... } is an
error; write if count != 0 instead.
Characters
char holds a single Unicode character and is written with single quotes.
Double quotes make a string, which is a different type:
fn main() {
let letter = 'z';
let digit = '7';
let crab = '🦀';
let accented = 'é';
println!("{letter} {digit} {crab} {accented}");
println!("{}", digit.is_ascii_digit());
println!("{}", letter.to_ascii_uppercase());
println!("{}", std::mem::size_of::<char>());
}z 7 🦀 é
true
Z
4A char is always 4 bytes, enough for any Unicode scalar value: letters from
any alphabet, digits, symbols and emoji. That is not the same as a byte (u8),
and it is not always what a person would call one character: some visible
characters, like many flags, are built from several chars.
Arithmetic
Numbers support the usual operators: +, -, *, / and % (remainder):
fn main() {
let apples = 17;
let people = 5;
println!("each gets {}", apples / people);
println!("left over {}", apples % people);
println!("as floats {}", 17.0 / 5.0);
println!("negative {} {}", -17 / 5, -17 % 5);
}each gets 3
left over 2
as floats 3.4
negative -3 -2Integer division throws away the fractional part: 17 / 5 is 3, not 3.4.
It rounds towards zero, so -17 / 5 is -3, and the remainder takes the sign
of the left-hand side. If you want a fractional answer, divide floats.
Dividing an integer by zero panics (the program stops with an error). Dividing
a float by zero gives inf, or NaN ("not a number") for 0.0 / 0.0.
Both sides of an operator must be the same type. Rust will not quietly
turn an i32 into an f64, or a u8 into a u32, for you. You convert
explicitly, which is what as is for (see below).
Numbers also come with methods for things that aren't operators:
fn main() {
let x: i32 = -9;
let y: f64 = 2.0;
println!("{} {}", x.abs(), x.pow(2));
println!("{} {:.3}", y.sqrt(), y.powf(0.5));
println!("{} {}", 7.5_f64.floor(), 7.5_f64.round());
println!("{}", 3.max(8));
}9 81
1.4142135623730951 1.414
7 8
8Overflow
Each integer type has a fixed range, so what happens when a result doesn't fit?
In a debug build (the default for cargo run), Rust checks every arithmetic
operation and panics on overflow:
fn main() {
let mut volume: u8 = 250;
for _ in 0..10 {
volume += 1;
}
println!("{volume}");
}(for _ in 0..10 repeats the body ten times; loops get their own lesson.)
In a release build (cargo build --release) the checks are off for speed and
the value wraps around instead: 255 + 1 becomes 0. Code that relies on
either behaviour is fragile, so when overflow is possible, say what you want
with one of the explicit methods:
fn main() {
let volume: u8 = 250;
println!("{:?}", volume.checked_add(3));
println!("{:?}", volume.checked_add(10));
println!("{}", volume.wrapping_add(10));
println!("{}", volume.saturating_add(10));
}Some(253)
None
4
255checked_addreturnsSome(result), orNoneif it would overflow. The lesson onOptionandResultexplains how to use that value.wrapping_addwraps around on purpose: 250 + 10 is 260, which is 4 past 256.saturating_addstops at the type's limit:255.
The same families exist for the other operators: checked_sub,
wrapping_mul, saturating_sub and so on.
Converting with as
Because Rust never converts numbers for you, you do it with as:
fn main() {
let items: u8 = 12;
let price: f64 = 2.5;
let total = items as f64 * price;
println!("{total}");
let rating = 4.8_f64;
println!("{}", rating as u32);
let code = 'A' as u32;
let next = (code + 1) as u8 as char;
println!("{code} {next}");
}30
4
65 BConverting to a wider type of the same kind, like u8 to u32 or i32 to
i64, always keeps the value. Other conversions can change it, and as never
complains:
fn main() {
let big: i32 = 300;
let negative: i16 = -1;
let huge: f64 = 1e12;
println!("{}", big as u8);
println!("{}", negative as u16);
println!("{}", huge as i32);
println!("{}", -2.7_f64 as u32);
}44
65535
2147483647
0- Integer to smaller integer keeps only the low bits: 300 is
256 + 44, so it becomes44. - Signed to unsigned of the same size reinterprets the bits:
-1asu16is65535, the largestu16. - Float to integer drops the fraction and clamps to the target's range:
1e12becomesi32::MAX, and a negative float becomes0as an unsigned integer.
So as is right when you know the value fits, or when truncating is what you
want. When a value might not fit and you need to know, use try_from, which
returns a Result you can check (covered in the error handling lessons):
fn main() {
let big: i32 = 300;
println!("{:?}", u8::try_from(big));
println!("{:?}", u8::try_from(200));
}Err(TryFromIntError(PosOverflow))
Ok(200)Common mistakes
Mixing numeric types
Adding an integer to a float, or two different integer types, doesn't compile:
fn main() {
let count: u32 = 3;
let average: f64 = 2.5;
let total = count * average;
println!("{total}");
}Convert one side so both match: count as f64 * average.
A literal that doesn't fit its type
The compiler rejects a literal outside its type's range:
fn main() {
let level: u8 = 256;
println!("{level}");
}Pick a type big enough for the values you need, such as u16.
Expecting a fraction from integer division
7 / 2 is 3, because both sides are integers. If you want 3.5, the values
have to be floats before the division: 7.0 / 2.0, or a as f64 / b as f64
for variables. Converting afterwards, (a / b) as f64, is too late: the
fraction is already gone.
Double quotes for a char
fn main() {
let initial: char = "R";
println!("{initial}");
}"R" is a string slice (&str), even with one character in it. A char uses
single quotes: 'R'.
Summary
- Integers are
i8toi128(signed) andu8tou128(unsigned), plusisize/usizefor sizes and indexes. The default isi32. - Floats are
f32andf64, defaultf64. They round, so don't compare them with==. boolistrueorfalseand is never a number.charis a 4-byte Unicode character in single quotes.+ - * / %need both sides to be the same type. Integer division drops the fraction.- Overflow panics in debug builds and wraps in release; use
checked_*,wrapping_*orsaturating_*to choose. asconverts between numeric types (andchar) without checks;try_fromconverts only if the value fits.
Practise what you read
Challenges that use the ideas from this lesson.