TL;DR
- Newtype パターンを使うと、同じ内部型を持つ型どうしを区別でき、孤児ルールも回避できる。
- 一方で、Newtype に対する操作を内部の型に委譲する手間がかかる。
内容
https://effective-rust.com/newtype.html
型を区別する
型エイリアスは、事実上ただのドキュメントにすぎない。次のコードでは PoundForceSeconds と NewtonSeconds を区別できず、NewtonSeconds を求める関数に PoundForceSeconds を渡してもコンパイルが通ってしまう。
pub type PoundForceSeconds = f64;
pub type NewtonSeconds = f64;
pub fn update_trajectory(force: NewtonSeconds) { /* ... */ }Newtype パターンでタプル構造体にすれば、取り違えはコンパイルエラーになる。From を実装しておけば、変換も簡単に書ける。
pub struct PoundForceSeconds(f64);
pub struct NewtonSeconds(f64);
impl From<PoundForceSeconds> for NewtonSeconds {
fn from(val: PoundForceSeconds) -> Self {
NewtonSeconds(4.44822 * val.0)
}
}
let new_direction = update_trajectory(thruster_force.into());Newtype パターンは、bool の引数の曖昧さをなくすのにも使える。
struct DoubleSided(pub bool);
struct ColorOutput(pub bool);
fn print_page(sides: DoubleSided, color: ColorOutput) { /* ... */ }メモリ効率やバイナリ互換性が気になるなら、#[repr(transparent)] 属性を付けると、内部の型と同じメモリ表現になることが保証される(参考: https://lo48576.gitlab.io/rust-custom-slice-book/defining-slice-types/repr-transparent.html )。
孤児ルールの回避
孤児ルールにより、クレートがある型にトレイトを実装できるのは、そのクレートがトレイトか型のどちらかを定義している場合だけである。そのため、外部クレートの型 rand::rngs::StdRng に標準の fmt::Display を実装するとコンパイルエラーになる(E0117)。
このルールは曖昧さを避けるためにある。依存関係にある 2 つのクレートが同じ型に同じトレイトを実装していたら、コンパイラやリンカはどちらを選べばよいか分からない。
Newtype で包んだ型は自分のクレートで定義した型になるので、トレイトを実装できる。
struct MyRng(rand::rngs::StdRng);
impl fmt::Display for MyRng {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "<MyRng instance>")
}
}Newtype の制限
Newtype に対する操作は、すべて内部の型に委譲する必要がある。値を使うたびに .0 が要り、トレイトの実装も Newtype 自身に対して改めて書く必要がある。
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub struct NewType(InnerType);
impl fmt::Display for NewType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
self.0.fmt(f)
}
}キーワード
自分の理解
孤児ルールがあるので、オープンクラスのようなことはできない。