komoto / エンジニアブログ

プログラミングについてアウトプットします。

【Generics】任意の型を受け取り処理をする

最近、テンプレートやジェネリクスを知る機会がありました。これまではある関数やクラスに特定の型を指定して処理をしていました。
しかしこれでは、その特定の型以外が渡された時に同じように処理できません。
キャストをすれば処理を行うことができますが、毎回キャストとかをするのは面倒ですし、あまり良くありません。

そんな時に利用できるのが、ジェネリクス(テンプレート)です。

対象読者

  • テンプレート、ジェネリックスという単語を初めて聞いた人
  • 引数の型の違いを吸収して、同じような処理を書きたいと考えている人
  • テンプレートを用いた書き方を知りたい人(C++, Kotlin)

ジェネリクス(=テンプレート)とは

ジェネリクス(テンプレート)は次のように定義されています。

Genericsとは、クラス、インターフェース、メソッドなどの「型」を「パラメータとして定義する」ことを可能にしたものです。
クラスの定義内で用いられる型を変数化し、インスタンス化しオブジェクトを生成するときまで扱う型を抽象的に表現しておき、生成時に「<」と「>」の間に具体的な型名を指定することで汎用的なクラスやメソッドを特定の型に対応づけることができる機能です。
引用サイト: Javaのジェネリクス (Generics) の使い方を現役エンジニアが解説【初心者向け】 | TechAcademyマガジン

つまりジェネリクス(テンプレート)は、特定の型に依存したコードを他の任意の型でも動作できるようにするなど、型を抽象化しコードを汎用化するために使用します。
受け取る型を制限したり、受け取った任意の型のうち型によって処理を変えたり、型によっては例外処理を行ったりすることもできます。

受け取る型を予想するのは難しいですし、その型の違いによってコードを書くのは大変ですよね。

ちなみに、ジェネリクスという言葉は言語によって異なるみたいです。

C++ → テンプレート
JavaC#、Kotlin → ジェネリクス

と呼び、意味合いも少し変わってくるようです。

テンプレートとジェネリクスの違いについてはこちら

ufcpp.net

この辺りはまだ勉強不足なので割愛させてください...

具体例

それでは簡単な具体例を挙げて考えていきます。

需要はおそらくないと思いますが、今回は引数で受け取った任意のものを3回表示するというメソッドを考えます。

言語はC++Kotlinで書いてきたいと思います。

まずはジェネリクスを使わない場合の書き方から見ていきます。

class MyClass {
public:
    void getThreeInt(int x) {
        for(int i = 0; i < 3; i++) cout << x << endl;
    }

    void getThreeStr(string str) {
        for (int i = 0; i < 3; i++) cout << str << endl;
    }
      ・
      ・
      ・
};

int main(void) {
    auto i = MyClass();
    i.getThreeInt(5); // 5 5 5

    auto str = MyClass();
    str.getThreeStr("Hello"); // Hello Hello Hello

    return 0;
}
class MyClass {
    fun getThreeInt(x: Int) {
        println(x)
        println(x)
        println(x)
    }
    fun getThreeStr(x: String){
        println(x)
        println(x)
        println(x)
    }
   ・
   ・
   ・
}

fun main(args: Array<String>) {
    val i = MyClass()
    i.getThreeInt(5) // 5 5 5

    val str = MyClass()
    str.getThreeStr("Hello") // Hello Hello Hello
}

テンプレートを使わない場合だと、受け取る引数の型ごとにメソッドを定義しておく必要が出てきます。

main関数の方で文字列をInt型に変換して渡せば、メソッドは1つで済みますがスマートでなくあまりメリットはありません。

これをテンプレートを使うと、型の違いを吸収できてとてもスッキリします。

class MyClass {
public:
    template<typename T>
    void getThree(T x) {
        for (int i = 0; i < 3; i++) cout << x << endl;
    }
};

int main(void){
    auto x = MyClass();
    x.getThree(5); // 5 5 5
    x.getThree("Hello"); // Hello Hello Hello

    return 0;
}
class MyClass<T> {
  fun getThree(x: T) {
    println(x)
    println(x)
    println(x)
  }
}

fun main(args: Array<String>) {
    val i = MyClass<Int>()
    i.getThree(5) // 5 5 5

    val str = MyClass<String>()
    str.getThree("Hello") // Hello Hello Hello
}

C++の場合はメソッドの上部にtemplate<typename T>を、Kotlinの場合はクラス宣言時にクラス名の横に<T>をつけることで、型を抽象化できクラスやメソッドを汎用化できます。

上の場合はクラスのメソッドにジェネリクスを適用していますが、クラスのメンバ変数にもジェネリクスを適用したいなら次のようにすることもできます。

template<typename T>
class MyClass {
public:
    void getThree(T x) {
        for (int i = 0; i < 3; i++) cout << x << endl;
    }

    MyClass(T member) {
        this->member = member;
    }

    T getmember() {
        return member;
    }

private:
    T member;
};

int main(void){
        auto x = MyClass<int>(2020);
        cout << x.getmember() << endl; // 2020
        auto y = MyClass<string>("Hello");
        cout << y.getmember() << endl; // Hello
    
        return 0;
}