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 asT - Available types ā
NonZeroU8,NonZeroU16,NonZeroU32,NonZeroU64,NonZeroU128,NonZeroUsize(and signed variants) - Safe construction ā Creating requires validation via
new()returningOption
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 asT(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()returnsOption new_unchecked()is unsafe and requires the invariant- Use for identifiers, handles, and division safety
