概要
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>
の部分)が増えたのと、数箇所、int
が Type
に置き換わったのみです。
このジェネリック版の 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」で例に挙げた連結リストなど、 複数の値を一まとめにして管理するクラスのことを、 コンテナクラスまたはコレクションクラスと呼びます。 コンテナクラスは、格納する要素の型、格納する方式によってさまざまな種類があり、 整列、検索、置換などのさまざまな操作が考えられます。 以下にいくつか例を挙げてみます。
格納する要素の型 |
int 、double 、string ・・・
|
---|---|
格納方式 | 配列、可変長配列、連結リスト、両端キュー ・・・ |
操作 | 整列、検索、置換、総和計算 ・・・ |
格納する型の種類が 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
というメソッドを持っているのでこれを使います。
以下のように、「クラス Type
は IComparable
を実装している」という制約を課すことで、
「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
が、制約の側にも出てきます)。
ちなみに、互いに矛盾したり、意味が重複していて無駄な制約は同時には指定できません。
具体的には、class
、struct
、基底型は同時には指定できません。
class X<T>
where T : struct, class // 「クラス、かつ、構造体」なんてことはあり得ない。エラーに
{
}
class Base { }
class X<T>
where T : Base, class // 基底クラスを持っている時点で参照型。エラーに
{
}
また、class
、struct
、基底型の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」で説明します。
Enum
とDelegate
に関しては、これらはキーワードではなく、それぞれSystem
名前空間にあるEnum
クラス、Delegate
クラスのことです。
詳しくは「[余談] 暗黙的な派生」で説明します。
ちなみに、unmanaged
である時点で必ずstruct
なので、struct
、class
、基底型制約とは同時には指定できません。
一方で、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>
というクラスを作ることを、
「int
で Stack
をインスタンス化する」といいます。
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 がサポートしていない(原理的にできない)こともできる。 |