概要

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 制約と同時に指定できる)。

C# 8.0 での追加

Ver. 8.0

C# 8.0 で notnull 制約が増えました。

型引数に対する制約条件
制約の与え方 説明

where T : notnull

Tには非 null な型しか渡せない

詳しくは「null 許容参照型」で説明します。

ちなみに、C# 8.0 でnull 許容参照型を有効化した場合、 class 制約や、基底クラス制約は「非 null」の意味になり、 null 許容参照型を受け付けたい場合は制約に ? を付けることになります。

#nullable enable
using System;
 
class Program
{
    static void NotNull<T>() where T : notnull { }
    static void Class<T>() where T : class { }
    static void NullableClass<T>() where T : class ? { }
    static void BaseType<T>() where T : Exception { }
    static void NullableBaseType<T>() where T : Exception? { }
 
    static void Main()
    {
        // OK。警告もなし。
        NotNull<int>();
        NotNull<string>();
        Class<string>();
        NullableClass<string>();
        NullableClass<string?>();
        BaseType<ArgumentException>();
        NullableBaseType<ArgumentException>();
        NullableBaseType<ArgumentException?>();
 
        // 警告。
        Class<string?>();
        BaseType<ArgumentException?>();
        NotNull<int?>();
        NotNull<string?>();
 
        // コンパイル エラー。
        Class<int>();
        BaseType<int>();
    }
}
 

補足: 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()制約はお手軽ですが、 パフォーマンス的にシビアな場面では使わないよう注意が必要です。

アンチ制約

Ver. 13

C# 13 で allows ref struct という機能が追加されました。 これはジェネリック型の where 句に書くもので、型引数 T に何らかの条件を付けるという意味では他の制約と同じですが、 T に及ぼす影響が真逆なのでアンチ制約、あるいは、反制約(anti-constraint)と呼びます。

通常の制約は以下のような意味を持ちます。

  • メソッドの中でできることを増やす
  • その代わり、使える型が減る
制約あり 制約なし
M<int>();
M<object>();
M<string>(); // 書けなくなる。
M<Uri>();    // 書けなくなる。

static object M<T>()
    where T : new()
{
    // new T() が書ける。
    return new T();
}
M<int>();
M<object>();
M<string>(); // 書ける。
M<Uri>();    // 書ける。

static object M<T>()
    // 制約なしの場合
{
    // こっちが書けない。
    return new T();
}

一方で、アンチ制約はこれとは逆で、以下のような意味を持ちます。

  • メソッドの中でできることが減る
  • その代わり、使える型が増える
アンチ制約あり アンチ制約なし
M<int>();
M<object>();
M<Span<string>>();      // 書ける。
M<ReadOnlySpan<int>>(); // 書ける。

static object? M<T>()
    where T : allows ref struct
{
    // ref struct を object に渡せない。
    return default(T);
}
M<int>();
M<object>();
M<Span<string>>();      // 書けない。
M<ReadOnlySpan<int>>(); // 書けない。

static object? M<T>()
    // アンチ制約なしの場合
{
    // 書けるようになる。
    return default(T);
}

allows は三単現の動詞の「許可する」です。 通常の where T : X が「T は X でなければならない」なのに対して、 where T : allows X は「T が X であることを許す」という意味になります。

ちなみに、allows は意味が逆なことを表すためにわざわざキーワードを追加したもので、 コンパイラーの実装都合だけでいうと where T : ref struct だけでも構文解析は可能だったそうです。

C# 13 時点でアンチ制約(= allows を使うもの)は ref struct だけですし、 他に将来アンチ制約として足したいものあまり多くはないんですが、 1つだけ有望そうな候補があります。

これまで、where T : struct 制約を指定すると null 許容値型を T に渡せなくなるという制約がありました。

// struct 制約が付いていると null 許容型を指定できなくなる。
M<int?>();

static void M<T>()
    where T : struct
{
    // T = int? だとすると、T? が int?? になっちゃう。
    // (.NET は「2重 nullable」を認めていない。)
    T? x = null;
}

そこで、allows nullable (仮)アンチ制約を導入してはどうかという案が出ています。

// これができるようになってほしい。
M<int?>();

