ジェネリクス

目次

キーワード

概要

C# 2.0 で、 C++でいうところのテンプレート、一般にはジェネリクスなどと呼ばれるものが実装されました。 (C++ のテンプレートとは少し仕様がことなりますが。)

ジェネリックス(generics:総称性)、 あるいは、総称的プログラミング(generic programming)とも呼ばれますが、 この機能は、 さまざまな型に対応するために、型をパラメータとして与えて、その型に対応したクラスや関数を生成するもの機能です。

ジェネリックスの例

ジェネリックメソッド

例えば、2つの値の大きいほうをとる関数(静的メソッド)、Max を作りたいとします。 int型に限定したものなら簡単に作れて、以下のようになります。

int Max(int x, int  y)
{
  return x > y ? x : y;
}

ところが、同じことをdouble型で行おうとすると、同じような関数をもう一つ追加してやる必要があります。

double Max(double x, double y)
{
  return x > y ? x : y;
}

この2つの関数は、引数の型が int から double に変わったところ意外はまったく同じコードになっています。 このように、まったく同じコードを複数箇所に書くのは、書くのも面倒ですし、保守もしづらくなるのでなるべくしたくありません。

この問題に対して、 ジェネリックスというものを用いれば、 必要に応じていろいろな型に対応した Max 関数を生成できます。 Max 関数のジェネリックス版は以下のようになります。

public static Type Max<Type>(Type a, Type b)
  where Type : IComparable
{
  return a.CompareTo(b) > 0 ? a : b;
}

このように、 メソッド名の後ろに、< > で囲って、 型をパラメータとして与えることができます。

(C++ のテンプレートと違って)C# のジェネリクスを使うと、 比較などの演算子は使えなくなってしまうので、 わざわざ CompareTo を使う必要があったり、 多少の不便はありますが、 それでも、いちいち int 版と double 版を分けて書かなくてはいけないという問題は解決できます。 (where については後ほど説明します。)

ジェネリックス版の Max 関数は以下のようにして呼び出します。

int    n1 = Max<int>(5, 10);   // int 版の Max を明示的に呼び出し
int    n2 = Max(5, 10);        // int 版の Max が自動的に生成される
double x  = Max(5.0, 10.0);    // double 版の Max が自動的に生成される
string s  = Max("abc", "cat"); // string 版の Max が自動的に生成される(辞書式順序で比較)

ジェネリッククラス

関数と同じく、クラスでもさまざまな型に対応したものを作成したいときがあります。 例えば、コレクションクラス(配列とかリストとかの、物の集まりのこと)などがその典型です。

ここでは例としてスタックを考えて見ましょう。 これも格納できる型を特定の型に限ったものは簡単に作成できます。

// int 専用版スタッククラス
// エラー処理とかはサボっています
class StackInt
{
  int[] buf;
  int top;
  public StackInt(int max) { this.buf = new int[max]; this.top = 0;}
  public void Push(int val) { this.buf[this.top++] = val; }
  public int Pop(){ return this.buf[--this.top]; }
  public int Size{ get{return this.top; } }
  public int MaxSize{ get{ return this.buf.Length; } }
}

これを任意の型を格納できるように、ジェネリックスを使って記述すると以下のようになります。

// generics 版スタッククラス
class Stack<Type>
{
  Type[] buf;
  int top;
  public Stack(int max) { this.buf = new Type[max]; this.top = 0;}
  public void Push(Type val) { this.buf[this.top++] = val; }
  public Type Pop(){ return this.buf[--this.top]; }
  public int Size{ get{return this.top; } }
  public int MaxSize{ get{ return this.buf.Length; } }
}

元の int 限定版とほとんど変わりありません。 クラス名 Stack の後ろに型パラメータ(<Type> の部分)が増えたのと、数箇所、intType に置き換わったのみです。

このジェネリックス版の Stack クラスを参照するには、以下のように書きます。

const int SIZE = 5;
Stack<int>    si = new Stack<int>(SIZE);    // int型の変数を格納できるスタックになる
Stack<double> sd = new Stack<double>(SIZE); // double型の変数を格納できるスタックになる

for(int i=1; i<=SIZE; ++i)
{
  si.Push(i);
  sd.Push(1.0/i);
}

while(si.Size != 0)
{
  Console.Write("1/{0} = {1}\n", si.Pop(), sd.Pop());
}

ジェネリックスの利点

C# の配列や、 例で挙げたスタックなど、 複数の値を一まとめにして管理するクラスをのことを、 コンテナクラスまたはコレクションクラスと呼びます。 コンテナクラスは、格納する要素の型、格納する方式によってさまざまな種類があり、 整列、検索、置換などのさまざまな操作が考えられます。 以下にいくつか例を挙げてみます。

表1: コンテナの要素・方式・操作

格納する要素の型intdoublestring ・・・
格納方式配列、可変長配列、連結リスト、両端キュー ・・・
操作整列、検索、置換、総和計算 ・・・

格納する型の種類が i 個、格納方式の数が j 個、操作の数が k 個あるとき、 これらのさまざまな種類のコンテナとその操作を個別に実装しようとすると、 全部で i×j×k 個のコードを書く必要があります。 それに対し、もし任意の型を格納できるコンテナがあり、任意の種類のコンテナを扱えるコンテナ操作関数があれば、i+j+k 個のコードを書くだけですみます。

前者は格納する要素の型、格納方式、操作が相互に依存性を持っているため、i×j×k 個という大量のコードを書く必要があるわけです。

表2: 要素・方式に依存性がある場合

