Rust Data Types

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.