じゃあ、おうちで学べる

本能を呼び覚ますこのコードに、君は抗えるか

`NonZero<T>` で「ゼロではない」を型に乗せる

はじめに

ゼロ割り、ゼロサイズ確保、ゼロインデックス。「ゼロでない数値」を期待する場面はあちこちにあります。これを 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 と同じサイズです。None0 で表現する最適化が効きます。

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 チェックも減って、 ミスも減ります。

関連