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) で「成功か失敗か」を表します。 |
サンプルコード
パターン1: Categoryによるエントリのモデリング
sample_enum_data_modeling.rs
#[derive(Debug)]
enum Category {
TypeA { label: String, origin: String },
TypeB { base: String },
Special { affiliation: String, power: f64 },
}
#[derive(Debug)]
struct Entry {
id: u32,
name: String,
category: Category,
}
impl Entry {
fn is_special(&self) -> bool {
matches!(self.category, Category::Special { .. })
}
fn profile(&self) -> String {
match &self.category {
Category::TypeA { label, .. } =>
format!("[{}] {} - {} に所属", self.id, self.name, label),
Category::TypeB { base } =>
format!("[{}] {} - {} 出身", self.id, self.name, base),
Category::Special { affiliation, .. } =>
format!("[{}] {} - {} 所属", self.id, self.name, affiliation),
}
}
}
#[derive(Debug)]
enum MetaData {
Title(String),
WithValue { title: String, value: u32 },
Collection(Vec<MetaData>),
}
impl MetaData {
fn describe(&self) -> String {
match self {
MetaData::Title(t) => t.clone(),
MetaData::WithValue { title, value } => format!("{} ({})", title, value),
MetaData::Collection(list) => {
let titles: Vec<String> = list.iter().map(|b| b.describe()).collect();
format!("[{}]", titles.join(", "))
}
}
}
}
fn main() {
let entries = vec![
Entry {
id: 1,
name: String::from("user_a"),
category: Category::TypeA {
label: String::from("group_a"),
origin: String::from("group_a"),
},
},
Entry {
id: 2,
name: String::from("user_c"),
category: Category::TypeA {
label: String::from("group_b"),
origin: String::from("group_b"),
},
},
Entry {
id: 3,
name: String::from("user_e"),
category: Category::TypeB {
base: String::from("org_a"),
},
},
Entry {
id: 4,
name: String::from("user_b"),
category: Category::Special {
affiliation: String::from("org_a"),
power: 0.95,
},
},
];
for entry in &entries {
println!("{}", entry.profile());
}
let special_count = entries.iter().filter(|f| f.is_special()).count();
println!("Special: {}/{}", special_count, entries.len());
let collection = MetaData::Collection(vec![
MetaData::Title(String::from("item_a")),
MetaData::WithValue { title: String::from("item_b"), value: 138 },
MetaData::Title(String::from("item_c")),
]);
println!("Meta: {}", collection.describe());
}
rustc enum_data_modeling.rs ./enum_data_modeling [1] user_a - group_a に所属 [2] user_c - group_b に所属 [3] user_e - org_a 出身 [4] user_b - org_a 所属 Special: 1/4 Meta: [item_a, item_b (138), item_c]
パターン2: 状態機械(state machine)パターン
enumの各バリアントがシステムの状態を表します。不正な状態遷移をコンパイル時に防ぐことができます。次の例では、タスクのフェーズを状態機械で表現しています。
state_machine.rs
#[derive(Debug)]
enum TaskPhase {
Waiting,
Assigning { assignee1: String, assignee2: String },
Running { step: u32, time_left: u32 },
Done { winner: String, steps_completed: u32 },
}
impl TaskPhase {
fn next(self) -> TaskPhase {
match self {
TaskPhase::Waiting => TaskPhase::Assigning {
assignee1: String::from("user_a"),
assignee2: String::from("user_b"),
},
TaskPhase::Assigning { assignee1, .. } => TaskPhase::Running {
step: 1,
time_left: 99,
},
TaskPhase::Running { step, .. } => TaskPhase::Done {
winner: String::from("user_a"),
steps_completed: step,
},
TaskPhase::Done { .. } => TaskPhase::Waiting,
}
}
fn describe(&self) -> String {
match self {
TaskPhase::Waiting => String::from("待機中"),
TaskPhase::Assigning { assignee1, assignee2 } =>
format!("割り当て: {} vs {}", assignee1, assignee2),
TaskPhase::Running { step, time_left } =>
format!("Step {} 残り{}秒", step, time_left),
TaskPhase::Done { winner, steps_completed } =>
format!("完了: {} ({}ステップ)", winner, steps_completed),
}
}
}
fn main() {
let mut phase = TaskPhase::Waiting;
for _ in 0..4 {
println!("{}", phase.describe());
phase = phase.next();
}
}
rustc state_machine.rs ./state_machine 待機中 割り当て: user_a vs user_b Step 1 残り99秒 完了: user_a (1ステップ)
パターン3: コマンドパターン(各バリアントが操作を表す)
enumの各バリアントをコマンドとして扱います。操作の種類を型で表現できるため、処理のディスパッチをmatchで安全に記述できます。
command_pattern.rs
#[derive(Debug)]
enum Command {
Move { dx: i32, dy: i32 },
Execute { action_name: String, cost: u32 },
Wait,
UseSpecial { name: String, cost: u32 },
}
struct TaskState {
worker_name: String,
hp: i32,
meter: u32,
}
impl TaskState {
fn execute(&mut self, cmd: &Command) {
match cmd {
Command::Move { dx, dy } =>
println!("{} が ({}, {}) に移動", self.worker_name, dx, dy),
Command::Execute { action_name, cost } =>
println!("{} が「{}」を実行 (コスト: {})", self.worker_name, action_name, cost),
Command::Wait =>
println!("{} が待機", self.worker_name),
Command::UseSpecial { name, cost } => {
if self.meter >= *cost {
self.meter -= cost;
println!("{} が「{}」を発動 (meter: {})", self.worker_name, name, self.meter);
} else {
println!("メーター不足");
}
}
}
}
}
fn main() {
let mut state = TaskState {
worker_name: String::from("user_a"),
hp: 1000,
meter: 100,
};
let commands = vec![
Command::Move { dx: 2, dy: 0 },
Command::Execute { action_name: String::from("action_a"), cost: 120 },
Command::Wait,
Command::UseSpecial { name: String::from("special_a"), cost: 50 },
Command::UseSpecial { name: String::from("special_b"), cost: 100 },
];
for cmd in &commands {
state.execute(cmd);
}
}
rustc command_pattern.rs ./command_pattern user_a が (2, 0) に移動 user_a が「action_a」を実行 (コスト: 120) user_a が待機 user_a が「special_a」を発動 (meter: 50) メーター不足
概要
Rustのenumを使った設計は「不正な状態を型で表現できない(Making illegal states unrepresentable)」というコンセプトを実現します。例えば『Special』バリアントにしか『power』がないため、Special以外のエントリに『power』でアクセスするコードはコンパイルエラーになります。
『matches!()』はパターンに一致するかどうかをboolで返すマクロです。特定のバリアントかどうかをboolで確認するときに使います。また、コード内の『..』は残りのフィールドを無視するパターンです。
enumの基本定義は『enum / バリアント定義』を参照してください。
よくあるミス
ミス1: matchで全バリアントを網羅しないコンパイルエラー
Rustのmatch式はすべてのバリアントを網羅する必要があります。1つでも抜けるとコンパイルエラーになります。
ng_match_exhaustive.rs
#[derive(Debug)]
enum Category {
TypeA,
TypeB,
Special,
}
fn describe(cat: &Category) -> &str {
match cat {
Category::TypeA => "Type A",
Category::TypeB => "Type B",
}
}
rustc ng_match_exhaustive.rs error[E0004]: non-exhaustive patterns: `Category::Special` not covered
すべてのバリアントを網羅するか、ワイルドカード『_』を使います。
ok_match_exhaustive.rs
#[derive(Debug)]
enum Category {
TypeA,
TypeB,
Special,
}
fn describe(cat: &Category) -> &str {
match cat {
Category::TypeA => "Type A",
Category::TypeB => "Type B",
Category::Special => "Special",
}
}
fn main() {
let categories = vec![
Category::TypeA,
Category::TypeB,
Category::Special,
];
for c in &categories {
println!("{:?}: {}", c, describe(c));
}
}
rustc ok_match_exhaustive.rs ./ok_match_exhaustive TypeA: Type A TypeB: Type B Special: Special
ミス2: データを持つバリアントのパターンマッチ構文ミス
データを持つバリアントをmatchで取り出すときは、タプル形式(丸括弧)と構造体形式(波括弧)の構文を正確に使い分ける必要があります。
ng_pattern_syntax.rs
#[derive(Debug)]
enum Command {
Attack(String, u32),
Move { dx: i32, dy: i32 },
}
fn execute(cmd: &Command) {
match cmd {
Command::Attack { name, damage } => println!("{} ({})", name, damage),
Command::Move(dx, dy) => println!("move {} {}", dx, dy),
}
}
rustc ng_pattern_syntax.rs
error[E0769]: `Attack` is a tuple variant, not a struct variant
--> ng_pattern_syntax.rs:10:9
|
10 | Command::Attack { name, damage } => println!("{} ({})", name, damage),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error[E0532]: expected tuple struct or tuple variant, found struct variant `Command::Move`
--> ng_pattern_syntax.rs:11:9
|
11 | Command::Move(dx, dy) => println!("move {} {}", dx, dy),
| ^^^^^^^^^^^^^^^^^^^^^
タプルバリアントには丸括弧、構造体バリアントには波括弧を使います。
ok_pattern_syntax.rs
#[derive(Debug)]
enum Command {
Attack(String, u32),
Move { dx: i32, dy: i32 },
}
fn execute(cmd: &Command) {
match cmd {
Command::Attack(name, damage) => println!("{} ({})", name, damage),
Command::Move { dx, dy } => println!("move {} {}", dx, dy),
}
}
fn main() {
let commands = vec![
Command::Attack(String::from("action_a"), 120),
Command::Move { dx: 2, dy: 0 },
];
for cmd in &commands {
execute(cmd);
}
}
rustc ok_pattern_syntax.rs ./ok_pattern_syntax action_a (120) move 2 0
記事の間違いや著作権の侵害等ございましたらお手数ですがこちらまでご連絡頂ければ幸いです。