Rust の #[derive] マクロ

トレイト実装を自動生成する属性マクロの完全ガイド — 展開図・依存関係・scandata への応用 · 作成 2026-05-18
目次
  1. derive マクロとは何か
  2. 展開図: Before / After
  3. 標準ライブラリが提供する derive 一覧
  4. 依存関係グラフ
  5. derive できない場合の対処
  6. 外部クレートの derive
  7. 判断フロー: derive で済むか手動 impl か
  8. scandata 文脈への応用
  9. 関連項目

1. #[derive] マクロとは何か

#[derive(TraitName)]属性マクロ の一種で、struct / enum / union の定義に付けると コンパイラがその型に対する トレイト実装 (impl ブロック) を自動生成してくれます。

// これだけ書けば…
#[derive(Debug, Clone)]
pub struct Point { x: f64, y: f64 }

// コンパイラが内部で impl を生成してくれる(次のセクションで詳述)

Rust における derive手続きマクロ (procedural macro) の一形態です。 標準ライブラリが提供するものはコンパイラに組み込まれており、 外部クレート(serde など)が提供するものは proc-macro クレートとして実装されています。

位置づけ: derive はボイラープレートを排除するための最重要ツールです。 Rust では多くの機能がトレイトで表現されるため、derive なしでは デバッグ出力・コピー・比較・ハッシュ化など基本操作のたびに 数十行の impl を手書きする必要があります。

2. 展開図: Before / After

2-1. あなたが書くコード

#[derive(Debug, Clone)]
pub struct TsmHeader {
    pub width:  u16,
    pub height: u16,
    pub frames: u32,
    pub fps:    f32,
}

2-2. コンパイラが内部で生成する同等のコード

Debug の展開
impl std::fmt::Debug for TsmHeader {
    fn fmt(
        &self,
        f: &mut std::fmt::Formatter<'_>
    ) -> std::fmt::Result {
        f.debug_struct("TsmHeader")
            .field("width",  &self.width)
            .field("height", &self.height)
            .field("frames", &self.frames)
            .field("fps",    &self.fps)
            .finish()
    }
}
Clone の展開
impl std::clone::Clone for TsmHeader {
    fn clone(&self) -> Self {
        TsmHeader {
            width:  self.width.clone(),
            height: self.height.clone(),
            frames: self.frames.clone(),
            fps:    self.fps.clone(),
        }
    }
}

derive の本質: 各フィールドに対して同じトレイトが実装されていれば、 struct 全体への実装を「フィールド分繰り返す」コードをコンパイラが生成します。

derive 展開の仕組み あなたが書くコード #[derive(Debug, Clone)] pub struct TsmHeader { width: u16, height: u16, frames: u32, fps: f32, } コンパイラ proc-macro derive マクロ 各フィールドを 走査してコード トークンを生成 (AST 変換) コンパイラが生成するコード impl Debug for TsmHeader { fn fmt(&self, f: &mut Formatter) { f.debug_struct("TsmHeader") .field("width", &self.width) .field("height", &self.height) ... .finish() } } // + Clone の impl も同様
図1: #[derive] がコンパイル時に行うコード展開の流れ

3. 標準ライブラリが提供する derive 一覧

