enum を使ったデータモデリング
| 対応: | Rust 1.0(2015) |
|---|
Rustのenumは値の種類によって異なるデータを持てるため、複雑なドメインモデルを安全に表現できます。状態機械(state machine)やコマンドパターンなど実践的な設計に活用されます。
構文
// 支払い方法を表すenumです。
#[derive(Debug)]
enum Payment {
Cash(f64),
CreditCard { number: String, amount: f64 },
CryptoCurrency { coin: String, wallet: String, amount: f64 },
}
// 状態機械パターン: 接続状態を表します。
#[derive(Debug)]
enum ConnectionState {
Disconnected,
Connecting { host: String, port: u16 },
Connected { session_id: u64, latency_ms: u32 },
Error(String),
}
活用パターン一覧
| パターン | 概要 |
|---|---|
| 状態機械 | 各バリアントがシステムの状態を表します。不正な状態遷移をコンパイル時に防げます。 |
| コマンドパターン | 各バリアントが1つの操作(コマンド)を表します。 |
| AST(抽象構文木) | 再帰的なenumでプログラムの構造を表現します(Box<T>でラップ必要)。 |
| エラー型 | 失敗の種類をバリアントで表現します。 |
| Option<T> の内部 | Some(T) / None で「値があるかどうか」を表します。 |
| Result<T, E> の内部 | Ok(T) / Err(E) で「成功か失敗か」を表します。 |
サンプルコード
sample_enum_data_modeling.rs
// 格闘スタイルを表すenumです。
#[derive(Debug)]
enum FightingStyle {
AncientArts { school: String, founder: String },
SouthTownStreet { base: String },
Orochi { affiliation: String, power: f64 },
}
// KOFファイターを表す構造体です。
#[derive(Debug)]
struct Fighter {
id: u32,
name: String,
style: FightingStyle,
}
impl Fighter {
fn is_orochi(&self) -> bool {
matches!(self.style, FightingStyle::Orochi { .. })
}
fn profile(&self) -> String {
match &self.style {
FightingStyle::AncientArts { school, .. } =>
format!("[{}] {} - {} の使い手", self.id, self.name, school),
FightingStyle::SouthTownStreet { base } =>
format!("[{}] {} - {} 出身", self.id, self.name, base),
FightingStyle::Orochi { affiliation, .. } =>
format!("[{}] {} - {} 所属", self.id, self.name, affiliation),
}
}
}
// BGMを表すenumです(再帰的なenum)。
#[derive(Debug)]
enum BgmData {
Title(String),
WithBpm { title: String, bpm: u32 },
Playlist(Vec<BgmData>),
}
impl BgmData {
fn describe(&self) -> String {
match self {
BgmData::Title(t) => t.clone(),
BgmData::WithBpm { title, bpm } => format!("{} ({}BPM)", title, bpm),
BgmData::Playlist(list) => {
let titles: Vec<String> = list.iter().map(|b| b.describe()).collect();
format!("[{}]", titles.join(", "))
}
}
}
}
fn main() {
let fighters = vec![
Fighter {
id: 1,
name: String::from("八神庵"),
style: FightingStyle::AncientArts {
school: String::from("八神流古武術"),
founder: String::from("八神一族"),
},
},
Fighter {
id: 2,
name: String::from("草薙京"),
style: FightingStyle::AncientArts {
school: String::from("草薙流古武術"),
founder: String::from("草薙一族"),
},
},
Fighter {
id: 3,
name: String::from("テリー・ボガード"),
style: FightingStyle::SouthTownStreet {
base: String::from("サウスタウン"),
},
},
Fighter {
id: 4,
name: String::from("ゲーニッツ"),
style: FightingStyle::Orochi {
affiliation: String::from("オロチ四天王"),
power: 0.95,
},
},
];
for fighter in &fighters {
println!("{}", fighter.profile());
}
let orochi_count = fighters.iter().filter(|f| f.is_orochi()).count();
println!("オロチ系: {}/{}", orochi_count, fighters.len());
// BgmData の使用例です。
let playlist = BgmData::Playlist(vec![
BgmData::Title(String::from("嵐のサキソフォン2")),
BgmData::WithBpm { title: String::from("ESAKA"), bpm: 138 },
BgmData::Title(String::from("Kurikinton")),
]);
println!("BGM: {}", playlist.describe());
}
rustc enum_data_modeling.rs ./enum_data_modeling [1] 八神庵 - 八神流古武術 の使い手 [2] 草薙京 - 草薙流古武術 の使い手 [3] テリー・ボガード - サウスタウン 出身 [4] ゲーニッツ - オロチ四天王 所属 オロチ系: 1/4 BGM: [嵐のサキソフォン2, ESAKA (138BPM), Kurikinton]
概要
Rustのenumを使った設計は「不正な状態を型で表現できない(Making illegal states unrepresentable)」というコンセプトを実現します。例えば『Orochi』バリアントにしか『power』がないため、オロチ系でないファイターに『power』でアクセスするコードはコンパイルエラーになります。
『matches!()』はパターンに一致するかどうかをboolで返す便利なマクロです。特定のバリアントかどうかをboolで確認するときに使います。また、コード内の『..』は残りのフィールドを無視するパターンです。
enumの基本定義は『enum / バリアント定義』を参照してください。
記事の間違いや著作権の侵害等ございましたらお手数ですがこちらまでご連絡頂ければ幸いです。