要素の型
intdoublestring・・・
格納方式StackStackIntStackDoubleStackString・・・
ListListIntListDoubleListString・・・
SetSetIntSetDoubleSetString・・・









 ・
  ・

逆に、後者は格納する型、格納方式、操作に依存性がないため、i+j+k 個という少ないコードを書くだけですみます。 ジェネリックスを用いることで、 このような依存性の少ないコードを書くことが出来ます。

表3: 要素・方式に依存性がない場合

要素の型格納方式
intStack<Type>
doubleList<Type>
stringSet<Type>

このような依存性・相関性の低い状態のことを直交性が高いといいます。 ジェネリックスの利点は、 このような要素・方式・操作などの直行性を最大限に引き出せることです。

C# のジェネリックス

例だけ見ても、もうほとんど分かるかと思いますが、 C# では以下のようにしてジェネリックな(どんな型に対しても総称的に使える)クラス・メソッドを定義できます。

class クラス名<型引数>
  where 型引数中の型が満たすべき条件
{
  クラス定義
}
アクセスレベル 戻り値の型 メソッド名<型引数>(引数リスト)
  where 型引数中の型が満たすべき条件
{
  メソッド定義
}

クラス名・メソッド名の後に続く <> の中の部分を型引数(type parameter)といい、 関数の引数と同じようにして、型をパラメータにすることが出来ます。 テンプレートクラスを参照する側ではクラス名の後に続く <> の中に利用したい型名を書くことで、その型に特化したクラスを生成することが出来ます。

クラス、メソッドの他に、 インターフェースデリゲートもジェネリックなものが定義できます。 定義の仕方はクラス・メソッドに対するものと同様で、 インターフェース名、デリゲート名の後ろに型引数を書きます。

キーワード where に関しては次のサブセクションで説明します。

制約条件

where はなくてもかまいませんが、 その場合、型引数で与えた型に対するメソッド呼び出しなどは出来なくなります。

// 一番目の引数だけを帰す単純なメソッド。
static Type First<Type>(Type a, Type b)
{
  // 特にメソッド呼び出し等はないのでこれは OK。
  return a;
}

// 例で挙げた Max 関数。
// where の部分を消してみる。
static Type Max<Type>(Type a, Type b)
{
  // ↓Type 型 に CompareTo なんて定義されていないと怒られてエラーになる。
  return a.CompareTo(b) > 0 ? a : b;
}

型引数で与えた型でメソッド呼び出しをしたい場合などには、 where キーワードを使って型に制約条件を付加します。 付加できる制約条件は、(型引数を T とすると)以下の5つです。

表4: 型引数に対する制約条件

制約の与え方説明
where T : structT値型である
where T : classT参照型である
where T : new()引数なしのコンストラクタを持つ。他の制約条件と同時に課す場合には、一番最後に指定する必要がある。
where T : [base class]T[base class] で指定された型を継承する。
where T : [interface]T[interface] で指定されたインタフェースを実装する。

例えば、2つの値の比較が必要な場合、 そのクラスは IComparable インタフェースを実装しているはずなので、 以下のように、「クラス TypeIComparable を実装している」という制約を課します。

static Type Max<Type>(Type a, Type b)
  where Type : IComparable
{
  // ↑この制約条件のお陰で、
  // ↓Type 型 は CompareTo を持っているというのが分かる。
  return a.CompareTo(b) > 0 ? a : b;
}

インスタンス化

ジェネリックなクラス・メソッドに対して、 具体的な型を与えることを「インスタンス化する」といいます。

例えば、class Stack<Type> として定義した ジェネリッククラスに対して、 具体的な型 int を与え、 class Stack<int> というクラスを作ることを、 「intStack をインスタンス化する」といいます。

const int SIZE = 5;
Stack<int>    si = new Stack<int>(SIZE);    // Stack を int でインスタンス化
Stack<double> sd = new Stack<double>(SIZE); // Stack を double でインスタンス化

int    n = Max(5, 10);        // Max を int でインスタンス化
double x = Max(5.0, 10.0);    // Max を double でインスタンス化
string s = Max("abc", "cat"); // Max を string でインスタンス化

複雑な型引数の使い方

型引数は複数の型を含んでいてもかまいません。

class Pair<K, V>
{
  K key;
  V val;

  public K Key  { get{return this.key;} set{this.key = value;} }
  public V Value{ get{return this.val;} set{this.val = value;} }
}

また、ジェネリッククラス・メソッド内では型引数を使って、 他のジェネリッククラスのインスタンス化ができます。

class TestGenerics
{
  // リスト中の要素を Console.Write で画面に出力。
  static void Show<Type>(System.Collections.Generic.IList<Type> list)
  {
    foreach(Type x in list)
      Console.Write("{0}\n", x);
  }

  public static void Main()
  {
    int[] i = new int[]{1, 2, 3, 4, 5};
    Show(i);
  }
}

デフォルト値

変数を初期化するとき、 数値型の場合は 0 で、 参照型の場合は null で初期化する事がよくあります。 そこで、C# ジェネリックスでは、 default(Type) というキーワードを用意しています。 default(Type) は、 数値型に対しては 0、 参照型に対しては null になります。 また、構造体に対しては、 構造体の全てのメンバに対して 0 または null で初期化したものを与えます。

class TestGenerics
{
  // 配列を 0 または null で満たします。
  static void FillWithDefault<Type>(Type[] array)
  {
    for(int i=0; i<array.Length; ++i)
      array[i] = default(Type);
  }

  public static void Main()
  {
    int[]    i = new int[5];
    string[] s = new string[5];

    FillWithDefault(i);
    FillWithDefault(s);
  }
}