Scalar Types
A scalar type represents a single value. Rust has four primitive scalar types: integers, floating-point numbers, booleans, and characters. They are the smallest building blocks of every other type, have a fixed size known at compile time, and are built into the language, so they need no use or import.
| Kind | Types | Holds |
|---|---|---|
| Integer | i8 i16 i32 i64 i128 isize and u8 u16 u32 u64 u128 usize |
A whole number |
| Floating-point | f32 f64 |
A number with a fractional part |
| Boolean | bool |
true or false |
| Character | char |
One Unicode scalar value |
A compound type groups several values into one, as tuples and arrays do. A scalar holds exactly one.
Overview of the Scalar Types
| Type | Size | Alignment | Range or values |
|---|---|---|---|
i8 |
1 byte | 1 | -128 to 127 |
u8 |
1 byte | 1 | 0 to 255 |
i16 |
2 bytes | 2 | -32768 to 32767 |
u16 |
2 bytes | 2 | 0 to 65535 |
i32 |
4 bytes | 4 | -2147483648 to 2147483647 |
u32 |
4 bytes | 4 | 0 to 4294967295 |
i64 |
8 bytes | 8 | -9223372036854775808 to 9223372036854775807 |
u64 |
8 bytes | 8 | 0 to 18446744073709551615 |
i128 |
16 bytes | 16 | -2^127 to 2^127 - 1 |
u128 |
16 bytes | 16 | 0 to 2^128 - 1 |
isize |
pointer width | pointer width | same range as i64 on a 64-bit target |
usize |
pointer width | pointer width | same range as u64 on a 64-bit target |
f32 |
4 bytes | 4 | about ±3.4028235e38, IEEE 754 binary32 |
f64 |
8 bytes | 8 | about ±1.7976931348623157e308, IEEE 754 binary64 |
bool |
1 byte | 1 | false, true |
char |
4 bytes | 4 | '\0' to '\u{10FFFF}', excluding surrogates |
The sizes and alignments can be read from the compiler:
use std::mem::{align_of, size_of};
fn main() {
println!("{:<6} {:>5} {:>5}", "type", "size", "align");
println!("{:<6} {:>5} {:>5}", "i8", size_of::<i8>(), align_of::<i8>());
println!("{:<6} {:>5} {:>5}", "i16", size_of::<i16>(), align_of::<i16>());
println!("{:<6} {:>5} {:>5}", "i32", size_of::<i32>(), align_of::<i32>());
println!("{:<6} {:>5} {:>5}", "i64", size_of::<i64>(), align_of::<i64>());
println!("{:<6} {:>5} {:>5}", "i128", size_of::<i128>(), align_of::<i128>());
println!("{:<6} {:>5} {:>5}", "isize", size_of::<isize>(), align_of::<isize>());
println!("{:<6} {:>5} {:>5}", "f32", size_of::<f32>(), align_of::<f32>());
println!("{:<6} {:>5} {:>5}", "f64", size_of::<f64>(), align_of::<f64>());
println!("{:<6} {:>5} {:>5}", "bool", size_of::<bool>(), align_of::<bool>());
println!("{:<6} {:>5} {:>5}", "char", size_of::<char>(), align_of::<char>());
}
type size align
i8 1 1
i16 2 2
i32 4 4
i64 8 8
i128 16 16
isize 8 8
f32 4 4
f64 8 8
bool 1 1
char 4 4
The output above is from a 64-bit macOS machine. isize and usize are 8 bytes here and would be 4 bytes on a 32-bit target. The alignment of i128 and u128 is 16 on current stable Rust for this target. Alignment is a platform detail and is not the same on every target.
Every scalar has constants that describe its limits, such as MIN and MAX:
fn main() {
println!("i8: {} to {}", i8::MIN, i8::MAX);
println!("u8: {} to {}", u8::MIN, u8::MAX);
println!("i32: {} to {}", i32::MIN, i32::MAX);
println!("u64: {} to {}", u64::MIN, u64::MAX);
println!("i128: {} to {}", i128::MIN, i128::MAX);
println!("u128: {} to {}", u128::MIN, u128::MAX);
println!("usize: {} to {}", usize::MIN, usize::MAX);
println!("f32: {:e} to {:e}", f32::MIN, f32::MAX);
println!("f64: {:e} to {:e}", f64::MIN, f64::MAX);
println!(
"char: {:?} to {:?} ({} to {})",
char::MIN,
char::MAX,
char::MIN as u32,
char::MAX as u32
);
}
i8: -128 to 127
u8: 0 to 255
i32: -2147483648 to 2147483647
u64: 0 to 18446744073709551615
i128: -170141183460469231731687303715884105728 to 170141183460469231731687303715884105727
u128: 0 to 340282366920938463463374607431768211455
usize: 0 to 18446744073709551615
f32: -3.4028235e38 to 3.4028235e38
f64: -1.7976931348623157e308 to 1.7976931348623157e308
char: '\0' to '\u{10ffff}' (0 to 1114111)
For floats, MIN is the most negative finite value. The smallest positive value is MIN_POSITIVE.
Scalar Literals
Each scalar type has a literal syntax.
| Literal | Example | Type if nothing else decides |
|---|---|---|
| Decimal integer | 98_222 |
i32 |
| Hexadecimal | 0xff |
i32 |
| Octal | 0o77 |
i32 |
| Binary | 0b1111_0000 |
i32 |
| Byte | b'A' |
u8 |
| Integer with suffix | 42u16, 7_i64 |
the suffix type |
| Float | 2.5, 1e3, 2.5E-3, 7. |
f64 |
| Float with suffix | 1_000.5_f32 |
the suffix type |
| Boolean | true, false |
bool |
| Character | 'R', '\n', '\u{2764}' |
char |
fn main() {
let decimal = 98_222;
let hex = 0xff;
let octal = 0o77;
let binary = 0b1111_0000;
let byte = b'A';
let typed = 42u16;
println!("{decimal} {hex} {octal} {binary} {byte} {typed}");
let a = 2.5;
let b = 1e3;
let c = 2.5E-3;
let d = 1_000.5_f32;
let e = 7.;
println!("{a} {b} {c} {d} {e}");
let t = true;
let ch = 'R';
let heart = '\u{2764}';
let newline = '\n';
println!("{t} {ch} {heart} {newline:?}");
}
98222 255 63 240 65 42
2.5 1000 0.0025 1000.5 7
true R ❤ '\n'
Underscores in numeric literals are ignored and exist only for readability. A float literal needs a decimal point or an exponent, so 1e3 and 7. are floats, while 7 is an integer.
Storage
Scalars are stored directly in the place that holds them: in a stack variable, a struct field, or an array slot. There is no pointer to follow and no heap allocation.
let count: i32 = 42;
let ratio: f64 = 0.5;
let ready: bool = true;
let letter: char = 'R';
Stack
+--------------------+
| count i32 42 | 4 bytes
+--------------------+
| ratio f64 0.5 | 8 bytes
+--------------------+
| ready bool true | 1 byte
+--------------------+
| letter char 'R' | 4 bytes
+--------------------+
Compilers may insert padding or keep a variable in a CPU register instead of memory. The diagram shows the logical layout.
Copy Semantics
Scalars implement the Copy trait. Assigning one scalar to another variable, or passing it to a function, duplicates the bits, and the original stays usable. This differs from types such as String, where assignment moves ownership:
fn main() {
let a = 42;
let b = a;
println!("{a} {b}");
let s1 = String::from("hi");
let s2 = s1.clone();
println!("{s1} {s2}");
}
42 42
hi hi
a is still valid after let b = a;. A String must be cloned explicitly to keep both values.
Operations on Scalars
Each scalar family supports a different set of operators.
| Operators | Integers | Floats | bool |
char |
|---|---|---|---|---|
Arithmetic + - * / % |
yes | yes | no | no |
Unary negation - |
signed only | yes | no | no |
Comparison == != < > <= >= |
yes | yes | yes | yes |
Bitwise & | ^ |
yes | no | yes | no |
Shifts << >> |
yes | no | no | no |
Bitwise or logical NOT ! |
yes (bitwise) | no | yes (logical) | no |
Short-circuit && || |
no | no | yes | no |
fn main() {
let x = 7;
println!("{} {} {} {}", x + 2, x - 2, x * 2, x / 2);
println!("{} {}", x % 3, -x);
println!("{} {} {}", x & 3, x | 8, x ^ 5);
println!("{} {} {}", x << 2, x >> 1, !x);
let f = 7.0_f64;
println!("{} {} {}", f / 2.0, f % 3.0, -f);
println!("{} {}", true && false, true || false);
println!("{} {}", !true, true ^ true);
}
9 5 14 3
1 -7
3 15 2
28 3 -8
3.5 1 -7
false true
false false
Integer division truncates toward zero, so 7 / 2 is 3. Float division keeps the fraction, so 7.0 / 2.0 is 3.5.
Mixing scalar types in one operation, such as an i32 with an i64, or an integer with a float, is a compile error. Values must be converted explicitly first.
Default Values
Each scalar implements Default:
| Type | Default::default() |
|---|---|
| integers | 0 |
| floats | 0.0 |
bool |
false |
char |
'\0' |
fn main() {
println!("{}", i32::default());
println!("{}", f64::default());
println!("{}", bool::default());
println!("{:?}", char::default());
}
0
0
false
'\0'
Ordering and Equality
All scalars can be compared with == and <. Characters compare by their Unicode code point and false is less than true:
fn main() {
println!("{}", 'a' < 'b');
println!("{}", false < true);
println!("{}", 2.5 < 3.0);
}
true
true
true
Integers, bool, and char implement the full set of comparison traits: PartialEq, Eq, PartialOrd, Ord, and also Hash. Floats implement only PartialEq and PartialOrd. The reason is NaN ("not a number"), which is not equal to anything, including itself, so floats do not form a total order and cannot be hashed consistently. Code that requires Ord, Eq, or Hash does not accept floats:
// ❌ does not compile
fn main() {
let mut values: Vec<f64> = vec![2.5, 1.0, 3.2];
values.sort();
println!("{values:?}");
}
error[E0277]: the trait bound `f64: Ord` is not satisfied
--> main.rs:4:12
|
4 | values.sort();
| ^^^^ the trait `Ord` is not implemented for `f64`
|
= help: the following other types implement trait `Ord`:
i128
i16
i32
i64
i8
isize
u128
u16
and 4 others
note: required by a bound in `slice::<impl [T]>::sort`
--> /rustc/b940084d7eb6a299eb4bfeb8e34901bc051e7ac4/library/alloc/src/slice.rs:131:4
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0277`.
A HashSet<f64> is rejected for the same reason: f64 implements neither Eq nor Hash. To sort floats, supply a comparison explicitly. f64::total_cmp defines a total order that places NaN at an end:
fn main() {
let mut values: Vec<f64> = vec![2.5, 1.0, 3.2];
values.sort_by(|a, b| a.total_cmp(b));
println!("{values:?}");
}
[1.0, 2.5, 3.2]
Scalars and Other Primitive Types
Scalars are a subset of Rust's primitive types, which also include str, tuples, arrays, slices, references, raw pointers, function pointers, and the unit and never types. Those are not scalars: each is either a group of values or a reference to other data rather than a single value.