Integer Types
An integer type stores a whole number, with no fractional part. Rust has twelve built-in integer types. They differ in two properties: the number of bits used for storage, and whether the type can hold negative values.
| Bits | Signed | Unsigned |
|---|---|---|
| 8 | i8 |
u8 |
| 16 | i16 |
u16 |
| 32 | i32 |
u32 |
| 64 | i64 |
u64 |
| 128 | i128 |
u128 |
| pointer width | isize |
usize |
The letter gives the signedness, i for signed and u for unsigned, and the number gives the width in bits. The pointer-width types are 64 bits on a 64-bit target and 32 bits on a 32-bit target.
Ranges
A type with n bits has 2^n distinct values. An unsigned type uses them for 0 to 2^n - 1. A signed type splits them between negative and non-negative numbers: -2^(n-1) to 2^(n-1) - 1.
| Type | Bits | Minimum | Maximum |
|---|---|---|---|
i8 |
8 | -128 | 127 |
u8 |
8 | 0 | 255 |
i16 |
16 | -32,768 | 32,767 |
u16 |
16 | 0 | 65,535 |
i32 |
32 | -2,147,483,648 | 2,147,483,647 |
u32 |
32 | 0 | 4,294,967,295 |
i64 |
64 | -9,223,372,036,854,775,808 | 9,223,372,036,854,775,807 |
u64 |
64 | 0 | 18,446,744,073,709,551,615 |
i128 |
128 | -2^127 | 2^127 - 1 |
u128 |
128 | 0 | 2^128 - 1 |
isize |
64 on a 64-bit target | same as i64 |
same as i64 |
usize |
64 on a 64-bit target | 0 | same as u64 |
The exact limits are available as associated constants on every integer type, together with BITS:
fn main() {
println!("{} {}", i8::MIN, i8::MAX);
println!("{} {}", u32::MIN, u32::MAX);
println!("{} {}", i64::MIN, i64::MAX);
println!("{} {}", i128::MIN, i128::MAX);
println!("{} {}", u128::MIN, u128::MAX);
println!("{} {} {}", u8::BITS, i128::BITS, usize::BITS);
}
-128 127
0 4294967295
-9223372036854775808 9223372036854775807
-170141183460469231731687303715884105728 170141183460469231731687303715884105727
0 340282366920938463463374607431768211455
8 128 64
The last line is from a 64-bit machine, where usize::BITS is 64.
The Default Integer Type
An integer literal whose type is not decided by anything else in the program becomes i32:
fn main() {
let x = 42;
println!("{}", std::any::type_name_of_val(&x));
}
i32
i32 is the default because it is a good general-purpose size: large enough for most everyday numbers and fast on both 32-bit and 64-bit processors.
Integer Literals
An integer literal can be written in four bases, with an optional type suffix and optional underscores.
| Form | Prefix | Example | Value |
|---|---|---|---|
| Decimal | none | 98_222 |
98222 |
| Hexadecimal | 0x |
0xff |
255 |
| Octal | 0o |
0o77 |
63 |
| Binary | 0b |
0b1111_0000 |
240 |
| Byte | b'...' |
b'A' |
65, type u8 |
fn main() {
let decimal = 1_000_000;
let hex = 0xFF;
let octal = 0o755;
let binary = 0b1010_1010;
let byte = b'A';
let suffixed = 42u16;
let underscore_suffix = 1_000_i64;
println!("{decimal} {hex} {octal} {binary} {byte} {suffixed} {underscore_suffix}");
}
1000000 255 493 170 65 42 1000
Underscores can appear anywhere after the first digit and are ignored. A suffix such as u16 or i64 sets the type of the literal. A literal with no suffix takes the type that its surroundings require, and falls back to i32 if nothing does.
Literals That Do Not Fit
The compiler checks every literal against the range of its type. A literal that does not fit is a compile-time error, because the overflowing_literals lint is set to deny:
// ❌ does not compile
fn main() {
let byte: u8 = 256;
println!("{byte}");
}
error: literal out of range for `u8`
--> main.rs:3:20
|
3 | let byte: u8 = 256;
| ^^^
|
= note: the literal `256` does not fit into the type `u8` whose range is `0..=255`
= note: `#[deny(overflowing_literals)]` on by default
error: aborting due to 1 previous error
// ❌ does not compile
fn main() {
let value: i8 = 128;
println!("{value}");
}
error: literal out of range for `i8`
--> main.rs:3:21
|
3 | let value: i8 = 128;
| ^^^
|
= note: the literal `128` does not fit into the type `i8` whose range is `-128..=127`
= help: consider using the type `u8` instead
= note: `#[deny(overflowing_literals)]` on by default
error: aborting due to 1 previous error
The smallest value of a signed type, such as -128i8, is accepted, since the negation is part of the literal check.
Formatting Integers
println! and format! can print an integer in any base, with padding and an explicit sign:
| Specifier | Meaning | 5u8 prints as |
|---|---|---|
{} |
Decimal | 5 |
{:b} |
Binary | 101 |
{:o} |
Octal | 5 |
{:x} |
Lowercase hexadecimal | 5 |
{:X} |
Uppercase hexadecimal | 5 |
{:#b} {:#x} {:#o} |
With a base prefix | 0b101 0x5 0o5 |
{:08b} |
Zero-padded to width 8 | 00000101 |
fn main() {
let n: u8 = 5;
println!("{n:b} {n:08b} {n:o} {n:x} {n:X} {n:#b} {n:#x} {n:#o}");
let big = 255u32;
println!("{big:#010x} {big:>6} {big:<6}| {big:^7}| {big:+}");
let s: i8 = -1;
println!("{s:b} {s:x}");
println!("{:b} {:x}", -2i16, -2i16);
}
101 00000101 5 5 5 0b101 0x5 0o5
0x000000ff 255 255 | 255 | +255
11111111 ff
1111111111111110 fffe
Negative numbers print in binary and hexadecimal as their raw two's complement bit pattern at the width of the type, so -1i8 is 11111111.
Parsing Integers
str::parse::<T>() converts decimal text, and from_str_radix converts text in a given base from 2 to 36. Both return a Result, because the text may not be a valid number for that type:
fn main() {
println!("{}", i32::from_str_radix("ff", 16).unwrap());
println!("{}", i32::from_str_radix("-1010", 2).unwrap());
println!("{}", u8::from_str_radix("zz", 36).is_err());
println!("{:?}", "300".parse::<u8>());
println!("{:?}", "-5".parse::<u8>());
println!("{:?}", "12a".parse::<i32>());
println!("{:?}", "".parse::<i32>());
println!("{:?}", "+7".parse::<i32>());
}
255
-10
true
Err(ParseIntError { kind: PosOverflow })
Err(ParseIntError { kind: InvalidDigit })
Err(ParseIntError { kind: InvalidDigit })
Err(ParseIntError { kind: Empty })
Ok(7)
Parsing rejects values outside the range of the target type (PosOverflow), unexpected characters (InvalidDigit), and empty input (Empty). A leading + is accepted, and a leading - is an error for unsigned types.
usize and isize
usize and isize are as wide as a pointer on the target machine. usize is the type Rust uses for anything that is a size or a position in memory:
- Indexing:
vec[i],array[i], and slice ranges. - Lengths and capacities:
len(),capacity(). - Counts of items, such as the number returned by
Iterator::count. - The result of
size_of::<T>().
Because it matches the machine, a usize can index any in-memory collection. Indexing with another integer type is an error:
// ❌ does not compile
fn main() {
let values = vec![10, 20, 30];
let index: i32 = 1;
println!("{}", values[index]);
}
error[E0277]: the type `[{integer}]` cannot be indexed by `i32`
--> main.rs:5:27
|
5 | println!("{}", values[index]);
| ^^^^^ slice indices are of type `usize` or ranges of `usize`
|
= help: the trait `SliceIndex<[{integer}]>` is not implemented for `i32`
help: `usize` implements trait `SliceIndex<T>`
--> /rustc/b940084d7eb6a299eb4bfeb8e34901bc051e7ac4/library/core/src/slice/index.rs:179:0
|
= note: `SliceIndex<[T]>`
--> /rustc/b940084d7eb6a299eb4bfeb8e34901bc051e7ac4/library/core/src/bstr/traits.rs:197:0
|
= note: `SliceIndex<ByteStr>`
= note: required for `Vec<{integer}>` to implement `Index<i32>`
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0277`.
The fix is to hold the index as a usize, or to convert it with as usize or usize::try_from(...):
fn main() {
let values = vec![10, 20, 30];
let index: usize = 1;
println!("{}", values[index]);
println!("{}", values.len());
let signed: i32 = 2;
println!("{}", values[signed as usize]);
}
20
3
30
Because the size of usize depends on the target, it should not be used for data that must have the same width on every machine, such as values in a file format or a network protocol. Use u32, u64, and the other fixed-width types for those.
Arithmetic
All integer types support +, -, *, /, and %. Both operands must have the same type.
Integer division truncates toward zero, and % returns the remainder with the sign of the left operand:
fn main() {
println!("{} {}", 7 / 2, -7 / 2);
println!("{} {}", 7 % 3, -7 % 3);
println!("{} {}", (-7i32).rem_euclid(3), (-7i32).div_euclid(3));
}
3 -3
1 -1
2 -3
rem_euclid always returns a non-negative remainder when the divisor is positive, and div_euclid is the matching quotient. They are the right choice for wrap-around indexing and modular arithmetic.
Division by Zero
Dividing an integer by zero is never allowed. If the compiler can see the zero, it rejects the program:
// ❌ does not compile
fn main() {
let divisor = 0;
println!("{}", 10 / divisor);
}
error: this operation will panic at runtime
--> main.rs:4:20
|
4 | println!("{}", 10 / divisor);
| ^^^^^^^^^^^^ attempt to divide `10_i32` by zero
|
= note: `#[deny(unconditional_panic)]` on by default
error: aborting due to 1 previous error
If the divisor is only known at run time, the program panics, in both debug and release builds:
fn main() {
let divisor: i32 = std::env::args().count() as i32 - 1;
println!("{}", 10 / divisor);
}
thread 'main' panicked at main.rs:5:20:
attempt to divide by zero
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
checked_div returns None instead of panicking when the divisor is zero.
Results That Do Not Fit
The result of an operation can leave the range of its type, for example i32::MAX + 1. This is overflow. In a debug build, Rust panics with attempt to add with overflow. In a release build, the value wraps around using two's complement arithmetic, and no error is raised. The standard library has four families of methods for stating the behavior explicitly:
| Method family | Behavior on overflow | Example 200u8 + 100 |
|---|---|---|
checked_* |
Returns None |
200u8.checked_add(100) is None |
wrapping_* |
Wraps around | 200u8.wrapping_add(100) is 44 |
saturating_* |
Clamps to MIN or MAX |
200u8.saturating_add(100) is 255 |
overflowing_* |
Returns the wrapped value and a flag | 200u8.overflowing_add(100) is (44, true) |
fn main() {
println!("{:?} {:?}", 200u8.checked_add(100), 200u8.checked_add(50));
println!("{} {}", 200u8.wrapping_add(100), 200u8.saturating_add(100));
println!("{:?}", 200u8.overflowing_add(100));
}
None Some(250)
44 255
(44, true)
Methods on Integers
Integer types carry many methods. These are some of the most used:
| Method | Description | Example | Result |
|---|---|---|---|
pow(exp) |
Raise to a power | 7i32.pow(3) |
343 |
abs() |
Absolute value (signed only) | (-5i32).abs() |
5 |
abs_diff(other) |
Distance between two values, as an unsigned type | 10u32.abs_diff(25) |
15 |
signum() |
-1, 0, or 1 (signed only) |
(-12i32).signum() |
-1 |
min(other), max(other) |
Smaller or larger of two values | 3.min(9) |
3 |
clamp(lo, hi) |
Restrict to a range | 10.clamp(0, 5) |
5 |
isqrt() |
Integer square root, rounded down | 99u32.isqrt() |
9 |
ilog2(), ilog10() |
Integer logarithm, rounded down | 1024u32.ilog2() |
10 |
is_power_of_two() |
Unsigned only | 64u32.is_power_of_two() |
true |
count_ones() |
Number of 1 bits | 0b1011_0000u8.count_ones() |
3 |
leading_zeros() |
Zero bits before the highest 1 bit | 0b0000_1000u8.leading_zeros() |
4 |
trailing_zeros() |
Zero bits after the lowest 1 bit | 0b0000_1000u8.trailing_zeros() |
3 |
fn main() {
println!("{}", 7i32.pow(3));
println!("{}", (-5i32).abs());
println!("{}", 10u32.abs_diff(25));
println!("{}", (-12i32).signum());
println!("{} {}", 3.min(9), 3.max(9));
println!("{}", 10.clamp(0, 5));
println!("{}", 99u32.isqrt());
println!("{} {}", 1000u32.ilog10(), 1024u32.ilog2());
println!("{} {}", 17u32.is_power_of_two(), 64u32.is_power_of_two());
println!("{}", 0b1011_0000u8.count_ones());
println!("{}", 0b0000_1000u8.leading_zeros());
println!("{}", 0b0000_1000u8.trailing_zeros());
}
343
5
15
-1
3 9
5
9
3 10
false true
3
4
3
Bit Operations
Integers support the bitwise operators & (AND), | (OR), ^ (XOR), ! (NOT), and the shifts << and >>. Several methods work on whole bit patterns:
fn main() {
let x = 0b1100u8;
let y = 0b1010u8;
println!("{:04b} {:04b} {:04b} {:08b}", x & y, x | y, x ^ y, !x);
println!("{} {}", x << 2, x >> 2);
}
1000 1110 0110 11110011
48 3
Byte Representation
A multi-byte integer is stored as a sequence of bytes. Endianness is the order of those bytes: big-endian puts the most significant byte first and little-endian puts it last. Integers convert to and from byte arrays in any order:
fn main() {
let x: u32 = 0x1234_5678;
println!("{:02x?}", x.to_be_bytes());
println!("{:02x?}", x.to_le_bytes());
println!("{:02x?}", x.to_ne_bytes());
println!("{:#x}", u32::from_be_bytes([0x12, 0x34, 0x56, 0x78]));
println!("{:#x}", x.swap_bytes());
println!("{:#x}", x.rotate_left(8));
println!("{:#x}", x.reverse_bits());
}
[12, 34, 56, 78]
[78, 56, 34, 12]
[78, 56, 34, 12]
0x12345678
0x78563412
0x34567812
0x1e6a2c48
to_ne_bytes uses the native order of the machine, which is little-endian on this one, so it matches to_le_bytes. Network protocols use big-endian order, which is why to_be_bytes and from_be_bytes appear in networking code.
x = 0x12345678
Address: +0 +1 +2 +3
Big-endian: 12 34 56 78
Little-endian:78 56 34 12
Wide Integers
i128 and u128 hold values far beyond 64 bits. They are used for exact arithmetic on very large numbers, such as factorials or big identifiers. They are slower than 64-bit types on most CPUs, because arithmetic on them is performed with multiple machine instructions:
fn main() {
let a = 1_000_000_i64 * 1_000_000;
println!("{a}");
let wide = 1u128 << 100;
println!("{wide}");
println!("{}", i64::MAX as i128 * 4);
}
1000000000000
1267650600228229401496703205376
36893488147419103228
Choosing an Integer Type
| Need | Use |
|---|---|
| No special requirement | i32 |
| An index, a length, or a count of items in memory | usize |
| Raw bytes, or a value known to fit in 0 to 255 | u8 |
| A quantity that can never be negative and must be large | u64 |
| Large totals, timestamps, file sizes | i64 or u64 |
| A value that must have the same width on every platform | The fixed-width type: u16, u32, u64, ... |
| Exact arithmetic beyond 64 bits | i128 or u128 |
| Storing millions of small values compactly | The smallest type that fits: u8, u16, ... |