目次

キーワード

概要

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

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

ポイント
  • ジェネリック: 型だけ違って処理の内容が同じようなものを作るときに使う。

  • ジェネリッククラス:IComparable<T> { int CompareTo(T x, T y); }

  • ジェネリックメソッド:T max<T>(T x, T y) { ... }

英語だと generics。 カタカナ語で訳すにしても「ジェネリクス」の方が適切な気はします(実際、Javaではジェネリクスという訳語が一般的)。 「形容詞 + 複数形のs」で名詞化している単語で、通常、単数形では名詞を指しません (エコノミクスとかエレクトロニクスとかが近くて、名詞としては常に複数形のsが付きます)。 「ジェネリック」になっているのは、マイクロソフトの翻訳ルールが機械的で、 「複数形のsは一律削除する」というルールで運用しているからです。

ジェネリックの例

ジェネリックメソッド

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

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

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

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

要素・方式に依存性がある場合
要素の型
int double string ・・・
格納方式 Stack StackInt StackDouble StackString ・・・
List ListInt ListDouble ListString ・・・
Set SetInt SetDouble SetString ・・・


















 ・

  ・

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

要素・方式に依存性がない場合
要素の型 格納方式
int Stack<Type>
double List<Type>
string Set<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つです。

型引数に対する制約条件
制約の与え方 説明
where T : struct Tは「値型」である
where T : class Tは「参照型」である
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);
  }

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

既定値

変数を初期化するとき、 数値型の場合は 0 で、 参照型の場合は null で初期化する事がよくあります。 これら、0 や null などの値を既定値(default value)と呼びます。

そこで、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);
  }

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

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

共変性・反変性

Ver. 4.0

C# 4.0 から、ジェネリックの型引数に共変性・反変性を持たせることができるようになりました。 詳しくは「ジェネリクスの共変性・反変性」を参照してください。

C++ や Java の template/generics との違い

(変更予定) この比較表は「Java/C++ 開発者向け」の一節に移してもいいかも。

C# Java C++
実装方式 MSIL に generics 用の命令がある。 (.NET 2.0 で追加された。) キャストの分のコードが減って実行効率がいい。 Java バイトコード上は generics に対応していない。 Java コンパイラがキャストを自動的に挿入してくれる。 単なるシンタックスシュガー。 (古いバージョンとの互換性重視。) 超高機能なマクロみたいなもの。 全部インライン展開されるので、 実行効率はいいものの、コンパイルに時間がかかるし、実行ファイルサイズが膨れ上がる。 また、ソースファイルとして提供せざるを得ない。
実体 IL 上は List<int> と List<string> でほとんど同じ扱い。 値型と参照型の違いを吸収するための命令も IL に追加されてる。 参照型同士(たとえば List<string> と List<object> )なら JIT 結果もほぼ共有される いわゆる「型消去」。 Vector<int> と Vector<string> で実体は同じ。 (内部的にどころか実際に)object の Vector と同じものになる。 全部インラインに展開される。 vector<int> と vector<string> で別個にコードが生成される。
型安全性 List<int> と List<string> はちゃんと別の型として扱われる。 リフレクションでも正確に型を取れる。 Vector<int> と Vector<string> を区別できない。 リフレクションでは要素の型を取れない。 vector<int> と vector<string> はちゃんと別の型として扱われる。
キャスト 内部的には object の List と同じ扱いであるものの、 MSIL レベルで対応しているおかげでキャストの必要はなくなる。 キャスト(特に boxing/unboxing)が不要な分、実行効率がいい。 コンパイラが自動的にキャストコードを挿入している。 実体がそもそも別、インラインに展開されたコードになるので、 キャストも不要。 実行ファイルサイズ爆発する原因。
メンバー参照 インターフェースを使った型制約に基づく。 インターフェースを使った型制約に基づく。 ダックタイピング」。
その他 C# 4.0 で共変性・反変性がサポートされる。 変性の代わりにワイルドカード利用。 互換性重視なので、J2SE 5.0 でコンパイルしたものも、古いバージョンの VM で問題なく動く。 マクロみたいなものなので、型だけじゃなくて int も template の引数にできる。 template の特殊化など、C# generics がサポートしていない(原理的にできない)こともできる。

更新履歴

ブログ