目次

キーワード

概要

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

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

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

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

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

※genericsの訳語

英語だと、名詞では generics、形容詞が generic です。 なので名詞の genericsは、カタカナ語で訳すにしても「ジェネリクス」の方が適切な気はします。実際、Java などではジェネリクスという訳語が一般的です。 (一方、形容詞で generic type や generic method と言うときには訳もだいたい「ジェネリック」です。)

generics は「形容詞 + s」で名詞化している単語で、通常、s が付かない状態では名詞になりません。 類似の単語だとエコノミクス(economics)とかエレクトロニクス(electronics)とかがそうで、名詞としては常にsが付きます(s を取った状態だと形容詞)。

(ちなみに、この手の -ics で終わる単語は s で終わっているものの、扱いは単数。 「○○ic な事例を集めた学問」→「○○ics」みたいな感じなので複数形の単語とも取れる一方で、 抽象的に学問として扱う場合は不可算名詞。 そして、「その学問における一事例」みたいなときには「○○ics」のままで単数扱いになります。 )

マイクロソフトのドキュメントなどで、名詞形の generics であっても「ジェネリック」と訳されているのは、 マイクロソフトの翻訳ルールが機械的なせい(機械翻訳しやすいように/機械翻訳との整合性をとるため)です。 「カタカナ語にするときは複数形や三単現のsは一律削除する」というルールで運用していて、generics のように本来 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 以下に、型引数が満たすべき条件(constraint: 制約条件)を書きます。 制約は付けなくてもかまいませんが、 その場合、型引数で与えた型に対するメソッド呼び出しなどは出来なくなります。

例えば以下の例で、 Firstメソッドのように何のメンバーも呼び出さない場合には制約は不要です。 一方で、Maxメソッドのように何かを呼びたい場合は、それが何のメンバーなのかを示すため、 後述する「インターフェイス制約」などが必要になります。

// 一番目の引数だけを帰す単純なメソッド。
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;
}

この例の場合、以下のような「インターフェイス制約」というものを付けます。 2つの値の比較したい場合、 IComparable というインターフェースがCompareToというメソッドを持っているのでこれを使います。 以下のように、「クラス TypeIComparable を実装している」という制約を課すことで、 「IComparableを実装している任意の型に対して呼べるメソッド」が作れて、 メソッド中ではIComparableのメンバーを呼び出せるようになります。

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

型引数 T に対する制約は、where T : 制約 という書き方で指定します。 C# で指定できる型制約には以下のようなものがあります。

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

前述の例でもそうだったように、一番よく使うのはインターフェイス制約でしょう。 メンバー呼び出しには必須になります。

複数の型引数に対して制約を付けたい場合は where を複数並べます。 また、1つの型引数に対して複数の制約を付けたい場合は , で制約を並べます。

using System;
using System.Collections.Generic;

class X<TItem, TList>
    where TItem : class, IEquatable<TItem>, new()
    where TList : struct, IList<TItem>
{
}

上記の例のように、制約の中にさらにジェネリックな型(IList<TItem>など)を掛けますし、 型引数も使えます(型引数であるTItemが、制約の側にも出てきます)。

ちなみに、互いに矛盾したり、意味が重複していて無駄な制約は同時には指定できません。 具体的には、classstruct、基底型は同時には指定できません。

class X<T>
    where T : struct, class // 「クラス、かつ、構造体」なんてことはあり得ない。エラーに
{
}

class Base { }
class X<T>
    where T : Base, class // 基底クラスを持っている時点で参照型。エラーに
{
}

また、classstruct、基底型の3つは、インターフェイス、new()の2つよりも前に書く必要があります。

using System;

class Ok<T>
    where T : struct, IDisposable // これは行ける
{
}

class Ng<T>
    where T : IDisposable, struct // こっちはダメ
{
}

C# 7.3 での追加

Ver. 7.3

C# 7.3 では、3つほど指定できる制約が増えました。

型引数に対する制約条件
制約の与え方 説明
where T : unmanaged Tは「アンマネージ型」である
where T : Enum Tは「列挙型」である
where T : Delegate Tは「デリゲート型」である

unmanaged制約を付けると、その型をポインター化したりできるようになります。 詳しくは「unsafe」で説明します。

EnumDelegateに関しては、これらはキーワードではなく、それぞれSystem名前空間にあるEnumクラス、Delegateクラスのことです。 詳しくは「[余談] 暗黙的な派生」で説明します。

ちなみに、unmanagedである時点で必ずstructなので、structclass、基底型制約とは同時には指定できません。

一方で、Enum制約はstruct制約と同時に指定できます。 通常、基底型制約はstruct制約と同時には指定できませんが、Enumだけは特別に認められます。 Enumはクラスですが、「[余談] 暗黙的な派生」で説明するように、 ちょっと特殊なクラスで、実態としてはクラスよりもインターフェイスに近いです (インターフェイスであれば struct 制約と同時に指定できる)。

補足: new() 制約

new()制約を付けることで、型引数Tに対して引数なしのコンストラクターnew T()を呼べるようになります。

例えば以下のように、new T()で要素を初期化しながら配列を作るなどの処理ができます。

// 既定値ではなく、new T() で要素を初期化しながら配列生成
static T[] Array<T>(int n)
    where T : new()
{
    var array = new T[n];
    for (int i = 0; i < array.Length; i++)
        array[i] = new T(); // new() 制約のおかげで空のコンストラクターを呼べる
    return array;
}

ただ、new()制約を使ったコンストラクター呼び出しnew T()は、 内部的にはActivatorを使った動的な処理になっています。 実行時型情報を使うので、 通常のnewと比べて10倍くらい遅いです。

(参考: new 制約の遅さ。 手元の環境でのベンチマークでは、 非ジェネリックな場合が6μ秒なのに対して、 new()制約やActivatorを使ったものは100μ秒程度かかりました。 new()制約を使うより、外からFunc<T>をもらって外でnewしてもらう方が10倍速かったりします。)

new()制約はお手軽ですが、 パフォーマンス的にシビアな場面では使わないよう注意が必要です。

インスタンス化

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

例えば、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 がサポートしていない(原理的にできない)こともできる。

更新履歴

ブログ