これまでもなんとなくは見てきてたけど、こういう名前があるとも知らんかったので。
こういうやつ
use std::marker::PhantomData;
// 状態をそれぞれstructにして
struct Open;
struct Closed;
struct Locked;
// ジェネリクスで指定し
struct Door<State> {
// マーカーを置く
_state: PhantomData<State>,
}
// その型ごとに実装する
impl Door<Open> {}
impl Door<Closed> {}
impl Door<Locked> {}
PhantomData
はその名の通り幻影のような存在感で、コンパイルされると消えるそうな。
PhantomData - The Rustonomicon https://doc.rust-lang.org/nomicon/phantom-data.html
基盤としてのstructは共通しつつ、状態ごとに実装を明確に分けられるのが便利というパターンらしい。
共通の実装がしたい
分けられるのが嬉しいのはわかったけど、分けたくないシーン、サボりたいシーンもあるはず・・・。
共通のstruct側に実装は書くけど、ちょっと分岐したい、みたいな。
こうすればできた。
impl<State: 'static> Door<State> {
fn can_enter(&self) -> bool {
if TypeId::of::<State>() == TypeId::of::<Open>() {
return true;
}
false
}
}
ただこれは、TypeId
での比較がランタイムコストになるので、PhantomData
の意味が薄れてしまう。
調べる限り、traitにしちゃうのがベターらしい。
trait DoorState {
const CAN_ENTER: bool;
}
impl DoorState for Open {
const CAN_ENTER: bool = true;
}
impl DoorState for Closed {
const CAN_ENTER: bool = false;
}
impl DoorState for Locked {
const CAN_ENTER: bool = false;
}
impl<State: DoorState> Door<State> {
fn can_enter(&self) -> bool {
State::CAN_ENTER
}
}
コードは増えるけど、よりRustyらしい。
このコード例が簡単すぎてあまり説得力はない(個別に実装したらええやんとなるから)けど、まあやり方の一つということで。
ライブラリもある
共通の実装の話はさておき、本来のType Stateだけに目を向けると、
PhantomData
のためのstructが冗長- 状態遷移ももっと宣言的に書きたい
となるはずで、それに立ち向かってるcrateを公開してる人もいた。
ozgunozerk/state-shift: Macros for implementing Type-State-Pattern on your structs and methods https://github.com/ozgunozerk/state-shift