じゃあ、おうちで学べる

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

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

はじめに

ページサイズやワーカー数にゼロを渡してほしくない関数では、引数を NonZero<T> にできます。呼び出し側でゼロを拒否してから値を渡すため、関数内では同じ検査を繰り返さずに済みます。

Rust 1.79で NonZero<T> が安定化し、NonZero<usize>やNonZero<u32>と書けるようになりました。従来のNonZeroUsizeやNonZeroU32も型エイリアスとして使えます。これは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で検査し、ゼロなら返ってくるNoneを呼び出し側で処理します。検査済みの値を渡せば、関数内でゼロかどうかを確かめ直す必要はありません。

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 { ... }は、式をコンパイル時に評価する構文です。この例ではNonZero::new(50).expect(...)をコンパイル時に評価します。50を0に変えると、その評価中にpanicしてコンパイルエラーになります。

ジェネリック表記のメリット

NonZeroUsizeとNonZero<usize>は同じ型を表します。表記を揃えやすくなった一方、ジェネリック関数で使う際には制約があります。

コードの一貫性

NonZero<T>という表記からは、次のようなジェネリック関数を考えたくなります。ただし、これは構想を示す疑似コードであり、そのままstable Rustでコンパイルできる例ではありません。

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)
}

NonZero<T>のTには制約があります。公式資料のgeneric usageの注意にもあるとおり、必要なZeroablePrimitiveは不安定なAPIです。ジェネリック表記が安定したことと、任意の整数型を抽象化した関数を書けることは分けて考える必要があります。

標準ライブラリの構造と一致

Option<T>, Result<T, E>, Vec<T> のように、 標準ライブラリの type wrapper はジェネリックです。NonZero<T> も同じ形にしておくほうが頭の整理がつきます。

「ゼロでない」以外のバリデーション型

NonZero<T> は「整数のゼロを許さない」という制約を標準ライブラリが提供する型です。独自の制約を表したいなら、newtypeでラップしてsmart constructorを書けます。後述のOptionのサイズ保証はNonZeroに対するもので、任意のnewtypeに自動で付くわけではありません。

#[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>> は、NonZero が使わない 0 を None の表現に使うため、T と同じサイズです。

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>> が4バイト、Option<u32> が8バイトになります。同じ要素数を配列などに格納する場合、その差だけ値の保存に使うメモリを減らせます。

まとめ

  • NonZero<T> で「ゼロでない数値」を型で保証できる
  • 通常の整数は構築時に検査し、ゼロならNoneを処理する
  • Rust 1.79以降ではNonZero<usize>のような表記も使える
  • リテラルから作るときは const { NonZero::new(...).expect(...) } で runtime panic 回避
  • 自分のドメイン制約には newtype + smart constructor で同じ発想を適用できる
  • Option<NonZero<T>> は T と同じサイズで None も表せる

page_count が NonZero<usize> を受け取れば、呼び出し側は通常の整数をそのまま渡せません。入力を NonZero::new で検査してから渡し、ページ数を計算する側ではゼロの検査を省けます。

関連