Rust Data Types

Signed and Unsigned Integers

Every integer type in Rust belongs to one of two families: signed integers (i8, i16, i32, i64, i128, isize), which can represent negative, zero, and positive whole numbers, and unsigned integers (u8, u16, u32, u64, u128, usize), which can represent only zero and positive whole numbers. Both families occupy the exact same bit widths in memory, differing only in how the processor and compiler interpret their bit patterns.

Paired Bit Widths and Value Ranges

For every bit width $n$, an unsigned integer uses all $n$ bits for magnitude, yielding a range of $0$ to $2^n - 1$. A signed integer reserves one bit for the sign, splitting the $2^n$ possible bit patterns evenly between negative values ($-2^{n-1}$ to $-1$) and non-negative values ($0$ to $2^{n-1} - 1$).

Bit Width Signed Type Signed Range (MIN..=MAX) Unsigned Type Unsigned Range (MIN..=MAX)
8-bit i8 -128 to 127 u8 0 to 255
16-bit i16 -32_768 to 32_767 u16 0 to 65_535
32-bit i32 -2_147_483_648 to 2_147_483_647 u32 0 to 4_294_967_295
64-bit i64 -9.22 × 10¹⁸ to 9.22 × 10¹⁸ u64 0 to 1.84 × 10¹⁹
128-bit i128 -1.70 × 10³⁸ to 1.70 × 10³⁸ u128 0 to 3.40 × 10³⁸
Pointer-sized isize -2³¹..2³¹-1 (32-bit) or -2⁶³..2⁶³-1 (64-bit) usize 0..2³²-1 (32-bit) or 0..2⁶⁴-1 (64-bit)
fn main() {
    println!("i8:  {} to {}", i8::MIN, i8::MAX);
    println!("u8:  {} to {}", u8::MIN, u8::MAX);
    println!("i16: {} to {}", i16::MIN, i16::MAX);
    println!("u16: {} to {}", u16::MIN, u16::MAX);
}
i8:  -128 to 127
u8:  0 to 255
i16: -32768 to 32767
u16: 0 to 65535

Binary Representation: Magnitude vs Two's Complement

Unsigned integers use pure base-2 positional notation. Signed integers in Rust are guaranteed by the language specification to use two's complement representation on all platforms.

In an $n$-bit two's complement integer, the most significant bit (MSB, bit $n-1$) has a negative weight of $-2^{n-1}$, while the remaining $n-1$ bits have positive weights ($2^{n-2}$ down to $2^0$):

8-bit Unsigned (u8): all 8 bits carry positive weight
+------+------+------+------+------+------+------+------+
| +128 |  +64 |  +32 |  +16 |   +8 |   +4 |   +2 |   +1 |
+------+------+------+------+------+------+------+------+
|   1  |   1  |   1  |   1  |   1  |   1  |   1  |   1  |  = 128 + 64 + ... + 1 = 255
+------+------+------+------+------+------+------+------+

8-bit Signed (i8): MSB carries negative weight (-128)
+------+------+------+------+------+------+------+------+
| -128 |  +64 |  +32 |  +16 |   +8 |   +4 |   +2 |   +1 |
+------+------+------+------+------+------+------+------+
|   1  |   1  |   1  |   1  |   1  |   1  |   1  |   1  |  = -128 + 127 = -1
+------+------+------+------+------+------+------+------+
|   1  |   0  |   0  |   0  |   0  |   0  |   0  |   0  |  = -128 + 0   = -128 (i8::MIN)
+------+------+------+------+------+------+------+------+
|   0  |   1  |   1  |   1  |   1  |   1  |   1  |   1  |  = 0 + 127    = +127 (i8::MAX)
+------+------+------+------+------+------+------+------+

For bit patterns from 0b0000_0000 (0) through 0b0111_1111 (127), u8 and i8 have identical binary representations and numeric values. Only bit patterns with the MSB set to 1 (0b1000_0000 through 0b1111_1111) differ in meaning:

Binary Pattern Hex Value as u8 Value as i8
0b0000_0000 0x00 0 0
0b0000_0001 0x01 1 1
0b0111_1111 0x7F 127 127 (i8::MAX)
0b1000_0000 0x80 128 -128 (i8::MIN)
0b1000_0001 0x81 129 -127
0b1111_1110 0xFE 254 -2
0b1111_1111 0xFF 255 (u8::MAX) -1

Two's complement has no negative zero (-0), which means every signed type has one more negative number than positive numbers: |MIN| == MAX + 1.

The Asymmetry of MIN and unsigned_abs

Because a signed integer's minimum value has no positive counterpart of the same signed type (for example, +128 cannot fit in i8), negating MIN or calling .abs() on MIN overflows:

fn main() {
    let min_val: i8 = i8::MIN; // -128

    // Calling `min_val.abs()` or `-min_val` panics in debug mode!
    // Instead, `.unsigned_abs()` returns the magnitude as the matching unsigned type (`u8`):
    let magnitude: u8 = min_val.unsigned_abs();
    println!("|{min_val}| as u8 = {magnitude}");

    let checked: Option<i8> = min_val.checked_abs();
    println!("checked_abs of {min_val} = {checked:?}");
}
|-128| as u8 = 128
checked_abs of -128 = None

unsigned_abs() is guaranteed never to overflow for any signed value, including MIN, because the magnitude of iN::MIN ($2^{n-1}$) always fits within uN (whose maximum is $2^n - 1$).

Sign-Specific Operations and Methods

Signed types support the unary negation operator (-) and sign-inspection methods that are not defined on unsigned types:

