#185 Jun 6, 2026

185. Range<NonZeroU32> — Iterate NonZero Integers Without Re-Wrapping Every Step

NonZeroU32 keeps Option<Id> at 4 bytes — but until Rust 1.96 you couldn’t iterate lo..hi over them. You’d drop back to u32 and re-wrap every step.

NonZeroU32 and friends are great for indices and IDs because Option<NonZeroU32> fits the niche and stays 4 bytes wide. The catch: Range<NonZeroU32> wasn’t an iterator. The moment you wanted to walk a range of IDs, you fell back to plain u32 and unwrapped your way back in:

1
2
3
4
5
6
7
8
9
use std::num::NonZeroU32;

let lo = NonZeroU32::new(1).unwrap();
let hi = NonZeroU32::new(5).unwrap();

// Pre-1.96: lift, iterate plain ints, re-wrap each step.
let ids: Vec<NonZeroU32> = (lo.get()..hi.get())
    .map(|n| NonZeroU32::new(n).unwrap())
    .collect();

Three problems: the Range drops the invariant, every step pays for an unwrap, and a future refactor that changes the bound type silently swaps your iterator out from under you.

Rust 1.96 stabilized Step for NonZero integers (PR #127534). Now the range itself is an iterator that yields NonZeroU32:

1
2
3
4
5
6
7
8
use std::num::NonZeroU32;

let lo = NonZeroU32::new(1).unwrap();
let hi = NonZeroU32::new(5).unwrap();

let ids: Vec<NonZeroU32> = (lo..hi).collect();
assert_eq!(ids.len(), 4);
assert_eq!(ids[0].get(), 1);

It works for the inclusive form too, so you can sweep the whole representable range without overflow gymnastics:

1
2
3
4
5
6
use std::num::NonZeroU8;

let total: u32 = (NonZeroU8::MIN..=NonZeroU8::MAX)
    .map(|n| n.get() as u32)
    .sum();
assert_eq!(total, 32_640); // 1 + 2 + ... + 255

If you keep your IDs in a NonZeroU32 newtype to shrink Option, the iteration story now matches: the range yields the right type the whole way through, no per-step unwrap, no invariant laundering through u32.

← Previous 184. Option::take — The Ergonomic Sibling of mem::take, Just for Option Next → 186. str::split_inclusive — Split a String and Keep the Separator With Each Chunk