How do I work with NonZero in Rust?

Walkthrough

NonZero types (like NonZeroU32, NonZeroIsize, etc.) wrap integer types with a compile-time guarantee that the value is never zero. This enables powerful optimizations, particularly that Option<NonZeroT> has the same size as the underlying integer type.

Key characteristics:

  • Never zero — Guaranteed non-zero value at compile time
  • Null optimization — Option<NonZeroT> is same size as T
  • Available types — NonZeroU8, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU128, NonZeroUsize (and signed variants)
  • Safe construction — Creating requires validation via new() returning Option

When to use NonZero types:

  • Identifier types that can never be zero (IDs, handles)
  • Memory-efficient optional integers
  • FFI with non-zero integer parameters
  • Division safety (no division by zero)
  • Sentinel values in collections

Code Examples

Basic NonZero Usage

use std::num::NonZeroU32;
 
fn main() {
    // Create a NonZeroU32
    let nonzero = NonZeroU32::new(42).expect("42 is non-zero");
    println!("Value: {}", nonzero);
    
    // Zero returns None
    let zero = NonZeroU32::new(0);
    assert!(zero.is_none());
    
    // Get the underlying value
    let value: u32 = nonzero.get();
    println!("Underlying value: {}", value);
}

Option Size Optimization

use std::num::NonZeroU32;
 
fn main() {
    // Option<NonZeroU32> is the same size as u32!
    // None is represented as 0
    
    println!("Size of Option<NonZeroU32>: {}", std::mem::size_of::<Option<NonZeroU32>>());
    println!("Size of u32: {}", std::mem::size_of::<u32>());
    // Both are 4 bytes!
    
    println!("Size of Option<u32>: {}", std::mem::size_of::<Option<u32>>());
    // This is 8 bytes (4 for value + 1 for discriminant + padding)
    
    // Create optional non-zero values
    let some = NonZeroU32::new(42);
    let none: Option<NonZeroU32> = None;
    
    println!("Some: {:?}", some);
    println!("None: {:?}", none);
}

NonZero for Identifiers

use std::num::NonZeroU64;
 
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct UserId(NonZeroU64);
 
impl UserId {
    fn new(id: u64) -> Option<Self> {
        NonZeroU64::new(id).map(Self)
    }
    
    fn get(&self) -> u64 {
        self.0.get()
    }
}
 
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ProductId(NonZeroU32);
 
impl ProductId {
    fn new(id: u32) -> Option<Self> {
        NonZeroU32::new(id).map(Self)
    }
    
    fn get(&self) -> u32 {
        self.0.get()
    }
}
 
fn main() {
    let user = UserId::new(12345).expect("Valid user ID");
    let product = ProductId::new(999).expect("Valid product ID");
    
    println!("User ID: {}", user.get());
    println!("Product ID: {}", product.get());
    
    // Zero IDs are rejected
    assert!(UserId::new(0).is_none());
    assert!(ProductId::new(0).is_none());
    
    // Optional IDs are memory-efficient
    let maybe_user: Option<UserId> = Some(user);
    println!("Size of Option<UserId>: {}", std::mem::size_of::<Option<UserId>>());
    // Same as u64!
}

Safe Division with NonZero

use std::num::NonZeroU32;
 
fn safe_divide(numerator: u32, denominator: NonZeroU32) -> u32 {
    // No need to check for zero - it's guaranteed non-zero!
    numerator / denominator.get()
}
 
fn main() {
    let divisor = NonZeroU32::new(10).unwrap();
    
    let result = safe_divide(100, divisor);
    println!("100 / 10 = {}", result);
    
    // Division by zero is impossible with NonZero
    // safe_divide(100, NonZeroU32::new(0).unwrap()); // Would panic at unwrap
}

NonZero in Collections

use std::num::NonZeroUsize;
 
struct SparseArray<T> {
    data: Vec<Option<T>>,
}
 
impl<T> SparseArray<T> {
    fn new(size: usize) -> Self {
        Self {
            data: vec![None; size],
        }
    }
    
    fn set(&mut self, index: usize, value: T) {
        self.data[index] = Some(value);
    }
    
    fn get(&self, index: usize) -> Option<&T> {
        self.data.get(index)?.as_ref()
    }
}
 
// Compact index storage using NonZero
struct CompactIndex {
    // Uses Option<NonZeroUsize> for optional indices
    // None means "no index", Some(NonZeroUsize(n-1)) means index n
    // This is same size as usize!
    index: Option<NonZeroUsize>,
}
 
impl CompactIndex {
    fn none() -> Self {
        Self { index: None }
    }
    
    fn some(index: usize) -> Self {
        // Store index + 1 so 0 can be represented
        Self {
            index: NonZeroUsize::new(index.checked_add(1).expect("Index overflow")),
        }
    }
    
    fn get(&self) -> Option<usize> {
        self.index.map(|n| n.get() - 1)
    }
}
 