derive 生成するトレイト 何ができる 前提条件 注意点
Debug std::fmt::Debug {:?} / {:#?} でデバッグ出力 全フィールドが Debug 公開したくないフィールドも出力される
Clone std::clone::Clone .clone() で深いコピー 全フィールドが Clone Arc を含む場合は参照カウント +1 のみ
Copy std::marker::Copy 代入・関数渡しで暗黙コピー(move しない) Clone + 全フィールドが Copy ヒープ確保型 (String, Vec, Arc) は不可
PartialEq std::cmp::PartialEq == / != 比較 全フィールドが PartialEq f64 は NaN 問題あり(部分的な等値)
Eq std::cmp::Eq 全反射的な等値(x == x が常に真) PartialEq + 全フィールドが Eq f64 / f32 には付けられない
PartialOrd std::cmp::PartialOrd < / > / <= / >= 比較 PartialEq + 全フィールドが PartialOrd フィールド宣言順で辞書順比較
Ord std::cmp::Ord 全順序(sort(), BTreeMap のキー) Eq + PartialOrd + 全フィールドが Ord f64 / f32 には付けられない
Hash std::hash::Hash HashMap / HashSet のキーに使える 全フィールドが Hash Eq を実装するなら Hash との整合必須
Default std::default::Default T::default() でゼロ相当値を生成 全フィールドが Default 数値は 0、String は空、Vec は空リスト
最頻出パターン: #[derive(Debug, Clone)] はほぼすべての struct に付けます。 整数・bool のみで構成される小さな struct なら Copy も追加します。 ハッシュマップのキーにするなら PartialEq, Eq, Hash が必須セットです。

4. 依存関係グラフ

derive には「これを付けるにはあれも必要」という依存関係があります。 矢印は「必要とする」方向です。

derive の依存関係グラフ 必須依存 暗黙的推奨 Clone .clone() で複製 Copy 暗黙コピー 必須 PartialEq == で比較 Eq 全反射的等値 必須 Hash HashMap キー 推奨 PartialOrd < > 比較 必須 Ord 全順序・sort() 必須 Default ゼロ値生成 Debug {:?} 出力 f64 / f32 の制約: NaN != NaN のため Eq / Hash / Ord は付けられない。 f64 を含む型は PartialEq と PartialOrd のみ使用可。
図2: 標準 derive の依存関係。赤矢印 = 必須、青破線矢印 = 整合性推奨

よくある「正しい組み合わせ」

// 整数値 newtype(完全体)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct FrameIndex(pub usize);

// ヒープを含む型(Copy 不可)
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Filename(pub String);

// f64 を含む型(Eq/Hash/Ord は不可)
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct TimeMs(pub f64);

5. derive できない場合の対処

5-1. なぜ derive できないか

derive は「全フィールドが同じトレイトを実装していれば struct にも実装できる」という単純な規則で動きます。 以下のケースでは derive が使えません:

ケース理由対処
フィールドの一部が未実装 例: Array3<i16>PartialEq 未実装 手動 impl or newtype でラップ
カスタム比較ロジック 例: 大文字小文字を無視した PartialEq 手動 impl PartialEq
循環参照 (Rc の親子) Clone が深い再帰になりうる 手動 impl で浅いコピーを実装
ジェネリック境界の追加指定 derive が生成する bound が過剰・過少 #[derive] + impl ... where ... の組み合わせ or 手動 impl
raw ポインタ / unsafe フィールド derive が安全に生成できない 手動 impl + unsafe impl

5-2. 手動 impl の例

// Array3<i16> は PartialEq を持たないため、データ次元だけを比較する例
use ndarray::Array3;

pub struct FramesData {
    pub data: Array3<i16>,
}

impl PartialEq for FramesData {
    fn eq(&self, other: &Self) -> bool {
        // shape だけ比較(実データは高価なので比較しない設計)
        self.data.shape() == other.data.shape()
    }
}

5-3. 条件付き bound の例

// derive が生成する bound が過剰な場合の回避策
pub struct Wrapper<T> {
    inner: T,
    cached: Option<String>,
}

// derive(Clone) は T: Clone を要求するが、cached は独立して Clone できる
// 通常はこれで OK(T: Clone がほぼ常に成立するため)
#[derive(Clone)]
pub struct Wrapper<T: Clone> { ... }

// または T に Clone bound なしで手動 impl
impl<T: Clone> Clone for Wrapper<T> {
    fn clone(&self) -> Self {
        Wrapper { inner: self.inner.clone(), cached: self.cached.clone() }
    }
}

6. 外部クレートが提供する derive

外部クレートも proc-macro クレートとして derive を提供できます。 Cargo.toml に依存を追加すれば、標準の derive と同じ文法で使えます。

クレートderive何ができるscandata での用途
serde Serialize, Deserialize JSON / MessagePack / TOML 等への変換 WebSocket 通信で ValueObject を MessagePack 化 (rmp-serde)
thiserror Error エラー型に std::error::Error を実装、Display を自動生成 lib クレートのエラー型定義
binrw BinRead, BinWrite バイナリフォーマットの読み書きを宣言的に記述 TSM ヘッダのバイナリパース
derive_more Add, Sub, Display, From, Into, ... 算術演算子・変換トレイトを derive で追加 Newtype に + 演算子を付けるとき
strum EnumString, Display, EnumIter enum と文字列の相互変換・イテレーション チャネル種別 enum の文字列化

serde の例

use serde::{Serialize, Deserialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TsmHeader {
    pub width:  u16,
    pub height: u16,
    pub frames: u32,
    pub fps:    f32,
}

// JSON に変換
let header = TsmHeader { width: 128, height: 128, frames: 64, fps: 100.0 };
let json = serde_json::to_string(&header)?;
// → {"width":128,"height":128,"frames":64,"fps":100.0}

binrw の例(TSM ヘッダパース)

use binrw::BinRead;

#[derive(BinRead, Debug, Clone)]
#[br(little)]  // リトルエンディアン
pub struct TsmHeader {
    pub width:  u16,
    pub height: u16,
    #[br(pad_before = 8)]  // 8 バイトスキップ
    pub frames: u32,
    pub fps:    f32,
}

// ファイルから直接パース
let header: TsmHeader = cursor.read_le()?;

7. 判断フロー: derive で済むか手動 impl か

判断フロー: derive vs 手動 impl トレイト実装が必要 標準ライブラリの derive 一覧にある? YES 全フィールドが そのトレイトを実装? YES #[derive()] を付ける ボイラープレートなし NO 手動 impl を書く または newtype でラップ NO 外部クレートに derive がある? (serde等) YES 外部クレートの derive を使う NO 手動 impl を書く カスタムロジックを実装 カスタム比較 / 出力が 必要 (大文字無視等)? YES → 手動 impl 確定 NO → newtype 検討
図3: derive を使うかどうかの判断フロー

8. scandata 文脈への応用

8-1. TsmHeader#[derive(Debug, Clone)] を付ける意味

TSM ファイルの FITS ヘッダをパースした結果を格納する TsmHeader は、 以下の理由で DebugClone が特に重要です:

8-2. Copy を付けない理由

// TSM ヘッダは複数フィールドを持つそれなりの大きさの struct
#[derive(Debug, Clone)]  // Copy は付けない
pub struct TsmHeader {
    pub width:      u16,
    pub height:     u16,
    pub frames:     u32,
    pub fps:        f32,
    pub channel:    u8,
    // ... さらに FITS ヘッダのフィールドが続く
}
Copy を控える指針
Copy を付けると「代入するたびに暗黙コピーが走る」型になります。 TsmHeader のような複数フィールドを持つ struct では、 意図しないコピーがパフォーマンスに悪影響を与えることがあります。
代わりに Clone だけを実装して 明示的な .clone() 呼び出しを要求することで、 「ここでコピーが発生する」とコードから明確に読み取れます。

8-3. Newtype 識別子への推奨 derive

// 整数ベース: 軽量なので Copy も付ける
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Channel(pub u8);

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct FrameIndex(pub usize);

// String ベース: Copy 不可
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Filename(pub String);

// serde + binrw を組み合わせる場合
#[derive(Debug, Clone, BinRead, Serialize, Deserialize)]
#[br(little)]
pub struct TsmHeader { ... }

8-4. 永続データ構造と Clone の関係

im::HashMap にデータを格納するには、値型が Clone を実装している必要があります。 Arc<Array3<i16>> で巨大データを包んでおくと、Clone は参照カウントの加算だけで完了し、 メモリコピーが発生しません。

#[derive(Clone)]          // im::HashMap に入れるために必須
pub enum ValueObj {
    Frames(FramesData),
    Image(ImageData),
}

#[derive(Clone)]
pub struct FramesData {
    pub data: std::sync::Arc<ndarray::Array3<i16>>,  // Clone = Rc++
    pub tag:  DataTag,
}

目次に戻る