Lesson 6 of 94
Scalar Types: Integers, Floats, Booleans & Characters
A scalar type holds a single value: an integer, a floating-point number, a boolean, or a character. Rust's scalar types have three notable properties:
- Rust has unsigned integers (
u8,u32,u64...). - Rust never converts between number types automatically. Adding an
i32to ani64is a compile error until you convert one of them. - Integer overflow is not silent: it is a compile error when the compiler can see it, and a crash (panic) in debug builds when it happens at runtime.
The type table
| Rust | Size | Range |
|---|---|---|
i8 |
8 bits | -128 to 127 |
i16 |
16 bits | -32,768 to 32,767 |
i32 |
32 bits | about ±2.1 billion |
i64 |
64 bits | about ±9.2 × 10¹⁸ |
i128 |
128 bits | about ±1.7 × 10³⁸ |
isize |
pointer size | 64 bits on 64-bit machines |
u8 |
8 bits | 0 to 255 |
u16 |
16 bits | 0 to 65,535 |
u32 |
32 bits | 0 to about 4.3 billion |
u64 |
64 bits | 0 to about 1.8 × 10¹⁹ |
u128 |
128 bits | 0 to about 3.4 × 10³⁸ |
usize |
pointer size | Used for lengths and indexes |
f32 |
32 bits | about 7 significant digits |
f64 |
64 bits | about 16 significant digits |
bool |
8 bits | true, false |
char |
32 bits | Any Unicode scalar value |
The i means signed, u means unsigned, and the number is the size in bits.
1. Default integer and float types
With no annotation, an integer literal is i32 and a decimal literal is f64.
fn main() {
let items = 12;
let weight = 2.5;
let big: i64 = 9_000_000_000;
let tiny: i8 = -100;
println!("{items} {weight} {big} {tiny}");
}12 2.5 9000000000 -1002. The minimum and maximum of each type
Every integer type has MIN and MAX constants.
fn main() {
println!("i8: {} to {}", i8::MIN, i8::MAX);
println!("u8: {} to {}", u8::MIN, u8::MAX);
println!("i32: {} to {}", i32::MIN, i32::MAX);
println!("u32: {} to {}", u32::MIN, u32::MAX);
println!("i64: {} to {}", i64::MIN, i64::MAX);
println!("u64: {} to {}", u64::MIN, u64::MAX);
println!("u128 max: {}", u128::MAX);
}i8: -128 to 127
u8: 0 to 255
i32: -2147483648 to 2147483647
u32: 0 to 4294967295
i64: -9223372036854775808 to 9223372036854775807
u64: 0 to 18446744073709551615
u128 max: 3402823669209384634633746074317682114553. Unsigned integers cannot be negative
Use an unsigned type when a negative value would be a bug: a quantity, an age, a byte, a length. The compiler rejects negative literals for unsigned types.
fn main() {
let quantity: u32 = -1;
println!("{quantity}");
}error[E0600]: cannot apply unary operator `-` to type `u32`
--> src/main.rs:2:25
|
2 | let quantity: u32 = -1;
| ^^ cannot apply unary operator `-`
|
= note: unsigned values cannot be negated
help: you may have meant the maximum value of `u32`
|
2 - let quantity: u32 = -1;
2 + let quantity: u32 = u32::MAX;
|Using u32 for a quantity makes a negative value impossible.
4. A literal that does not fit is an error
fn main() {
let level: u8 = 256;
println!("{level}");
}error: literal out of range for `u8`
--> src/main.rs:2:21
|
2 | let level: u8 = 256;
| ^^^
|
= note: the literal `256` does not fit into the type `u8` whose range is `0..=255`
= note: `#[deny(overflowing_literals)]` on by default5. Integer literal forms
Rust supports decimal, hexadecimal, octal, and binary literals, byte literals, and type suffixes such as 10u8 or 5_i64.
fn main() {
let million = 1_000_000;
let hex = 0xFF;
let octal = 0o755;
let binary = 0b1010_1010;
let byte = b'A'; // a u8 holding the ASCII code of 'A'
let small = 200u8; // suffix sets the type to u8
let wide = 5_i64; // suffix sets the type to i64
println!("{million} {hex} {octal} {binary} {byte} {small} {wide}");
}1000000 255 493 170 65 200 56. Arithmetic operators
+, -, *, /, and % are the usual arithmetic operators. Integer division truncates toward zero, and % keeps the sign of the left operand.
fn main() {
println!("7 / 2 = {}", 7 / 2);
println!("-7 / 2 = {}", -7 / 2);
println!("7 % 3 = {}", 7 % 3);
println!("-7 % 3 = {}", -7 % 3);
println!("7.0 / 2.0 = {}", 7.0 / 2.0);
}7 / 2 = 3
-7 / 2 = -3
7 % 3 = 1
-7 % 3 = -1
7.0 / 2.0 = 3.57. rem_euclid for a remainder that is never negative
For "wrap around" calculations such as days of the week or array positions, you usually want a remainder in 0..n. Rust has rem_euclid for this. It is a method, called with a dot.
fn main() {
let today: i32 = 2; // 0 = Monday, so 2 = Wednesday
let days_back: i32 = -10;
let result = (today + days_back) % 7;
let wrapped = (today + days_back).rem_euclid(7);
println!("with %: {result}");
println!("with rem_euclid: {wrapped}");
}with %: -1
with rem_euclid: 68. Overflow the compiler can see is a compile error
When the Rust compiler can see an overflow, it refuses to compile.
fn main() {
let max = i32::MAX;
let next = max + 1;
println!("{next}");
}error: this arithmetic operation will overflow
--> src/main.rs:3:16
|
3 | let next = max + 1;
| ^^^^^^^ attempt to compute `i32::MAX + 1_i32`, which would overflow
|
= note: `#[deny(arithmetic_overflow)]` on by defaultWhen an overflow only happens at runtime, debug builds stop the program with a panic (Rust's crash for bugs). You will see one in Control Flow. Release builds wrap around silently for speed, so do not rely on the panic: say what you want explicitly, as the next example shows.
9. Choosing overflow behavior explicitly
Integer types have methods for each overflow policy:
checked_*returnsSome(result), orNoneif it would overflow. These are values of typeOption, covered in Option;{:?}prints them.wrapping_*wraps around.saturating_*stops at the minimum or maximum.overflowing_*returns the wrapped result and whether it overflowed.
fn main() {
let volume: u8 = 250;
println!("checked: {:?}", volume.checked_add(10));
println!("checked ok: {:?}", volume.checked_add(5));
println!("wrapping: {}", volume.wrapping_add(10));
println!("saturating: {}", volume.saturating_add(10));
println!("overflowing: {:?}", volume.overflowing_add(10));
}checked: None
checked ok: Some(255)
wrapping: 4
saturating: 255
overflowing: (4, true)Use saturating_sub for counters that should stop at zero, and checked_* for money and anything where overflow means a bug you must handle.
10. Integer methods
fn main() {
let temperature: i32 = -15;
println!("abs: {}", temperature.abs());
println!("pow: {}", 2_i32.pow(10));
println!("min: {}", 7.min(3));
println!("max: {}", 7.max(3));
println!("clamp: {}", 150.clamp(0, 100));
println!("signum: {}", temperature.signum());
println!("is_power_of_two: {}", 64_u32.is_power_of_two());
println!("count_ones: {}", 0b1011_u8.count_ones());
}abs: 15
pow: 1024
min: 3
max: 7
clamp: 100
signum: -1
is_power_of_two: true
count_ones: 3Calling a method on a literal works: 7.min(3). When the literal's type matters, add a suffix, as in 2_i32.pow(10).
11. Floating-point numbers
f64 is the default and the right choice almost always. Floats follow the IEEE 754 standard.
fn main() {
let a: f64 = 0.1;
let b: f64 = 0.2;
println!("0.1 + 0.2 = {}", a + b);
println!("equal to 0.3? {}", a + b == 0.3);
println!("close to 0.3? {}", (a + b - 0.3).abs() < 1e-10);
let single: f32 = 1.0 / 3.0;
let double: f64 = 1.0 / 3.0;
println!("f32: {single}");
println!("f64: {double}");
}0.1 + 0.2 = 0.30000000000000004
equal to 0.3? false
close to 0.3? true
f32: 0.33333334
f64: 0.333333333333333312. Float methods
fn main() {
let x: f64 = 2.0;
let price: f64 = 19.456;
println!("sqrt: {}", x.sqrt());
println!("powi: {}", x.powi(10));
println!("powf: {}", x.powf(0.5));
println!("round: {}", price.round());
println!("floor: {}", price.floor());
println!("ceil: {}", price.ceil());
println!("trunc: {}", (-price).trunc());
println!("cents: {}", (price * 100.0).round() / 100.0);
println!("pi: {:.5}", std::f64::consts::PI);
}sqrt: 1.4142135623730951
powi: 1024
powf: 1.4142135623730951
round: 19
floor: 19
ceil: 20
trunc: -19
cents: 19.46
pi: 3.14159std::f64::consts::PI is a constant in the standard library. The :: separates path segments.
13. Method calls need a known float type
Rust needs to know whether a float is f32 or f64 before it can call a method on it. If nothing tells it, you get an error. Fix it with a type annotation or a suffix such as 2.0_f64.
fn main() {
let x = 2.0;
println!("{}", x.sqrt());
}error[E0689]: can't call method `sqrt` on ambiguous numeric type `{float}`
--> src/main.rs:3:22
|
3 | println!("{}", x.sqrt());
| ^^^^
|
help: you must specify a type for this binding, like `f32`
|
2 | let x: f32 = 2.0;
| +++++14. Special float values: infinity and NaN
Float division by zero does not crash. It produces infinity, or NaN ("not a number").
fn main() {
let positive: f64 = 1.0 / 0.0;
let negative: f64 = -1.0 / 0.0;
let not_a_number = 0.0_f64 / 0.0;
println!("{positive} {negative} {not_a_number}");
println!("NaN == NaN? {}", not_a_number == not_a_number);
println!("is_nan? {}", not_a_number.is_nan());
println!("is_finite? {}", positive.is_finite());
}inf -inf NaN
NaN == NaN? false
is_nan? true
is_finite? falseInteger division by zero is different: it is a compile error if the compiler can see it, and a panic at runtime otherwise.
15. Booleans and logical operators
&&, ||, and ! work as logical operators and short-circuit.
fn main() {
let is_admin = false;
let is_owner = true;
let is_banned = false;
let can_edit = (is_admin || is_owner) && !is_banned;
println!("can edit: {can_edit}");
println!("as number: {}", can_edit as i32);
}can edit: true
as number: 116. No truthiness: conditions must be bool
Unlike C or JavaScript, a number is never treated as true or false.
fn main() {
let unread = 3;
if unread {
println!("You have mail");
}
}error[E0308]: mismatched types
--> src/main.rs:3:8
|
3 | if unread {
| ^^^^^^ expected `bool`, found integerWrite the comparison instead: if unread > 0.
17. Characters are full Unicode
A Rust char is 4 bytes and can hold any Unicode character, including emoji.
fn main() {
let letter = 'a';
let accented = 'é';
let kanji = '字';
let crab = '🦀';
println!("{letter} {accented} {kanji} {crab}");
println!("code point of crab: U+{:X}", crab as u32);
println!("size of char: {} bytes", std::mem::size_of::<char>());
}a é 字 🦀
code point of crab: U+1F980
size of char: 4 bytesstd::mem::size_of::<char>() returns the size of a type in bytes. The ::<char> part names the type to measure; it is called the "turbofish" and appears again in Generics.
18. Character methods
fn main() {
let c = 'q';
let digit = '7';
println!("{}", c.is_alphabetic());
println!("{}", c.is_uppercase());
println!("{}", c.to_ascii_uppercase());
println!("{}", digit.is_ascii_digit());
println!("{:?}", digit.to_digit(10));
println!("{}", ' '.is_whitespace());
}true
false
Q
true
Some(7)
trueto_digit(10) returns Some(7) because the conversion can fail (for example on 'x'). This is an Option; see Option.
19. Bitwise operators
Bitwise NOT is ! (not ~), and there is no >>> operator. For a logical (unsigned) shift, use an unsigned type.
fn main() {
let read: u8 = 0b100;
let write: u8 = 0b010;
let execute: u8 = 0b001;
let permissions = read | write; // combine flags
let can_write = permissions & write != 0; // test a flag
let without_write = permissions & !write; // clear a flag
let toggled = permissions ^ execute; // flip a flag
println!("permissions: {permissions:03b}");
println!("can write: {can_write}");
println!("without write: {without_write:03b}");
println!("toggled: {toggled:03b}");
println!("1 << 4 = {}", 1 << 4);
println!("0xF0 >> 4 = {}", 0xF0 >> 4);
}permissions: 110
can write: true
without write: 100
toggled: 111
1 << 4 = 16
0xF0 >> 4 = 1520. No automatic widening between number types
Rust never widens numbers implicitly: every conversion is explicit.
fn main() {
let quantity: i32 = 3;
let unit_price: i64 = 1_500;
let total: i64 = quantity * unit_price;
println!("{total}");
}error[E0308]: mismatched types
--> src/main.rs:4:33
|
4 | let total: i64 = quantity * unit_price;
| ^^^^^^^^^^ expected `i32`, found `i64`This feels strict at first, but it removes a whole family of bugs: a quiet conversion never loses data or changes a sign without you asking for it.
21. Converting with as
as converts between number types. Converting to a wider type is always safe. Converting to a narrower type or between signed and unsigned can change the value.
fn main() {
let quantity: i32 = 3;
let unit_price: i64 = 1_500;
println!("total: {}", quantity as i64 * unit_price);
println!("i32 -> f64: {}", 7 as f64 / 2.0);
println!("f64 -> i32 truncates: {}", 9.99 as i32);
println!("f64 -> i32 negative: {}", -9.99 as i32);
let big: i32 = 300;
println!("300 as u8 wraps: {}", big as u8);
println!("-1 as u32: {}", -1_i32 as u32);
println!("huge float saturates: {}", 1e20 as i32);
println!("NaN becomes zero: {}", f64::NAN as i32);
}total: 4500
i32 -> f64: 3.5
f64 -> i32 truncates: 9
f64 -> i32 negative: -9
300 as u8 wraps: 44
-1 as u32: 4294967295
huge float saturates: 2147483647
NaN becomes zero: 0Float-to-integer conversions saturate in Rust (1e20 as i32 gives i32::MAX), and NaN becomes 0.
22. Lossless conversion with From
When a conversion can never lose data, prefer i64::from(x) over x as i64. If someone later changes x to a type that could lose data, the code stops compiling instead of silently truncating. Conversion Traits covers this in depth.
fn main() {
let small: u8 = 200;
let medium: i32 = -40;
let widened = u32::from(small);
let as_i64 = i64::from(medium);
let as_f64 = f64::from(medium);
println!("{widened} {as_i64} {as_f64}");
}200 -40 -4023. usize for sizes and indexes
Lengths and positions in Rust are usize, an unsigned integer as wide as a memory address (64 bits on today's machines). You will meet it constantly: len() returns usize, and array indexes must be usize.
fn main() {
let name = "Rustacean";
let length: usize = name.len();
let half = length / 2;
println!("{name} has {length} bytes; half is {half}");
println!("usize max: {}", usize::MAX);
}Rustacean has 9 bytes; half is 4
usize max: 1844674407370955161524. Realistic example: store money as integer cents
Floats cannot represent most decimal fractions exactly, so real billing systems store money as whole cents. This splits a bill fairly, giving the leftover cents to the first people.
fn main() {
let bill_cents: i64 = 10_000; // 100.00
let people: i64 = 3;
let share = bill_cents / people;
let leftover = bill_cents % people;
println!("Each pays {}.{:02}", share / 100, share % 100);
println!("Leftover cents: {leftover}");
let first_share = share + 1;
println!("The first {leftover} person(s) pay {}.{:02}", first_share / 100, first_share % 100);
}Each pays 33.33
Leftover cents: 1
The first 1 person(s) pay 33.3425. Realistic example: human-readable file sizes
fn main() {
let bytes: u64 = 5_368_709_120;
let kib = bytes as f64 / 1024.0;
let mib = kib / 1024.0;
let gib = mib / 1024.0;
println!("{bytes} bytes");
println!("= {kib:.1} KiB");
println!("= {mib:.1} MiB");
println!("= {gib:.2} GiB");
}5368709120 bytes
= 5242880.0 KiB
= 5120.0 MiB
= 5.00 GiB26. Realistic example: packing an RGB color into one integer
Graphics code often stores a color as one u32 in the form 0xRRGGBB. Shifts and masks pack and unpack it.
fn main() {
let red: u8 = 255;
let green: u8 = 128;
let blue: u8 = 64;
let packed: u32 = (u32::from(red) << 16) | (u32::from(green) << 8) | u32::from(blue);
println!("packed: {packed:#08X}");
let r = (packed >> 16) & 0xFF;
let g = (packed >> 8) & 0xFF;
let b = packed & 0xFF;
println!("unpacked: r={r} g={g} b={b}");
let gray = (r * 30 + g * 59 + b * 11) / 100;
println!("grayscale: {gray}");
}packed: 0xFF8040
unpacked: r=255 g=128 b=64
grayscale: 15927. Realistic example: body mass index
fn main() {
let weight_kg: f64 = 72.5;
let height_cm: u32 = 178;
let height_m = f64::from(height_cm) / 100.0;
let bmi = weight_kg / height_m.powi(2);
let rounded = (bmi * 10.0).round() / 10.0;
println!("Height: {height_m} m");
println!("BMI: {rounded}");
println!("Healthy range: {}", (18.5..25.0).contains(&bmi));
}Height: 1.78 m
BMI: 22.9
Healthy range: true18.5..25.0 is a range, and contains(&bmi) checks whether bmi lies inside it. Ranges appear in Control Flow, and the & in References & Borrowing.
Summary
- Integers:
i8toi128(signed),u8tou128(unsigned), andisize/usize(pointer-sized). The default isi32. - Floats:
f32andf64. The default isf64. - Overflow is a compile error or a debug-mode panic, never silent in debug. Use
checked_,wrapping_, andsaturating_methods to choose behavior. - Conditions must be
bool. charis a 4-byte Unicode scalar value, not a UTF-16 unit.- There are no automatic numeric conversions. Use
T::from(x)for lossless conversions andasfor everything else.
Next: Functions.
