ジェネリック(generic)を理解しよう!

[C#] ジェネリック(generic)を理解しよう!

※ 当サイトは広告を含みます。

ここではC#のジェネリックを学習します。もしくはジェネリック型と呼びます。
また、C++だとテンプレートと呼ばれ、機能的にはC#よりも優れてます。

ジェネリック(generic)

ジェネリックとは、汎用的な型を利用してメソッドやクラスの処理を共通化する機能です。
そして、この時に利用する型をジェネリック型(generic type)と言います。

管理人

Genericは英語で汎用とか一般を意味します(たぶん)。

りさ

汎用的に使える型って意味ですか?

管理人

そうだよ(適当)。

りさ

雑...

ジェネリックを理解するにあたり、次のようなint型の数値を入れ替えるメソッドを考えます。
このメソッドは、2つのint型の引数をref参照で受け取り、参照先の数値を入れ替えます。


namespace Sample
{
  internal class Program
  {
    static void Main(string[] args)
    {
      int i1 = 0;
      int i2 = 8;

      Console.WriteLine($"i1={i1}, i2={i2}");

      Swap(ref i1, ref i2);

      Console.WriteLine($"i1={i1}, i2={i2}");
    }

    // int型の数値を入れ替える
    internal static void Swap(ref int a, ref int b)
    {
      var tmp = a;
      a = b;
      b = tmp;
    }
  }
}

さらにdouble型の数値を入れ替えるメソッドを考えましょう。


namespace Sample
{
  internal class Program
  {
    static void Main(string[] args)
    {
      int i1 = 0;
      int i2 = 8;

      double d1 = 12.34;
      double d2 = 56.78;

      Console.WriteLine($"i1={i1}, i2={i2}, d1={d1}, d2={d2}");

      Swap(ref i1, ref i2);
      Swap(ref d1, ref d2);

      Console.WriteLine($"i1={i1}, i2={i2}, d1={d1}, d2={d2}");
    }

    // int型の数値を入れ替える
    internal static void Swap(ref int a, ref int b)
    {
      var tmp = a;
      a = b;
      b = tmp;
    }

    // double型の数値を入れ替える
    internal static void Swap(ref double a, ref double b)
    {
      var tmp = a;
      a = b;
      b = tmp;
    }
  }
}

では、ここで作成したint型とdouble型の2つのメソッドを比較してみましょう。


// int型の数値を入れ替える
internal static void Swap(ref int a, ref int b)
{
  var tmp = a;
  a = b;
  b = tmp;
}

// double型の数値を入れ替える
internal static void Swap(ref double a, ref double b)
{
  var tmp = a;
  a = b;
  b = tmp;
}
りさ

ほぼ同じですね。

管理人

仮に追加でchar型の値を入れ替えたいと思ったらどうする?

りさ

また同じようなメソッドを作ります。

管理人

そういう時に便利なのがジェネリックだよ。

先程の例のような、どう見ても同じような処理で型が違うパターンを解決してくれるのがジェネリックです。
それでは早速ジェネリックを利用したパターンに書き換えてみましょう。次のコードがジェネリック版です。


namespace Sample
{
  internal class Program
  {
    static void Main(string[] args)
    {
      int i1 = 0;
      int i2 = 8;

      double d1 = 12.34;
      double d2 = 56.78;

      Console.WriteLine($"i1={i1}, i2={i2}, d1={d1}, d2={d2}");

      // ジェネリック型の呼び出し
      Swap<int>(ref i1, ref i2);
      Swap<double>(ref d1, ref d2);

      Console.WriteLine($"i1={i1}, i2={i2}, d1={d1}, d2={d2}");
    }

    // ジェネリック型
    internal static void Swap<T>(ref T a, ref T b)
    {
      var tmp = a;
      a = b;
      b = tmp;
    }
  }
}

まず、次の箇所がジェネリック型メソッドの定義です。この<T>がジェネリック型を意味します。


// ジェネリック型
internal static void Swap<T>(ref T a, ref T b)
{
  var tmp = a;
  a = b;
  b = tmp;
}

そして、このメソッドを呼び出してる部分がこれ。<>で引数の型を指定します。


// ジェネリック型の呼び出し
Swap<int>(ref i1, ref i2);
Swap<double>(ref d1, ref d2);

ちなみに呼び出し時は型推論できるため、<型>は書かなくても動きます。


// ジェネリック型の呼び出し
Swap(ref i1, ref i2);
Swap(ref d1, ref d2);

このように<T>を利用することで、型だけ異なる同じような処理を共通化できます。
また、<T>Tは単なる名前です。別の名前、例えば<Type>でも普通に動きます。

管理人

ただ、ほぼほぼ<T>と書きます。for文のi,j,kみたいな感じです。

このジェネリック型を利用した記述は2つあり、メソッドまたはクラスを対象にジェネリック型を定義します。
両者の違いはジェネリック型の有効範囲です。メソッドならメソッド内、クラスならクラス内が対象です。

ジェネリック型のメソッド(Generic methods)

これが先程のパターンです。ジェネリック型の範囲はメソッド内のみとなります。


// ジェネリック型のメソッド
internal static void Swap<T>(ref T a, ref T b)
{
  var tmp = a;
  a = b;
  b = tmp;
}
ジェネリック型のメソッド

ジェネリック型のクラス(generic classes)

こちらはクラスにジェネリック型を指定したパターンです。この場合の有効範囲はクラス内の全てです。
つまり、フィールドやプロパティも共通のジェネリック型を利用することができます。


namespace Sample
{
  internal class Program
  {
    // ジェネリック型のクラス
    internal class View<T>
    {
      private T value1;
      private T value2;

      public View(T value1, T value2)
      {
        this.value1 = value1;
        this.value2 = value2;
      }

      public void Display()
      {
        Console.WriteLine($"value1={this.value1}, value2={this.value2}");
      }
    }

    static void Main(string[] args)
    {
      int i1 = 0;
      int i2 = 8;

      double d1 = 12.34;
      double d2 = 56.78;

      // ジェネリック型のインスタンスを生成
      var v1 = new View<int>(i1, i2);
      var v2 = new View<double>(d1, d2);

      v1.Display();
      v2.Display();
    }
  }
}

クラスの定義と一緒に<T>と書くことで、そのクラス内では定義したT型(ジェネリック型)を利用できます。


// ジェネリック型のクラス
internal class View<T>
{
  private T value1;
  private T value2;

  public View(T value1, T value2)
  {
    this.value1 = value1;
    this.value2 = value2;
  }

  public void Display()
  {
    Console.WriteLine($"value1={this.value1}, value2={this.value2}");
  }
}
ジェネリック型のクラス

インスタンスの生成もメソッドと同じく<>で型を指定します。
なお、こちらは型推論は不可能。必ず利用する型名を記述します。


// ジェネリック型のインスタンスを生成
var v1 = new View<int>(i1, i2);
var v2 = new View<double>(d1, d2);

whereキーワード

whereキーワードは、ジェネリック型に対して制約を付加するために利用します。
制約と聞くとマイナスなイメージですが、C#ではこれを使わないとジェネリックが機能しません。

先に次のようなメソッドを考えます。

2つの数値を引数で受け取り、それを合算した値を返すメソッドです。
つまり単なる足し算です。ジェネリック型を利用すれば汎用的なメソッドを記述できそうですね。


namespace Sample
{
  internal class Program
  {
    static void Main(string[] args)
    {
      int i1 = 0;
      int i2 = 8;

      double d1 = 12.34;
      double d2 = 56.78;

      // ジェネリック型の呼び出し
      var i = Addition<int>(i1, i2);
      var d = Addition<double>(d1, d2);

      Console.WriteLine($"i1+i2={i}, d1+d2={d}");
    }

    // ジェネリック型の足し算
    internal static T Addition<T>(T a, T b)
    {
      return a + b;
    }
  }
}
管理人

こんな感じ。それでは実行してみよう。

りさ

これビルドエラーになります。

管理人

それが正しい。

実はこの記述では動きません。C++とかだと動くのですが、C#は型に厳しいことが影響してビルドエラーになります。
その理由は凄く単純です。ジェネリック型、つまりT型足し算(+演算子)できる保証がないからです。
当然ですが、ジェネリック型はどんな型でも受け入れ可能です。その時に+演算子を利用できる保証がないのでビルドエラーになりました。

それを解決してくれるのがwhereキーワードです。これは受け入れ可能な型を絞り、演算可能なジェネリック型であることをコンパイラに示します。
逆を言えば、ジェネリック型の範囲から演算が不可能な範囲を除外します。これにより絶対に演算できることが保証されるとビルドエラーが消えます。

では、実際にwhereキーワードを使って、先程のパターンを改善しましょう。


namespace Sample
{
  internal class Program
  {
    static void Main(string[] args)
    {
      int i1 = 0;
      int i2 = 8;

      double d1 = 12.34;
      double d2 = 56.78;

      // ジェネリック型の呼び出し
      var i = Addition<int>(i1, i2);
      var d = Addition<double>(d1, d2);

      Console.WriteLine($"{i1}+{i2}={i}, {d1}+{d2}={d}");
    }

    // ジェネリック型の足し算
    internal static T Addition<T>(T a, T b)
      where T : System.Numerics.INumber<T>
    {
      return a + b;
    }
  }
}

ポイントはwhereでINumber型を指定しました。よって、このジェネリック型のメソッドはINumber型を実装するクラスしか受け入れできません。


where T : System.Numerics.INumber<T>

このようにwhereで範囲を絞ることで、必要とする機能を有することをコンパイラに証明します。
ちなみにジェネリック型のクラスに記述する場合はこんな感じ。単に記述する場所が違うだけです。


namespace Sample
{
  internal class Program
  {
    // ジェネリック型のクラス
    internal class AdditionCalculator<T>
      where T : System.Numerics.INumber<T>
    {
      private T value1;
      private T value2;

      public AdditionCalculator(T value1, T value2)
      {
        this.value1 = value1;
        this.value2 = value2;
      }

      public T Calculate()
      {
        return this.value1 + this.value2;
      }
    }

    static void Main(string[] args)
    {
      int i1 = 0;
      int i2 = 8;

      double d1 = 12.34;
      double d2 = 56.78;

      // ジェネリック型のインスタンスを生成
      var ac1 = new AdditionCalculator<int>(i1, i2);
      var ac2 = new AdditionCalculator<double>(d1, d2);

      Console.WriteLine($"{i1}+{i2}={ac1.Calculate()}, {d1}+{d2}={ac2.Calculate()}");
    }
  }
}

このwhereキーワードには、直接的なクラスやインターフェイスだけではなく、何らかのクラスであるとか、nullではないって大雑把な記述もできます。
記述可能なパターンの例は以下です。もっと複雑な絞り込みが必要なら公式ドキュメントを確認してください。

where T : structT型はrecord struct型を含むnull非許容値型である。
where T : classT型は参照型である。
where T : notnullT型はnull非許容値型である。
where T : unmanagedT型はnull非許容値型かつunmanaged型である。
where T : <基底クラス名>T型は指定した基底クラスか派生クラスである。
where T : <インターフェイス名>T型は指定したインターフェイスを実装している。

公式ドキュメントhttps://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/constraints-on-type-parameters
https://learn.microsoft.com/en-us/dotnet/standard/generics/math

また、whereキーワードに記述する制約は,(カンマ)を利用して複数個を指定することができます。
ただし、条件が矛盾するような内容を記述するとビルドエラーになります。


where T : unmanaged, System.Numerics.INumber<T>

あとがき

このジェネリック型ですが、Library制作者でもない限り自分で作ることは殆どありません。
大抵の人は.NETに定義された大量のジェネリック型を利用するために使います。

◆ C#に関する学習コンテンツ

この記事は参考になりましたか?

関連記事

コメント

この記事へのコメントはありません。