Operation / Method Signed (i8..isize) Unsigned (u8..usize) Description
-x Valid Compile error (E0600) Negates the value
x.abs() Returns signed type Not defined Magnitude as same signed type (panics on MIN in debug)
x.unsigned_abs() Returns unsigned counterpart Not defined Magnitude as matching unsigned type (never panics)
x.signum() Returns -1, 0, or 1 Not defined Sign indicator of the integer
x.is_negative() true if x < 0 Not defined Tests whether the sign bit is set
x.is_positive() true if x > 0 Not defined Tests whether the value is strictly greater than zero
fn main() {
    let delta: i32 = -42;

    println!("negated:     {}", -delta);
    println!("abs:         {}", delta.abs());
    println!("signum:      {}", delta.signum());
    println!("is_negative: {}", delta.is_negative());
    println!("is_positive: {}", delta.is_positive());
}
negated:     42
abs:         42
signum:      -1
is_negative: true
is_positive: false

Applying the unary - operator to an unsigned integer fails at compile time:

// ❌ does not compile
fn main() {
    let count: u32 = 10;
    let negated = -count;
}
error[E0600]: cannot apply unary operator `-` to type `u32`
 --> src/main.rs:3:19
  |
3 |     let negated = -count;
  |                   ^^^^^^ cannot apply unary operator `-`
  |
  = note: unsigned values cannot be negated

error: aborting due to 1 previous error

Choosing Between Signed and Unsigned Types

Use Case Recommended Family Typical Type Reason
Array, slice, and Vec indexing Unsigned usize Memory addresses and collection lengths are never negative
Object counts and capacities Unsigned usize, u32, u64 The type itself documents and enforces the >= 0 invariant
Raw binary data, network packets, UTF-8 bytes Unsigned u8 Bytes represent raw 8-bit patterns (0x00..0xFF), not signed quantities
Bitmasks, bitwise shifts, and flags Unsigned u32, u64, u128 Avoids sign-extension behavior during right shifts (>>)
General arithmetic, loops, and deltas Signed i32, i64 Subtraction (a - b) naturally produces negative results without underflow
Pointer offsets and relative distances Signed isize Represents backward (negative) or forward (positive) memory offsets

Right Shift (>>) Behavior Difference

The right-shift operator (>>) behaves differently on signed and unsigned integers:

  • Unsigned (u8..usize): Performs a logical right shift, filling vacated high-order bits with 0.
  • Signed (i8..isize): Performs an arithmetic right shift, filling vacated high-order bits with copies of the sign bit (1 for negative numbers, 0 for non-negative numbers) to preserve the number's sign.
fn main() {
    let unsigned_byte: u8 = 0b1000_0000; // 128
    let signed_byte: i8 = -128;          // 0b1000_0000 in two's complement

    println!("u8 >> 2: {:08b} ({})", unsigned_byte >> 2, unsigned_byte >> 2);
    println!("i8 >> 2: {:08b} ({})", (signed_byte >> 2) as u8, signed_byte >> 2);
}
u8 >> 2: 00100000 (32)
i8 >> 2: 11100000 (-32)

Mixing Signed and Unsigned Values

Unlike C and C++, which silently convert signed integers to unsigned integers when both appear in the same expression (causing -1 < 1u to evaluate to false in C), Rust rejects any binary arithmetic or comparison between signed and unsigned types:

// ❌ does not compile
fn main() {
    let signed: i32 = -5;
    let unsigned: u32 = 10;

    if signed < unsigned {
        println!("smaller");
    }
}
error[E0308]: mismatched types
 --> src/main.rs:5:17
  |
5 |     if signed < unsigned {
  |                 ^^^^^^^^ expected `i32`, found `u32`
  |
help: you can convert a `u32` to an `i32` and panic if the converted value doesn't fit
  |
5 |     if signed < unsigned.try_into().unwrap() {
  |                         ++++++++++++++++++++

error: aborting due to 1 previous error

Safe Mixed-Sign Arithmetic Methods

When you need to add an isize offset to a usize index (or add a signed delta to any unsigned coordinate), standard library methods perform the mixed-sign operation directly without lossy casts:

fn main() {
    let index: usize = 10;
    let backward_step: isize = -3;
    let too_far_back: isize = -20;

    println!("checked_add_signed(-3):  {:?}", index.checked_add_signed(backward_step));
    println!("checked_add_signed(-20): {:?}", index.checked_add_signed(too_far_back));
    println!("saturating_add_signed:   {}", index.saturating_add_signed(too_far_back));

    let position: i32 = -5;
    let distance: u32 = 12;
    println!("checked_add_unsigned:    {:?}", position.checked_add_unsigned(distance));
}
checked_add_signed(-3):  Some(7)
checked_add_signed(-20): None
saturating_add_signed:   0
checked_add_unsigned:    Some(7)

Reinterpreting Bits Between Signed and Unsigned (cast_signed and cast_unsigned)

To reinterpret the underlying bits of an integer between its signed and unsigned counterparts of the exact same width without changing the bit pattern, use .cast_signed() and .cast_unsigned() (or checked conversion via TryFrom when the numeric value itself must be preserved):

fn main() {
    let raw: u8 = 250;

    // Bitwise reinterpretation (same 8 bits: 0b1111_1010 -> -6)
    let bits_as_signed: i8 = raw.cast_signed();
    let back_to_unsigned: u8 = bits_as_signed.cast_unsigned();
    println!("cast_signed: {raw}u8 -> {bits_as_signed}i8 -> {back_to_unsigned}u8");

    // Value-preserving conversion (fails because 250 > i8::MAX)
    let value_checked = i8::try_from(raw);
    println!("i8::try_from(250u8) = {value_checked:?}");
}
cast_signed: 250u8 -> -6i8 -> 250u8
i8::try_from(250u8) = Err(TryFromIntError(()))