fn main() {
    let mut arr = SparseArray::new(10);
    arr.set(3, String::from("Hello"));
    arr.set(7, String::from("World"));
    
    println!("Index 3: {:?}", arr.get(3));
    println!("Index 5: {:?}", arr.get(5));
    
    // Compact index storage
    let mut indices = Vec::new();
    for i in 0..10 {
        indices.push(CompactIndex::some(i * 2));
    }
    indices.push(CompactIndex::none());
    
    println!("Index storage: {} bytes for {} entries + 1 None", 
             indices.len() * std::mem::size_of::<CompactIndex>(),
             indices.len());
}

All NonZero Types

use std::num::*;
 
fn main() {
    // Unsigned types
    let u8_ = NonZeroU8::new(1).unwrap();
    let u16_ = NonZeroU16::new(1).unwrap();
    let u32_ = NonZeroU32::new(1).unwrap();
    let u64_ = NonZeroU64::new(1).unwrap();
    let u128_ = NonZeroU128::new(1).unwrap();
    let usize_ = NonZeroUsize::new(1).unwrap();
    
    // Signed types (since Rust 1.34)
    let i8_ = NonZeroI8::new(1).unwrap();
    let i16_ = NonZeroI16::new(1).unwrap();
    let i32_ = NonZeroI32::new(1).unwrap();
    let i64_ = NonZeroI64::new(1).unwrap();
    let i128_ = NonZeroI128::new(1).unwrap();
    let isize_ = NonZeroIsize::new(1).unwrap();
    
    println!("All NonZero types created successfully");
    
    // Signed NonZero can also be negative
    let negative = NonZeroI32::new(-42).unwrap();
    println!("Negative value: {}", negative);
}

NonZero with Arithmetic Operations

use std::num::NonZeroU32;
 
fn main() {
    let a = NonZeroU32::new(10).unwrap();
    let b = NonZeroU32::new(5).unwrap();
    
    // NonZero supports arithmetic operations (since Rust 1.51)
    
    // Multiplication of two NonZero values is NonZero
    let product = a.checked_mul(b);
    println!("10 * 5 = {:?}", product);
    
    // Division of NonZero by NonZero is NonZero
    let quotient = a.checked_div(b);
    println!("10 / 5 = {:?}", quotient);
    
    // Addition and subtraction can result in zero, so return Option
    let sum = a.get().saturating_add(b.get());
    println!("10 + 5 = {}", sum);
    
    // power of NonZero is NonZero
    let squared = a.checked_pow(2);
    println!("10^2 = {:?}", squared);
}

FFI with NonZero

use std::num::NonZeroU32;
use std::os::raw::c_uint;
 
// C function that expects non-zero handle
// void process_handle(unsigned int handle); // handle must not be 0
 
fn process_handle(handle: NonZeroU32) {
    // Safe: handle is guaranteed non-zero
    let raw: c_uint = handle.get();
    println!("Processing handle: {}", raw);
    // unsafe { process_handle_ffi(raw); }
}
 
// C function that returns non-zero handle or 0 on error
fn get_handle() -> Option<NonZeroU32> {
    // Simulate FFI call
    let raw: c_uint = 42;
    NonZeroU32::new(raw)
}
 
fn main() {
    // Safe FFI with compile-time guarantees
    if let Some(handle) = get_handle() {
        process_handle(handle);
    } else {
        println!("Failed to get handle");
    }
}

NonZero for Memory-Efficient Structs

use std::num::{NonZeroU32, NonZeroU64};
 
// Before: Uses extra byte for Option discriminant
struct UserV1 {
    id: Option<u64>,        // 16 bytes (8 + discriminant + padding)
    age: Option<u32>,       // 8 bytes (4 + discriminant + padding)
}
 
// After: Uses NonZero for zero-cost Option
struct UserV2 {
    id: Option<NonZeroU64>, // 8 bytes
    age: Option<NonZeroU32>, // 4 bytes
}
 
impl UserV2 {
    fn new(id: u64, age: u32) -> Option<Self> {
        Some(Self {
            id: NonZeroU64::new(id)?,
            age: NonZeroU32::new(age)?,
        })
    }
    
    fn id(&self) -> u64 {
        self.id.map(|n| n.get()).unwrap_or(0)
    }
    
    fn age(&self) -> u32 {
        self.age.map(|n| n.get()).unwrap_or(0)
    }
}
 
fn main() {
    println!("Size of UserV1: {}", std::mem::size_of::<UserV1>());
    println!("Size of UserV2: {}", std::mem::size_of::<UserV2>());
    
    // UserV2 is significantly smaller!
    
    let user = UserV2::new(12345, 25).expect("Valid user");
    println!("User ID: {}, Age: {}", user.id(), user.age());
}

NonZero in Linked Structures

use std::num::NonZeroUsize;
use std::ptr::NonNull;
 
// Arena allocator index
struct Arena<T> {
    data: Vec<T>,
}
 
impl<T> Arena<T> {
    fn new() -> Self {
        Self { data: Vec::new() }
    }
    
