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)
    }
}

キーワード

自分の理解

孤児ルールがあるので、オープンクラスのようなことはできない。