はじめに
ゼロ割り、ゼロサイズ確保、ゼロインデックス。「ゼロでない数値」を期待する場面はあちこちにあります。これを runtime チェックではなく 型 で保証できる仕組みが NonZero<T> です。
Rust 2024 edition で書き味がさらに素直になりました。これまでは NonZeroUsize, NonZeroU32 のように個別の型名を使っていました。それがジェネリック表記の NonZero<usize>, NonZero<u32> に統一されています。Rust 1.79 で NonZero<T> が安定化し、2024 edition では標準的な書き方になりました。
まず動くコード
ページング計算で「ページサイズはゼロにできない」を型で保証する例。
use std::num::NonZero; fn page_count(total_items: usize, page_size: NonZero<usize>) -> usize { total_items.div_ceil(page_size.get()) } fn main() { let page_size = const { NonZero::new(50).expect("50 != 0") }; let pages = page_count(1003, page_size); println!("pages = {pages}"); }
実行結果は次のとおりです。
pages = 21
page_count の引数が NonZero<usize> なので、ゼロを渡すコードはそもそも書けません。NonZero::new(0) は None を返し、 0 から NonZero を構築する手段はありません。ゼロ割りは関数まで到達する前にコンパイラが弾きます。
NonZero<T> の作り方
NonZero<T> を作るには3パターンあります。
use std::num::NonZero; // 1. リテラルから const block で構築 (推奨) let n1 = const { NonZero::new(50).expect("50 != 0") }; // 2. 動的な値から作る (失敗するかもしれない) fn try_build(x: usize) -> Option<NonZero<usize>> { NonZero::new(x) } // 3. unsafe で「絶対ゼロでない」を契約として宣言 let raw: usize = some_runtime_value(); let n3 = unsafe { NonZero::new_unchecked(raw) }; // ゼロを渡したら UB
通常は (1) か (2)。(3) の new_unchecked は契約違反すると Undefined Behavior なので、本当に必要な性能クリティカルな場面でだけ使います。
(1) の const { ... } ブロックは Rust 1.79 で安定化しました。コンパイル時に panic しないことを保証する 構文です。NonZero::new(50) はコンパイル時に評価され、.expect(...) も const context で動くので、もし 0 を書いていたらコンパイルエラーになります。runtime panic にはなりません。
ジェネリック表記のメリット
これまで個別の型名 (NonZeroUsize, NonZeroU32, ...) でも NonZero<T> でも同じ機能でしたが、ジェネリック表記のほうが2点で勝ります。
コードの一貫性
複数の整数型を扱うジェネリック関数で、 NonZero<T> のほうが自然に書けます。
use std::num::NonZero; fn safe_div<T>(a: T, b: NonZero<T>) -> T where T: std::ops::Div<Output = T> + From<NonZero<T>> // 仮想的な制約。実際にはより複雑になる + Copy, { a / T::from(b) }
ジェネリックパラメータ T をそのまま NonZero<T> に持っていけます。 個別の型名 (NonZeroUsize / NonZeroU32) で書こうとすると、関数を整数型ごとに複製する羽目です。
標準ライブラリの構造と一致
Option<T>, Result<T, E>, Vec<T> のように、 標準ライブラリの type wrapper はジェネリックです。NonZero<T> も同じ形にしておくほうが頭の整理がつきます。
「ゼロでない」以外のバリデーション型
NonZero<T> は標準ライブラリだけが提供できる「組込み型に対する制約」です。同じ発想を自分の型に適用したいなら、 newtype でラップして smart constructor を書くのが定石です。
#[derive(Debug, Clone, Copy)] pub struct PositiveAmount(u64); #[derive(Debug, thiserror::Error)] #[error("amount must be positive")] pub struct NotPositive; impl PositiveAmount { pub fn new(amount: u64) -> Result<Self, NotPositive> { if amount == 0 { Err(NotPositive) } else { Ok(Self(amount)) } } pub fn get(self) -> u64 { self.0 } } fn charge(account: &mut Account, amount: PositiveAmount) { account.balance += amount.get(); } # struct Account { balance: u64 }
「同じ u64 だけど、 PositiveAmount を経由しないと charge に渡せない」という制約を型で表現できます。これは NonZero<u64> とほぼ同じ発想ですが、自分のドメインに合わせた型を作れる柔軟性があります。
いつ NonZero<T> を使うか
「ゼロを許容しない」が引数のセマンティクスとして重要な場面で使います。代表例を挙げます。
- ページサイズ、バッチサイズ、ロット数
- 配列のインデックス (1-based)
- アロケーションサイズ
- スレッド数 / ワーカー数
逆に「カウンタ」「累積値」のような「ゼロから始まる数値」では NonZero を使う場面ではありません。
実用上の覚え方は 「もしこの値がゼロだったら関数の意味が壊れるか」 を考えることです。ゼロで意味が壊れる引数は NonZero<T>、ゼロでも意味のある引数は普通の整数。
サイズ最適化
地味なメリットですが、Option<NonZero<T>> は T と同じサイズです。None を 0 で表現する最適化が効きます。
use std::num::NonZero; use std::mem::size_of; fn main() { println!("{}", size_of::<u32>()); // 4 println!("{}", size_of::<NonZero<u32>>()); // 4 println!("{}", size_of::<Option<u32>>()); // 8 (タグ + 値) println!("{}", size_of::<Option<NonZero<u32>>>()); // 4 (None = 0 で表現) }
Option<NonZero<u32>> は Option<u32> より小さい。これは NonZero の値が 0 を取れないので、0 を使って None を表現できるためです。コレクションに大量に詰めるとメモリ効率に効きます。
まとめ
NonZero<T>で「ゼロでない数値」を型で保証できる- ゼロを渡すコードは関数まで到達する前にコンパイラが弾く
- 2024 edition の慣用句では
NonZero<usize>のようなジェネリック表記を使う - リテラルから作るときは
const { NonZero::new(...).expect(...) }で runtime panic 回避 - 自分のドメイン制約には newtype + smart constructor で同じ発想を適用できる
Option<NonZero<T>>はサイズ最適化が効く
引数のドキュメントに「page_size must be > 0」と書く代わりに、 NonZero<usize> で受ければそれが型で表現されます。書くドキュメントが減って、 runtime チェックも減って、 ミスも減ります。
関連
std::num::NonZero: https://doc.rust-lang.org/std/num/struct.NonZero.html- Rust 1.79 release notes (
NonZero<T>安定化): https://blog.rust-lang.org/2024/06/13/Rust-1.79.0/