    fn alloc(&mut self, value: T) -> Index {
        let index = self.data.len();
        self.data.push(value);
        // +1 so 0 can mean "null"
        Index(NonZeroUsize::new(index + 1).unwrap())
    }
    
    fn get(&self, index: Index) -> &T {
        &self.data[index.0.get() - 1]
    }
}
 
#[derive(Clone, Copy)]
struct Index(NonZeroUsize);
 
impl Index {
    fn null() -> Option<Self> {
        None
    }
}
 
struct Node<T> {
    value: T,
    next: Option<Index>,  // Same size as usize!
}
 
fn main() {
    let mut arena: Arena<String> = Arena::new();
    
    let a = arena.alloc(String::from("First"));
    let b = arena.alloc(String::from("Second"));
    let c = arena.alloc(String::from("Third"));
    
    println!("Item at a: {}", arena.get(a));
    println!("Item at b: {}", arena.get(b));
    
    println!("Size of Option<Index>: {}", std::mem::size_of::<Option<Index>>());
}

Unsafe Construction

use std::num::NonZeroU32;
 
fn main() {
    // Safe construction
    let safe = NonZeroU32::new(42).expect("Non-zero");
    
    // Unsafe construction when you KNOW the value is non-zero
    // Only use this when the invariant is guaranteed elsewhere
    let unsafe_val: u32 = 100;
    let unsafe_nonzero: NonZeroU32 = unsafe {
        NonZeroU32::new_unchecked(unsafe_val)
    };
    
    println!("Safe: {}, Unsafe: {}", safe, unsafe_nonzero);
    
    // NEVER do this:
    // let bad = unsafe { NonZeroU32::new_unchecked(0) }; // UB!
}

Converting Between NonZero Types

use std::num::{NonZeroU32, NonZeroU64};
 
fn main() {
    let u32_val = NonZeroU32::new(42).unwrap();
    
    // Convert to u64
    let u64_val = NonZeroU64::from(u32_val);
    println!("u64: {}", u64_val);
    
    // Get underlying value and convert
    let raw: u32 = u32_val.get();
    let as_u64: u64 = raw as u64;
    
    // Or use From/Into
    let converted: u64 = u32_val.get().into();
    println!("Converted: {}", converted);
}

NonZero with Bounds Checking

use std::num::NonZeroUsize;
 
fn main() {
    let len = 10;
    
    // Safe indexing with NonZero
    fn safe_index(slice: &[i32], index: NonZeroUsize) -> Option<&i32> {
        // NonZero is 1-indexed conceptually, so subtract 1
        slice.get(index.get() - 1)
    }
    
    let data = [1, 2, 3, 4, 5];
    
    let idx = NonZeroUsize::new(3).unwrap();
    if let Some(&val) = safe_index(&data, idx) {
        println!("Value at index 3 (1-indexed): {}", val);
    }
    
    // Out of bounds
    let big_idx = NonZeroUsize::new(100).unwrap();
    assert!(safe_index(&data, big_idx).is_none());
}

NonZero Constants

use std::num::NonZeroU32;
 
// Constants can be created using const fn
const MAX_ITEMS: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(1000) };
const PAGE_SIZE: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(4096) };
 
fn main() {
    println!("Max items: {}", MAX_ITEMS);
    println!("Page size: {}", PAGE_SIZE);
    
    // Use in const context
    const BUFFER_SIZE: usize = PAGE_SIZE.get() * 2;
    println!("Buffer size: {}", BUFFER_SIZE);
}

Summary

NonZero Types:

Type Underlying Min Value Max Value
NonZeroU8 u8 1 255
NonZeroU16 u16 1 65535
NonZeroU32 u32 1 4294967295
NonZeroU64 u64 1 2^64-1
NonZeroU128 u128 1 2^128-1
NonZeroUsize usize 1 platform max
NonZeroI8 i8 -128..=-1, 1..=127
NonZeroI32 i32 excludes 0

Key Methods:

Method Description Safety
new(n) Create from value (returns Option) Safe
new_unchecked(n) Create without checking Unsafe
get() Get underlying value Safe
checked_mul() Multiply NonZero values Safe
checked_div() Divide NonZero values Safe
checked_pow() Raise to power Safe

Size Comparison:

Type Size
u32 4 bytes
NonZeroU32 4 bytes
Option<u32> 8 bytes
Option<NonZeroU32> 4 bytes

When to Use NonZero:

Scenario Appropriate?
IDs that can't be zero āœ… Yes
Optional integers with memory constraints āœ… Yes
Division denominators āœ… Yes
FFI non-zero parameters āœ… Yes
General integer arithmetic āŒ Use normal types
Zero is a valid value āŒ Use normal types

Key Points:

  • NonZero types guarantee the value is never zero
  • Option<NonZeroT> has the same size as T (null optimization)
  • Available for all integer types (signed and unsigned)
  • Supports arithmetic operations that preserve non-zero property
  • Essential for memory-efficient optional integers
  • Safe construction via new() returns Option
  • new_unchecked() is unsafe and requires the invariant
  • Use for identifiers, handles, and division safety