static void M<T>()
    where T : struct, allows nullable // 仮文法
{
    // こっちにエラーを出す案。
    T? x = null;
}

インスタンス化

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

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

ジェネリックの実現方法

サンプルコード

ジェネリックの実装方法はプログラミング言語ごとに方針の差が結構あります。例えば、以下のようなやり方があります。

  • 全展開(C++など)
  • 値型のみ展開(C#など)
  • 型消去(Javaなど)

例えば、以下のようなコードを書いたとします。単純なジェネリック クラスと、その利用側コードです。

public class Wrapper<T>
{
    public T Value;
}

class Program
{
    static void Main(string[] args)
    {
        var i = new Wrapper<int> { Value = 1 };
        var b = new Wrapper<byte> { Value = 1 };
        var s = new Wrapper<string> { Value = "abc" };
        var a = new Wrapper<int[]> { Value = new[] { 1, 2, 3 } };

        int iv = i.Value;
        byte bv = b.Value;
        string sv = s.Value;
        int[] av = a.Value;
    }
}

4つの型パラメーターを使っていて、そのうち、2つ(intbyte)は値型、残り2つ(stringint[])は参照型です。

これが、どういう風に展開されるかを見ていきましょう。

全展開(C++など)

わかりやすい実装は、型パラメーターごとにすべて展開してしまう手法(全展開)です。 C++ではこの手法でジェネリックを実現しています。 (ちなみに、C++の言語機能としてはテンプレート(template)と呼びます。 コンパイル時に全部ひな形生成してしまう辺りが「テンプレート」(ひな形、鋳型)と呼ばれる所以です。)

先ほどのコードを「全展開」で実装すると、以下のようなものに相当するコードが生成されます。

// 使った分だけそれぞれ別の型に展開
public class Wrapper_int { public int Value; }
public class Wrapper_byte { public byte Value; }
public class Wrapper_string { public string Value; }
public class Wrapper_Array_int { public int[] Value; }

class Program
{
    static void Main(string[] args)
    {
        var i = new Wrapper_int { Value = 1 };
        var b = new Wrapper_byte { Value = 1 };
        var s = new Wrapper_string { Value = "abc" };
        var a = new Wrapper_Array_int { Value = new[] { 1, 2, 3 } };

        // キャストは不要
        int iv = i.Value;
        byte bv = b.Value;
        string sv = s.Value;
        int[] av = a.Value;
    }
}

このコードは、以下のようなメモリの使い方をします。

ジェネリック型の全展開

利点

  • 無駄なオーバーヘッドがなくなって、実行性能がいい

欠点

  • 型パラメーターに与える型が増えると、生成されるプログラムのサイズが大きくなりすぎる

型消去(Javaなど)

全展開の真逆の実装は、全部の型を消してしまう手法(型消去)です。 object型にはどんな型でも代入できるので、object型なクラス1つを用意して、適宜キャストを挟むコードを生成します。 Javaではこの手法でジェネリックを実現しています。

先ほどのコードを「型消去」で実装すると、以下のようなものに相当するコードが生成されます。

// object 型な1つのクラスに集約
// 元の型情報を残さない = 型消去
public class Wrapper { public object Value; }

class Program
{
    static void Main(string[] args)
    {
        var i = new Wrapper { Value = new Integer(1) };
        var b = new Wrapper { Value = new Byte(1) };
        var s = new Wrapper { Value = "abc" };
        var a = new Wrapper { Value = new[] { 1, 2, 3 } };

        // キャストが必要
        int iv = ((Integer)i.Value).Value;
        byte bv = ((Byte)i.Value).Value;
        string sv = (string)s.Value;
        int[] av = (int[])a.Value;
    }
}

//↓こんな感じのクラスが標準ライブラリ中にある

public class Integer
{
    public int Value;
    public Integer(int value) { Value = value; }
}

public class Byte
{
    public byte Value;
    public Byte(byte value) { Value = value; }
}

このコードは、以下のようなメモリの使い方をします。

ジェネリック型の型消去

利点

  • 実装がシンプル

欠点

  • 実行性能的に不利
    • 特に値型に対してボックス化が発生するのがかなりつらい

値型だけ展開

C#の実装は全展開と型消去の間くらいの手法になっています。

型消去の場合、値型に対してボックス化が起きることが実行性能上かなり不利になります。 そこで、C#の実装では、値型だけは展開し、参照型に対しては型消去に近いコードを生成します。

要するに、先ほどのコードから、以下のようなものに相当するコードが生成されます。

// 値型の場合: 使った分だけそれぞれ別の型に展開
public class Wrapper_int { public int Value; }
public class Wrapper_byte { public byte Value; }

// 参照型の場合、object 型な1つのクラスに集約
public class Wrapper { public object Value; }

class Program
{
    static void Main(string[] args)
    {
        var i = new Wrapper_int { Value = 1 };
        var b = new Wrapper_byte { Value = 1 };
        var s = new Wrapper { Value = "abc" };
        var a = new Wrapper { Value = new[] { 1, 2, 3 } };

        // 値型はキャスト不要
        int iv = i.Value;
        byte bv = b.Value;

        // 参照型
        // (C#(.NET) の場合はこのキャストを取り除くような最適化もしてる)
        string sv = (string)s.Value;
        int[] av = (int[])a.Value;
    }
}

このコードは、以下のようなメモリの使い方をします。

ジェネリック型に対して、値型だけ展開

利点

  • プログラム サイズの増大と実行性能のバランスがいい

欠点

  • コンパイラーの実装が大変
    • 機能の修正がしにくい

インターフェイス制約

C#のジェネリックでは、メソッドなどのメンバーを参照するためにインターフェイス制約(制約条件で、where T : [interface])を付ける必要があります。 例えば、以下のようなコードでは、前者の書き方ではだめで、 後者のように、where句を付けて初めてコンパイルできます。

// コンパイル エラー: T に Count プロパティがない
static int M<T>(T x) => x.Count;

// これなら大丈夫。IList.Count を参照できる
static int M<T>(T x)
    where T : System.Collections.IList
    => x.Count;

インターフェイス制約で困ることになるのは、静的メソッドを呼べないことです。 演算子も静的メソッドみたいなものなので呼べません。 例えば、以下のコードはコンパイル エラーになります。 (※ C# 11 以降は静的メソッド呼ぶ方法が追加されました。 詳しくは「インターフェイスの静的抽象メンバー」で説明しています。)

// interface 制約では静的メソッドを呼べない
// なので、ジェネリックを使うと静的メソッドを呼ぶ手段がない
// コンパイル エラーに
static T M<T>(T x) => T.StaticMethod(x);

// + (演算子)は実質的には静的メソッド
// 演算子もコンパイル エラーに
static T Add<T>(T x, T y) => x + y;

インターフェイス制約が必要なんだったら、インターフェイスをそのまま使えばいいと思うかもしれませんが、 わざわざジェネリックにすることで実行性能的に有利になることがあります。

特に、構造体が絡むと顕著で、かなり実行性能に影響があります。 例えば以下のコードを見てください。

using System;

// 無駄なヒープ確保をしないように構造体に
struct Disposable : IDisposable
{
    public void Dispose() { }
}

class Program
{
    static void WithInterface(IDisposable x) => x.Dispose();

    // やってることは WithInterface を同じに見えて…
    static void WithGenerics<T>(T x)
        where T : IDisposable
        => x.Dispose();

    static void Main()
    {
        // 構造体なので無駄なヒープ確保はしない
        default(Disposable).Dispose();

        for (int i = 0; i < 10000; i++)
        {
            // ところが、インターフェイスを介するとボックス化を起こす
            // 無駄なヒープ確保に
            // 1個や2個なら大したコストではないものの、何度も呼ばれるとさすがにつらい
            WithInterface(default(Disposable));
        }

        for (int i = 0; i < 10000; i++)
        {
            // ジェネリックを介するとボックス化が不要
            // 繰り返し呼んでも平気
            WithGenerics(default(Disposable));
        }
    }
}

IDisposableインターフェイスを実装したDisposableという構造体を作って、このDisposeメソッドを呼ぶことを考えます。 IDisposableインターフェイスなのは簡単に実装できるものを選んだというだけで、深い意味はありません。 構造体なので、普通にインスタンスを作って、普通にDisposeメソッドを呼ぶ分にはヒープ領域を一切使いません。

ところが、WithInterfaceメソッドのように、インターフェイスを介して引数に渡すと、ここでボックス化(ヒープ確保)が発生します。 1個や2個なら大したコストではないものの、この例のようにループの内側で大量に呼ばれると、なかなかきつい負担となります。

そこで、WithGenericsメソッドのように、ジェネリックを使います。 前節で説明しましたが、 C#のジェネリックでは値型を使ったときにコードを展開してくれます。 その結果、ボックス化を起こさずにメソッドの引数に値型を渡せます。

静的メソッド代わり

ジェネリックでは静的メソッドを呼ぶ手段がないという話をしました。 ちょっと強引な手段にはなりますが、この代わりとなる方法を考えてみましょう。

たとえば、以下のような累算処理を考えてみます。 int配列の全要素の和と積を求めるコードです。

static int Sum(int[] items)
{
    var sum = 0;
    foreach (var item in items)
        sum = sum + item;
    return sum;
}

static int Prod(int[] items)
{
    var sum = 1;
    foreach (var item in items)
        sum = sum * item;
    return sum;
}

static void M()
{
    var items = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    var sum = Sum(items);
    var prod = Prod(items);
}

標準ライブラリにあるAggregateメソッド(System.Linq.Enumerableクラスの拡張メソッド)を使って以下のように書けはするんですが。 これはこれで、デリゲート(インターフェイスと同程度の負担)を介することになります。

var sum = items.Aggregate(0, (x, y) => x + y);
var prod = items.Aggregate(1, (x, y) => x * y);

デリゲートやインターフェイスを介したメソッド呼び出しは、静的メソッドと比べるとほんの少し不利です。 微々たるものですが、「ちりも積もれば」で、微々たる差を気にしないといけないこともあります。

そこで、値型のジェネリックが展開される性質を使ってみます。 まず、以下のようなインターフェイスと構造体を作ります。

interface IBinaryOperator<T>
{
    T Zero { get; }
    T Operate(T x, T y);
}

struct Add : IBinaryOperator<int>
{
    public int Zero => 0;
    public int Operate(int x, int y) => x + y;
}

struct Mul : IBinaryOperator<int>
{
    public int Zero => 1;
    public int Operate(int x, int y) => x * y;
}

例えば、以下のように書けます。 値型のジェネリックの展開によって、デリゲートやインターフェイスを介するよりも最適化が掛かりやすく、 静的メソッドに近い性能になります。 (具体的にいうと、仮想メソッド呼び出しが消えて、小さいメソッドを最適化オプション付きで実行するとインライン展開も掛かります。 この例はまさにそういう最適化が掛かって、ジェネリックなしの場合と比べて10倍以上速くなったりします。)

static T Sum<T, TOperator>(T[] items, TOperator op)
    where TOperator : struct, IBinaryOperator<T>
{
    var sum = op.Zero;
    foreach (var item in items)
        sum = op.Operate(sum, item);
    return sum;
}

static void M()
{
    var items = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    // ジェネリックを介せばボックス化を避けれる
    var sum = Sum(items, default(Add));
    var prod = Sum(items, default(Mul));
}

もう1つ、違うバージョンを書いてみましょう。 default(Add)とか、呼び出し側でダミーのインスタンスを作って引数として渡すのも無駄なので、 これもメソッドの中でやってしまいましょう。

static T Sum<T, TOperator>(T[] items)
    where TOperator : struct, IBinaryOperator<T>
{
    var sum = default(TOperator).Zero;
    foreach (var item in items)
        sum = default(TOperator).Operate(sum, item);
    // ↑ メソッド内で default()
    // 空の構造体なのでほぼノーコスト
    return sum;
}

static void M()
{
    var items = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    // default(T) せず、型引数だけ書く
    var sum = Sum<int, Add>(items);
    var prod = Sum<int, Mul>(items);
}

こういう、型引数の変更だけで動作を切り替える手法をポリシー パターン(policy pattern)とかポリシー ベース設計(policy based design)とか呼んだりします。 ちなみに、C++のtemplateの場合はジェネリックに静的メソッドを呼べるので、C++では強引な手段を取らなくてもポリシー パターンを使いやすく、結構多用されます。