Types & Values
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Rust 1.99.0 and the 2024 edition, checked on 2026-10-06. Rust is statically typed:
every value has a type known at compile time. This page covers the built-in primitive types — the scalars and the
built-in compound types — plus the rules for converting between them. Slices and str have their own page
(Slices & Dynamically Sized Types), as do
user-defined types (Structs & Methods,
Enums & Pattern Matching).
The primitive types at a glance
The table lists every primitive with its size on a typical 64-bit target, its range, and the type an unsuffixed literal gets when nothing else constrains it.
| Type | Size (bytes) | Range / values | Literal examples |
|---|---|---|---|
|
1 |
-128..=127 / 0..=255 |
|
|
2 |
-32 768..=32 767 / 0..=65 535 |
|
|
4 |
about ±2.1 × 109 / 0..=4.29 × 109 |
|
|
8 |
about ±9.2 × 1018 / 0..=1.8 × 1019 |
|
|
16 |
about ±1.7 × 1038 / 0..=3.4 × 1038 |
|
|
pointer size (8 on 64-bit, 4 on 32-bit) |
as |
|
|
4 |
IEEE 754 binary32, about 7 decimal digits |
|
|
8 |
IEEE 754 binary64, about 15—16 decimal digits |
|
|
1 |
|
|
|
4 |
a Unicode scalar value: U+0000..=U+D7FF and U+E000..=U+10FFFF |
|
|
0 |
the single value |
|
|
sum of fields plus padding |
a fixed-size heterogeneous group |
|
|
|
|
|
|
0 (never has a value) |
the type of expressions that never complete |
|
Source: The Book: Data Types
You can check sizes yourself with std::mem::size_of (in the prelude since 1.80, so no import is needed):
fn main() {
println!("u8 {} u32 {} u128 {} usize {}", size_of::<u8>(), size_of::<u32>(), size_of::<u128>(), size_of::<usize>());
println!("f64 {} bool {} char {} () {}", size_of::<f64>(), size_of::<bool>(), size_of::<char>(), size_of::<()>());
println!("(u8, u32) {} [u16; 5] {}", size_of::<(u8, u32)>(), size_of::<[u16; 5]>());
// u8 1 u32 4 u128 16 usize 8
// f64 8 bool 1 char 4 () 0
// (u8, u32) 8 [u16; 5] 10
}
Source: std::mem::size_of
The (u8, u32) tuple is 8 bytes, not 5, because the u32 must be 4-byte aligned; the compiler inserts padding (and
may reorder fields). Layout is covered in Memory Layout
& repr.
Integers
Integer types are named by signedness and width: i for signed (two’s complement), u for unsigned, then the
number of bits. isize and usize have the width of a pointer on the target; usize is the type of indexes,
lengths and sizes, so slicing and len() use it.
Literals can be written in decimal (98_222), hexadecimal (0xff), octal (0o77) or binary (0b1111_0000), with
_ as a visual separator anywhere after the first digit, and with an optional type suffix (57u8, 1_000_i64).
Without a suffix, the type is inferred from context; if nothing constrains it, it defaults to i32.
Which type to choose? Use i32/u32 (or i64/u64 when values can be large) for ordinary numbers, usize for
anything that indexes memory, and the narrowest type that fits for data you store in bulk. Prefer unsigned types
only when negative values are impossible and you want the compiler to say so; subtracting two usize values that
can go below zero is a common source of overflow panics.
Integer overflow
What happens when arithmetic exceeds the range of the type depends on the build profile:
-
in debug builds (
cargo build,cargo test), overflow panics:attempt to add with overflow; -
in release builds (
--release), overflow wraps around in two’s complement, silently.
This is controlled by the overflow-checks profile setting, so you can keep checks on in release builds if the
performance cost is acceptable. Relying on either behaviour is a bug; when overflow is possible, say what you want
with one of the explicit method families:
| Family | Behaviour on overflow | 250u8 + 10 |
|---|---|---|
|
wraps modulo 2N |
|
|
returns |
|
|
clamps to the minimum or maximum |
|
|
returns |
|
|
always panics, in every profile |
|
|
a wrapper whose operators all wrap |
|
use std::num::Wrapping;
fn main() {
let copies: u8 = 250;
assert_eq!(copies.wrapping_add(10), 4);
assert_eq!(copies.checked_add(10), None);
assert_eq!(copies.checked_add(5), Some(255));
assert_eq!(copies.saturating_add(10), 255);
assert_eq!(copies.overflowing_add(10), (4, true));
assert_eq!((Wrapping(copies) + Wrapping(10)).0, 4);
// Subtraction on unsigned values is the classic trap:
let on_shelf: usize = 2;
let requested: usize = 3;
match on_shelf.checked_sub(requested) {
Some(left) => println!("{left} left"),
None => println!("not enough copies"),
}
// `on_shelf - requested` would panic in debug builds and wrap to 18446744073709551615 in release.
let result = std::panic::catch_unwind(|| copies.strict_add(10));
assert!(result.is_err()); // strict_* panics even in release builds
}
Integer division by zero always panics (attempt to divide by zero) — use checked_div when the divisor comes from
input. Shifting by the type’s width or more is also an overflow (1u8 << 8). The integer types have many more
methods: pow, abs, abs_diff, signum, rem_euclid, div_ceil, isqrt (1.84), count_ones, leading_zeros,
rotate_left, to_be_bytes/from_le_bytes, ilog10, and MIN/MAX/BITS constants.
Converting strings to numbers
str::parse converts text to any type that implements FromStr, returning a Result because the text may be
invalid. Specify the target type with an annotation or the turbofish:
fn main() {
let year: u16 = "1965".parse().expect("a year");
let pages = "412".parse::<u32>().unwrap_or(0);
let bad = "12x".parse::<i32>();
println!("{year} {pages} {bad:?}"); // 1965 412 Err(ParseIntError { kind: InvalidDigit })
let hex = u8::from_str_radix("ff", 16);
println!("{hex:?}"); // Ok(255)
}
Source: std: str::parse
Floating-point numbers
f32 and f64 are IEEE 754 binary floating-point numbers; f64 is the default because on modern CPUs it is about as
fast as f32 and much more precise. Floats have the usual IEEE semantics: rounding error (0.1 + 0.2 != 0.3),
signed zero, infinities, and NaN ("not a number"), produced by operations such as 0.0 / 0.0.
NaN is not equal to anything, including itself. That is why floats implement PartialEq and PartialOrd but not
Eq, Ord or Hash: you cannot sort() a Vec<f64>, use floats as HashMap keys, or call max() on an iterator
of floats. For a total order (NaN sorts after positive infinity, and -0.0 < +0.0), use total_cmp:
fn main() {
let mut ratings = vec![4.5, f64::NAN, 3.0, 5.0, -0.0];
// ratings.sort(); // does not compile: f64 is not Ord (E0277)
ratings.sort_by(f64::total_cmp);
println!("{ratings:?}"); // [-0.0, 3.0, 4.5, 5.0, NaN]
let nan = f64::NAN;
assert_eq!(nan.partial_cmp(&nan), None); // NaN is unordered, even with itself
assert!(nan.is_nan());
assert!((0.1 + 0.2 - 0.3_f64).abs() < f64::EPSILON); // compare with a tolerance
let best = ratings.iter().copied().filter(|r| !r.is_nan()).fold(f64::MIN, f64::max);
println!("{best}"); // 5
}
Source: std: f64::total_cmp
Float-to-integer casts with as saturate and map NaN to 0 (300.7_f64 as u8 == 255, f64::NAN as i32 == 0), a
rule Rust defined in 1.45 so that the cast can never be undefined behaviour. For numerically careful code — NaN
policies, mul_add, compensated summation, the algebraic float methods added in 1.98 — see
Numbers & Numeric Computing.
Booleans
bool has the values true and false and is one byte in size. Conditions in if and while must be bool
exactly; there is no truthiness, so if count {} with an integer is a type error (E0308). The logical operators
&& and || short-circuit, ! negates, and &, |, ^ also work on bool without short-circuiting. Two handy
methods turn a condition into an Option: then_some(value) (eager) and then(|| value) (lazy):
fn main() {
let in_stock = true;
let label = in_stock.then_some("available");
let count = 3;
let note = (count > 2).then(|| format!("{count} copies"));
println!("{label:?} {note:?}"); // Some("available") Some("3 copies")
println!("{}", u8::from(in_stock)); // 1 -- bool converts losslessly to integers
}
Source: std: bool
Characters
char is a Unicode scalar value: any code point except the surrogates U+D800..=U+DFFF. It is always 4 bytes,
whatever the character. Literals use single quotes: 'a', 'é', '\n', '\u{1F4DA}' (📚). A char is not a
byte and not a "user-perceived character": 'e' followed by a combining accent is two chars, and Strings store
UTF-8 bytes, not chars. See Strings & Text.
fn main() {
let c = 'é';
println!("{} {} {}", c.is_alphabetic(), c.len_utf8(), c as u32); // true 2 233
println!("{:?}", 'a'.to_ascii_uppercase()); // 'A'
println!("{:?}", char::from_u32(0x1F4DA)); // Some('📚')
println!("{:?}", char::from_digit(7, 10)); // Some('7')
println!("{:?}", '9'.to_digit(10)); // Some(9)
}
Tuples and the unit type
A tuple groups a fixed number of values of possibly different types. Access elements by position (t.0,
t.1) or destructure them with a pattern. Tuples are handy for returning several values from a function; for
anything with more than two or three fields, or that crosses an API boundary, a struct with named fields is clearer.
fn split_isbn(isbn: &str) -> (&str, &str) {
isbn.split_at(3)
}
fn main() {
let book: (&str, u16, f32) = ("Dune", 1965, 4.6);
let (title, year, rating) = book; // destructuring
println!("{title} ({year}) {rating} -- first field again: {}", book.0);
let (prefix, rest) = split_isbn("9780441013593");
println!("{prefix} / {rest}");
let single = (5,); // a one-element tuple needs the comma; (5) is just 5 in parentheses
let unit = (); // the empty tuple: the unit type and its only value
println!("{single:?} {unit:?}");
}
Source: Rust by Example: Tuples
The empty tuple () is the unit type. It has exactly one value, also written (), and occupies no memory.
Functions without a return type return (), expressions ending in ; evaluate to (), and Result<(), E> means
"success carries no data".
Arrays
An array [T; N] holds exactly N values of type T inline (on the stack, or inside whatever contains it). The
length is part of the type: [u8; 4] and [u8; 5] are different types. Arrays are useful for fixed-size data
(checksums, lookup tables, coordinates); for a collection that grows, use Vec<T>.
fn checksum(digits: [u8; 13]) -> u32 {
digits.iter().enumerate().map(|(i, d)| u32::from(*d) * if i % 2 == 0 { 1 } else { 3 }).sum()
}
fn main() {
let weekdays = ["Mon", "Tue", "Wed", "Thu", "Fri"];
let zeros = [0u8; 16]; // [value; length]
println!("{} {} {}", weekdays[0], weekdays.len(), zeros.len());
let isbn = [9, 7, 8, 0, 4, 4, 1, 0, 1, 3, 5, 9, 3];
println!("valid: {}", checksum(isbn).is_multiple_of(10)); // is_multiple_of: 1.87
let [first, .., last] = weekdays; // array pattern
println!("{first}..{last}");
let doubled = [1, 2, 3].map(|n| n * 2); // arrays have `map`
println!("{doubled:?}");
}
Indexing is bounds-checked: weekdays[7] with a constant index is a compile-time error, and with a runtime index it
panics (index out of bounds: the len is 5 but the index is 7). Use get(i), which returns an Option, when the
index comes from input. Arrays implement Copy and Clone if T does, IntoIterator by value (since the 2021
edition), Debug, PartialEq, Default for any length, and coerce to slices (&[T]), which is how they get most of
their methods. The length can be a generic parameter — see
Generics & Const Generics.
The never type
!, the never type, is the type of an expression that never produces a value: panic!(), loop {} without
break, return, break, continue, and calls to functions declared -> ! such as std::process::exit. Because
it has no values, ! coerces to any type, which is what lets one match arm return a value while another bails out:
fn parse_year(text: &str) -> u16 {
match text.parse() {
Ok(year) => year, // u16
Err(_) => panic!("bad year: {text}"), // `!` coerces to u16
}
}
fn fail(code: i32) -> ! {
eprintln!("fatal error");
std::process::exit(code)
}
fn main() {
println!("{}", parse_year("1965"));
if std::env::args().count() > 5 {
fail(2);
}
}
Source: The Rust Reference: Never type
Using ! as a type in other positions (for example Result<T, !>) is still unstable; on stable, the standard library
uses the empty enum std::convert::Infallible for the same purpose.
Casting with as
as performs primitive conversions explicitly; Rust never converts numeric types implicitly, so let x: u64 =
some_u32; is a type error (E0308). The rules:
-
integer to wider integer: zero-extends (unsigned source) or sign-extends (signed source);
-
integer to narrower integer: truncates the high bits (
300u32 as u8 == 44); -
signed to unsigned of the same width: reinterprets the bits (
-1i8 as u8 == 255); -
float to integer: rounds toward zero and saturates; NaN becomes 0;
-
integer to float: rounds to the nearest representable value;
-
u8tochar,charto any integer,boolto integer; -
between raw pointer types, and between pointers and integers (see Unsafe Rust).
fn main() {
let big: u32 = 300;
println!("{}", big as u8); // 44 -- silent truncation
println!("{}", -1i8 as u8); // 255
println!("{}", 3.99_f64 as i32); // 3
println!("{}", -5.0_f64 as u8); // 0 -- saturates
println!("{}", 'A' as u32); // 65
println!("{}", 97u8 as char); // a
// Prefer lossless From and checked TryFrom for integer conversions:
let wide: u64 = u64::from(big);
let narrow = u8::try_from(big);
println!("{wide} {narrow:?}"); // 300 Err(TryFromIntError(PosOverflow))
}
Because as silently truncates, prefer From (for conversions that cannot lose information) and TryFrom (for
those that can) in new code; Clippy’s pedantic cast_possible_truncation and cast_sign_loss lints point out risky
as casts. Conversions between your own types are the subject of
Type Conversions & Coercions.
Type inference and type aliases
The compiler infers local types using the whole function body (a Hindley-Milner-style algorithm extended with trait resolution), so a variable’s type can be decided by a later use:
type Isbn13 = [u8; 13]; // an alias: just another name, not a new type
type Index = std::collections::HashMap<String, Vec<usize>>;
fn main() {
let mut titles = Vec::new(); // Vec<?> for now...
titles.extend(["Dune", "Emma"]); // ...now known to be Vec<&str>
let mut index = Index::new();
index.entry("dune".to_string()).or_default().push(0);
let digits: Isbn13 = [9, 7, 8, 0, 4, 4, 1, 0, 1, 3, 5, 9, 3];
println!("{titles:?} {index:?} {}", digits.len());
}
Source: Rust by Example: Aliasing
A type alias does not create a distinct type — Isbn13 and [u8; 13] are interchangeable — so it cannot enforce
invariants. For that, use a newtype (a tuple struct around the value), as shelf-core does for Isbn; see
Structs & Methods. Aliases are most useful for shortening
long generic types, and the standard library uses them for module-specific results such as io::Result<T> (an alias
for Result<T, io::Error>).
Operators
Rust’s operators are mostly familiar. Arithmetic (+ - * / %), comparison (== != < > <= >=), logical
(&& || !), bitwise (& | ^ ! << >>), compound assignment (+=, <<=, …), plus a few Rust-specific ones:
| Operator | Meaning |
|---|---|
|
borrow (create a reference) |
|
dereference |
|
propagate an error or |
|
ranges: |
|
primitive cast |
|
path separator ( |
|
function return type |
|
match arm |
|
macro invocation ( |
There is no +`/`--`, no ternary `? :` (use `if` as an expression), and `^+ is XOR, not exponentiation (use
pow). Arithmetic operands must have the same type — 1u8 + 1u32 is error E0308 — and operators are defined by
the traits in std::ops, so your own types can overload them. The complete table is in The Book’s Appendix B.
Common errors
| Error | Typical cause |
|---|---|
E0308 mismatched types |
mixing integer widths ( |
E0277 trait bound not satisfied |
|
E0282 type annotations needed |
|
|
|
|
overflow the compiler can see at compile time, such as
|
Where the books are outdated
The books consulted for this section predate several numeric additions: the strict_* arithmetic methods (1.91),
isqrt (1.84), size_of and align_of in the prelude (1.80), and the algebraic float methods (1.98). Numerical
material in the Crossley et al. book was used only to check topic coverage; the float semantics described here come
from the Reference and the standard library documentation.
References
-
The Rust Programming Language: Appendix B — Operators and Symbols
-
Rust by Example: Primitives (literals, tuples, arrays)
-
Rust by Example: Types (casting, literals, inference, aliasing)