Chapter 38

構造体と列挙型のジェネリクス

とが
とが
2023.09.03に更新

ジェネリック構造体の定義

第 33 章で,ジェネリクスを使った関数の定義を説明しました.構造体や列挙型も,同様にジェネリクスを使って定義することができます.

まず,i32 型の 2 つの値 xy を用いて平面上の点の座標 (x, y) を表現する構造体を考えます.

struct PointI32(i32, i32);

i32 以外にも, i64f64 など様々な xy の型について同様の構造体が作れます.

struct PointI64(i64, i64);
struct PointF64(f64, f64);

ジェネリクスを用いてこれらをまとめて定義するには,次のように書きます.

struct Point<T>(T, T);

こうすると, Ti32 とした構造体 Point<i32>Ti64 とした構造体 Point<i64>Tf64 とした構造体 Point<f64> などが全ての型 T について一斉に使えるようになります.

こうして定義した Point を使うときは,次のようになります.

let p1: Point<i32> = Point::<i32>(1, 5);
// let p1: PointI32 = PointI32(1, 5);

let p2: Point<i64> = Point::<i64>(1, 5);
// let p2: PointI64 = PointI64(1, 5);

コメントアウトした行は,ジェネリクスで書き換える前の PointI32 PointI64 を使った場合の書き方です.このように,単に Point<i32> と書いて良い場合と,Point::<i32> と書かなければいけない場合があります.後者の ::<> をターボフィッシュといいます.

ターボフィッシュを用いて Point::<i32> と書かなければいけない理由は,Point<i32> だと 1 < 2 のような比較に用いる演算子 <> と区別できない状況が存在するためです.しかし,型の名前に比較演算子 <> が現れることはありません.型注釈の : や関数定義における -> の後には型が来ると分かっているので,ターボフィッシュは必要ありません.

ジェネリクスを使って同時に定義されていても,Point<T>Point<U> は(TU が違えば)別の型です.よって次はコンパイルエラーになります.

コンパイルエラー
let p: Point<i32> = Point::<i64>(1, 5);

型推論

Point<T> を使うとき,型パラメータを与える代わりに _ と書くと,その部分の型を推論させることができます.

let p: Point<_> = Point::<f64>(1., 5.);

pPoint<f64> 型の値を代入しているので,p の型も Point<f64> でなければなりません.よって _ の部分は f64 に推論されます.

let p: Point<i64> = Point::<_>(1, 5);

今度は p の型が Point<i64> なので,= の右辺も Point<i64> でなければなりません.よって _ の部分は i64 に推論されます.

関数のジェネリクスと同様,Point::<_>(1, 5)::<_> を省略して単に Point(1, 5) と書けます.

let p: Point<i64> = Point(1, 5);

問題.型注釈 : Point<i64> を取り去って,次のように書いたら p の型は何になりますか?

let p = Point(1, 5);
答え

型を決める情報が他に無いとき整数リテラルは自動で i32 になるので, Point<i32> です.

impl

ジェネリクスを使って Point<i32> Point<i64> Point<f64> ……などを一斉に定義したとき,これらに対し個別にメソッドや関連関数を実装することができます.たとえば,

struct Point<T>(T, T);

impl Point<i32> {
    fn abscissa(&self) -> &i32 {
        &self.0
    }
}

と書くと,Point<i32> 型の変数 p に対し p.abscissa() と書いて x 座標を取り出すことができるようになります.

let p = Point(5, 2);
assert_eq!(*p.abscissa(), 5);

この場合 p.abscissa()Point::<i32>::abscissa(&p) と同じ意味です(これも <i32> ではなく ::<i32> と書く必要があります.ただしこの i32 は推論できるので Point::abscissa(&p) でも構いません).

abscissa()Point<i32> にしか実装されていないため,たとえば Point<i64> 型の値に対して abscissa() を呼び出すことはできません.

コンパイルエラー
Point::<i64>(5, 2).abscissa();

impl Point<i32> { } だけでなく impl Point<i64> { } の中でも abscissa() を定義すれば,Point<i64> 型の値に対しても abscissa() を呼び出すことができるようになります.

struct Point<T>(T, T);

impl Point<i32> {
    fn abscissa(&self) -> &i32 {
        &self.0
    }
}
impl Point<i64> {
    fn abscissa(&self) -> &i64 {
        &self.0
    }
}

fn main() {
    assert_eq!(*Point::<i32>(5, 2).abscissa(), 5);
    assert_eq!(*Point::<i64>(5, 2).abscissa(), 5);
}

同様に impl Point<f32> { }impl Point<f64> { } の中でも abscissa() を定義すれば,Point<f32> 型や Point<f64> 型の値に対しても abscissa() が呼べるようになります.

ここで impl 自体に <T> を付けると,これらの impl を一度に行うことができます.

struct Point<T>(T, T);

impl<T> Point<T> {
    fn abscissa(&self) -> &T {
        &self.0
    }
}

fn main() {
    assert_eq!(*Point::<i32>(5, 2).abscissa(), 5);
    assert_eq!(*Point::<i64>(5, 2).abscissa(), 5);
}

implimpl<T> に変わり,impl Point<i32> { }impl Point<i64> { } において i32i64 だった部分が全て T に置き換わっています.こうすることで, Ti32 になったときの implTi64 になったときの implTf64 になったときの impl などが全ての型 T について一斉に実装されます.そのため,Point<i32> 型の値に対しても Point<i64> 型の値に対しても同じように abscissa() が呼べています.