Rust での Newtype パターンの適用方法について。
Newtype パターンは既存の型をラップし、新しい型として使うデザインパターンである。
なお、Rust で既存の型 Old
を新しい型 New
で単純にラップするには、sturuct New(Old);
と定義すればよい (ラップ先からラップ元へのアクセスは self.0
と記述)。これらは単純なラップのため、最適化によりゼロコストで実装できる。
以下は主な Newtype パターンの使用目的。
既存の型が汎用的すぎる場合、Newtype で特殊化するとよい。
例えば、数値型から距離型と質量型を作れば、それらを取り違えるミスは減る。また、それらそれぞれに専用関数も用意できるようになる。
以下では、f32
型から Length
型を作り、専用関数 from_km
を用意している。
use std::ops::Add;
fn main() {
let length1 = Length(300.0);
let length2 = Length::from_km(1.0);
assert!(length1 + length2 == Length(1300.0));
}
#[derive(PartialEq)]
struct Length(f32);
impl Length {
pub fn from_km(value: f32) -> Self {
Self(value * 1000.0)
}
}
impl Add for Length {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self(self.0 + rhs.0)
}
}
既存の型が完成済で編集できない場合、Newtype で新しい型を作るとよい。
例えば、Rust には孤児ルールがあるため、他クレート由来の型に他クレート由来のトレイトを新たに実装できない。そこで、既存の型へと処理を移譲するだけの新しい型を自クレート内に作成し、このルールに抵触しないようにする。
以下では、クレート sub1
の型 SomeStruct
にクレート sub2
のトレイト SomeTrait
を直接は実装できないため、SomeStruct
を Newtype
型でラップしている。
Newtype パターンは有用だが、ラップ用の単調なコードが長くなりやすい。例えば、数値型では四則演算用の関数のラップなどが必要になる。また、割と大きな型の全ての関数をラップすべきという状況も普通にありうる。
状況によっては下記の対策が使える。それ以外の場合、"Newtype" の名前の通り、既存の型の転用ではなく、新しい型の作成と割り切ったほうが良さそうである。
derive
実装
derive
実装できる箇所はまずはそれを使おう。なお、トレイトによっては derive
対応していても適用に追加要件があるが、Newtype パターンでは唯一のフィールドをラップするだけなので、まず問題にならない。
そして、Newtype パターンでのラップを最初から想定している型では、セットでラップ用のトレイトを提供してもよい (derive
対応がかなり面倒だが…)。
マクロを利用すれば、数値型の四則演算などの普遍的処理は、いくらかコードを短縮できる。ただし、マクロは純粋に構文木変換用のフィルタであるため、たとえ手続型マクロでも入力ソース以外にはアクセスできない。そのため、ラップ対象のメソッドやその引数の情報が得られず、原理的に全自動でのラップは不可能である。
Deref
継承
Deref
継承と呼ばれる手法を使えば、本来はスマートポインタのために使われる Deref
トレイトを流用して、全処理をラップ先に転送できる。ただし、これはアンチパターンの一種で、あまりお勧めできない。なぜなら、関数がラップ元とラップ先どちらにあるるかは、微妙だが本質的な使用感の違いになる場合があり、混乱を招きやすい。