ジェネリックとは?

ジェネリックとは?

おはようございます。

今回は C#のジェネリックについて書いていきます。

ジェネリックとは?

ジェネリック(Generics)とは、メソッドやクラスなどで内部使用するデータの型を、可変的に使えるように汎用的なメソッドやクラスを作る機能のことです。

クラス、メソッド、インターフェース、デリゲートに対して型パラメータを与えることでジェネリックな型・メソッドを定義することができます。

ジェネリッククラスの定義方法

"ジェネリッククラスの定義方法"
public class GenericsClass <型パラメータ1, 型パラメータ2, ...> {}

キーと値をフィールドに持つクラスを例に使用例を見ていきます。

using System;

public class Hello{

  public static void Main(){
    // ジェネリッククラスのインスタンス化
    var keyValue1 = new KeyValuePair<string, int>
    {
      Key = "one",
      Value = 1
    };
    // Your code here!
    Console.WriteLine(keyValue1.Key); // one
    Console.WriteLine(keyValue1.Value); // 1

    var keyValue2 = new KeyValuePair<int, double>
    {
      Key = 12345, // 数値型を指定
      Value = 3.14 // float型を指定
    };

    Console.WriteLine(keyValue2.Key); // 12345
    Console.WriteLine(keyValue2.Value); // 3.14
  }

  // ジェネリッククラスを定義
  public class KeyValuePair<TKey, TValue>
  {
    public TKey Key;
    public TValue Value;
  }
}

keyValue1 では、文字列型と数値型を指定していますが、keyValue2 では数値型と double 型を指定しています。このように、同じクラスでも可変的に型を指定することがジェネリックの特徴になります。

以下、実際のコードです。今回も Paiza のオンラインエディタを使用して検証しました。

ジェネリックの利点

先程のコードの例を元にジェネリックの利点を見ていきます。

型パラメータを指定しないで同じように実装していくと、格納したいデータの型ごとにクラスを用意する必要があります。

using System;

public class Hello{

  public static void Main(){
    // ジェネリッククラスのインスタンス化
    var keyValue1 = new KeyValuePair1
    {
      Key = "one",
      Value = 1
    };
    // Your code here!
    Console.WriteLine(keyValue1.Key); // one
    Console.WriteLine(keyValue1.Value); // 1

    var keyValue2 = new KeyValuePair2
    {
      Key = 12345, // int型を指定
      Value = 3.14 // float型を指定
    };

    Console.WriteLine(keyValue2.Key); // 12345
    Console.WriteLine(keyValue2.Value); // 3.14

    var keyValue3 = new KeyValuePair3
    {
      Key = 1, // int型を指定
      Value = new int[3] // int型のみ格納できる配列を指定
    };

    Console.WriteLine(keyValue3.Key); // 1
    Console.WriteLine(keyValue3.Value); // System.Int32[]
  }

  // string型とint型を格納したい
  public class KeyValuePair1
  {
    public string Key;
    public int Value;
  }

  // int型とdouble型を格納したい
  public class KeyValuePair2
  {
    public int Key;
    public double Value;
  }

  // 配列とint型を格納したい
  public class KeyValuePair3
  {
    public int Key;
    public int[] Value;
  }

  // ○○型と○○型を格納するクラスを作りたい
  // :
  // :
  // ○○型と○○型を格納するクラスを作りたい
  // :
  // :
}

最初は 1 つ 2 つのクラスで問題なかったとしても、機能の修正などで必要なクラスが増えてくるかもしれません。そうなったときに格納する型に依存していると膨大なコードを書く必要が出てくる可能性があります。

ジェネリッククラス・メソッドを使用することで、型に依存することがないコードをかくことができるので少ないコード量で済みます。

型パラメータ制約

オプションで型パラメータに対する制約を指定することができます。指定できる内容は主に表のような内容です。

制約構文概要
基本クラス制約where T : MyclassT 型は Myclass クラス、または Myclass 派生クラスであること
インターフェース制約where T : MyinfaceT 型は Myinface インターフェースを実装していること
値型制約where T : structT 型は 値型であること
参照型制約where T : classT 型は 参照型であること
コンストラクター制約where T : new()T 型は がデフォルトコンストラクターを持つこと

型パラメータを制約することで、制約型の操作とメソッドの呼び出しを使用できるようになります。実際にコードを見てみます。

"エラーとなる書き方"
using System;

public class Hello{
    public static void Main(){
        var m = new MyGenerics<int>();
        Console.WriteLine(m.Hoge(2, 1));
    }

    class MyGenerics<T>
    {
      public int Hoge(T x, T y)
      {
        return x.CompareTo(y); // CS1061エラーになる
      }
    }
}

上記のコードはエラーになります。T 型の変数 x、y が必ずしも CompareTo メソッドを持つとは限らないからです。この場合、T 型が CompareTo メソッドを持つことを前提とした実装が必要になります。

CompareTo メソッドを持つことは IComparable インターフェースを実装している事になります。

"インターフェース制約"
using System;

public class Hello{
    public static void Main(){
        var m = new MyGenerics<int>();
        Console.WriteLine(m.Hoge(2, 1));
    }

    class MyGenerics<T> where T : IComparable<T>
    {
      public int Hoge(T x, T y)
      {
        // CompareToメソッドを認識できる
        return x.CompareTo(y); // 1
      }
    }
}

因みに、CompareTo メソッドは左の値 - 右の値を実行して、大小の判定を行います。左の値のほうが大きければ 1 を返し、右の値の方が大きければ-1 を返します。同じ値の場合は 0 を返します。

ジェネリックメソッド

ジェネリックメソッドとは、メソッドの引数や配下のローカル変数の型をメソッドを呼び出す際に決められるメソッドのことです。

"ジェネリックメソッドの定義方法"
public void GenericsMethod <型パラメータ1, 型パラメータ2, ...>(引数の型 引数名, ...)
{
  // メソッド内の処理
}

// 戻り値がある場合は void は戻り値の型になる

実際に例を見てみましょう。

using System;

public class Hello{
    public static void Main(){
        TestSwapDouble();
        TestSwapint();
    }

    // 値を入れ替える処理を行うメソッド
    static void Swap<T>( T arg1, T arg2)
    {
        T temp;
        temp = arg1;
        arg1 = arg2;
        arg2 = temp;
    }

    // ジェネリックメソッドを呼び出す
    public static void TestSwapDouble()
    {
        double a = 1.3434;
        double b = 2.12342;

        Swap<double>(a, b);

        Console.WriteLine(a + " " + b);
    }

    // こっちはint型
    public static void TestSwapint()
    {
        int a = 1;
        int b = 2;

        Swap<int>(a, b);

        Console.WriteLine(a + " " + b);
    }
}

呼び出し元のメソッドには手を加えていませんが、呼び出す側のメソッドで任意の型を指定することで異なる型の値の処理を実行しています。

これもジェネリッククラスと同じく汎用性の高いコードを作成することができるので、コード量を少なくすることができます。

まとめ

  • ジェネリックとは可変的に使えるクラスやメソッドを定義する方法
  • ジェネリックを使用することで汎用性のあるクラスやメソッドが書けるので、少ないコードで処理を書くことができる。
  • 型制約を使用することで、厳格にルールを決めることもできる。