rustのジェネリクスを理解しよう

rust

この記事で書くこと

  • ジェネリクスとは
  • ジェネリクスの書き方
  • ジェネリクスがあることの嬉しさ

ジェネリクスとは

genericsの意味を調べてみると、以下のようなものでした。

  • generics:一般的な

つまり、ジェネリクスを使用することでより、「一般的な」処理を書くことができます。何に対して「一般的」にするのかというと、に対してです。

ジェネリクスの書き方

ジェネリクスは型に対して、一般性を持たせることができます。ジェネリクスは、構造体、enum、関数などで使用できます。ここでは、構造体で使用することにします。例えば、構造体の要素に対して、一般性を持たせるには以下のように書きます。

struct Hoge<T> {
    hoge_elm: T,
}

fn main() {
    let a = Hoge { hoge_elm: 10 };
    let b = Hoge { hoge_elm: "Hoge".to_string() };
    let fuga = Fuga { fuga_elm: 100 };
    let c = Hoge { hoge_elm: fuga };
    println!("{}", a.hoge_elm);
    println!("{}", b.hoge_elm);
    println!("{}", c.hoge_elm.fuga_elm);
}

struct Fuga {
    fuga_elm: usize,
}

実行結果

10
Hoge

コード解説

構造体定義の際、Tをジェネリックな変数として定義します。そしてhの要素をT型します。このTには、任意の型を設定することができます。上記の例では、aでは整数型のデフォルトであるi32、bではString、cでは自分で作った構造体のFugaが Tの型として指定されます。

ジェネリクスの嬉しさ

ジェネリクスの嬉しさは、型に一般性を持たせることで、様々な型に対応した処理を記述できることです。上記のコードの例でいくと、ジェネリクスを使わない場合、以下のようなコードになります。

struct Hoge_i32 {
    hoge_elm: i32,
}

struct Hoge_String {
    hoge_elm: String,
}

struct Hoge_Fuga {
    hoge_elm: Fuga,
}


fn main() {
    let a = Hoge_i32 { hoge_elm: 10 };
    let b = Hoge_String { hoge_elm: "Hoge".to_string() };
    let fuga = Fuga { fuga_elm: 100 };
    let c = Hoge_Fuga { hoge_elm: fuga };
    println!("{}", a.hoge_elm);
    println!("{}", b.hoge_elm);
    println!("{}", c.hoge_elm.fuga_elm);
}

struct Fuga {
    fuga_elm: usize,
}

処理は同じなのに型によって、別の構造体を作成しなければなりません。また、同じ名前の構造体は許されないので、型名を付与する等の処置をしなければなりません。ジェネリクスを使うと、このような「処理は同じだが複数の型に適用したい」といった場合に記述量を少なく書くことができます。また、ユーザーが作成した新たな型にも対応できます。

ジェネリクスの挙動

ジェネリクスを用いた処理は、実際に先ほど書いたコードのように、それぞれの型に展開されます。

以下のジェネリクスを使った構造体定義について考えてみます。

struct Hoge<T> {
    hoge_elm: T,
}

この構造体は、コードの中でi32とStringで使われると、以下のようなコードへとコンパイルされます。ちなみに、このコードはコンパイル時に生成されるものなので、ソースファイルに書いてもコンパイルできません。コードの中で、どの型を使用しているのかは、コンパイラが判断してくれます。

struct Hoge<i32> {
    hoge_elm: i32,
}

struct Hoge<String> {
    hoge_elm: String,
}

コンパイル後、ジェネリクスがそれぞれの型に展開されている例として、i32型だけに適用されるメソッドを書いてみます。

struct Hoge<T> {
    hoge_elm: T,
}

impl<T> Hoge<T> {
    fn ya(&self) {
        println!("ya");
    }
}

impl Hoge<i32> {
    fn hi(&self) {
        println!("hi");
    }
}


fn main() {
    let a = Hoge { hoge_elm: 10 };
    let b = Hoge { hoge_elm: "Hoge".to_string() };

    a.ya();
    a.hi();
    b.ya();
    //b.hi(); // not allowed

}

こちらのコードでは、どんな型でも実装されるyaメソッドと、i32型にのみ実装されるhiメソッドを書いています。任意の型に対してメソッドを実装するHoge<T>に対してメソッドを記述します。i32型のみに実装したいメッソどはHoge<i32>に対してメソッドを記述します。

hiメソッドがi32の型のみに実装されるのは、ジェネリクスがコンパイル時に、i32型とString型に書き分けられるからです。その書き分けられた型の、i32の方にのみhiメソッドを実装できるのです。

最後に

ここではジェネリクスの基礎について記載しました。この記事ではジェネリクスを使って構造体を定義しました。ジェネリクスは、構造体だけでなく、関数やトレイトでも使われます。関数で使う際には、トレイト境界についての知識が必要になってきます。

コメント

タイトルとURLをコピーしました