#[derive] マクロ#[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 を手書きする必要があります。
#[derive(Debug, Clone)]
pub struct TsmHeader {
pub width: u16,
pub height: u16,
pub frames: u32,
pub fps: f32,
}
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()
}
}
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 |
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 が必須セットです。
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);
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 |
// 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()
}
}
// 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() }
}
}
外部クレートも 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 の文字列化 |
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}
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()?;
TsmHeader に #[derive(Debug, Clone)] を付ける意味
TSM ファイルの FITS ヘッダをパースした結果を格納する TsmHeader は、
以下の理由で Debug と Clone が特に重要です:
println!("{:#?}", header) で即確認できる。
开発中に「ヘッダが正しく読めているか」を printf デバッグする手間をなくす。TsmHeader はファイル I/O 後に Repository と ValueObject の両方に渡す可能性があるため、
.clone() で複製できると設計が柔軟になる。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 を付けると「代入するたびに暗黙コピーが走る」型になります。
TsmHeader のような複数フィールドを持つ struct では、
意図しないコピーがパフォーマンスに悪影響を与えることがあります。
Clone だけを実装して 明示的な .clone() 呼び出しを要求することで、
「ここでコピーが発生する」とコードから明確に読み取れます。
// 整数ベース: 軽量なので 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 { ... }
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,
}
derive の実践的な使用例。Channel, Filename, TimeMs への derive の選択根拠derive(Clone) が PhantomData 経由でどう機能するかim クレートと derive(Clone) の関係(Arc パターン)pub の可視性と derive の関係(pub フィールドと Debug 出力)Option<T> の詳解。cached: Option<String> フィールドの設計根拠