概要
Ver. 4.0
C# 4.0 で、ジェネリクスの型引数に共変性・反変性を持たせることが可能になりました。 (共変性・反変性という言葉の意味は「covariance と contravariance」参照。)
ジェネリックの共変性・反変性
ジェネリクスの共変性・反変性というものがどういうものかというのを説明する前に、まず背景を。 ジェネリックコレクションに関して、昔から以下のようなことをしたいという要望がありました。
List<string> strings = {"aa", "bb", "cc"};
List<object> objs = strings;
これを認めてしまうと何がまずいかというと、 以下のような不正な値の書き換えが起こり得る。
// strings と objs は同じオブジェクト
objs[0] = 5; // int に書き換えられたらまずい
string str = strings[0];
この問題が起きる原因がどこにあるかというと、 List が set も get も可能なインデクサーを持っていることです。
get しかない場合なら、ここで挙げたような不正な書き換えは起こらないわけです。 戻り値(あるいは get)でしか使わない型の場合、
IEnumerable<string> strings = new[] {"aa", "bb", "cc"};
IEnumerable<object> objs = strings;
// foreach (object x in strings) ってやっても問題ないんだから、
// objs に strings を代入しても OK。
みたいな事が出来ても問題ないはず。 (こういうのを共変性(covariance)と言います。)
逆に、引数(あるいは set)でしか使わない場合も、
Action<object> objAction = x => { Console.Write(x); };
Action<string> strAction = objAction;
// objAction("string"); ってやっても問題ないんだから、
// strAction に objAction を代入しても OK。
みたいな事をして大丈夫。 (こういうのを反変性(contravariance)といいます。)
in/out 修飾子
ということで、C# 4.0 から、ジェネリクなインターフェース、もしくは、デリゲートに対して、 共変性・反変性を実現するための仕組みが追加されました。
共変性のためには「型を出力(戻り値、get)にしか使わない」、 反変性のためには「型を入力(引数、set)にしか使わない」という保証があればいいので、 それぞれ、ジェネリクスの型引数に out と in という修飾子を付けることでこれを保証します。 (ちなみに、この out と in 修飾子のことを変性注釈(variance annotation)と呼ぶそうです。)
まず、出力(メソッドの戻り値、プロパティの get)にしか使わない型には out という修飾子を指定します。 例えば、.NET Framework 4.0 では、IEnumerator の型引数に out が付きました。
public interface IEnumerator<out T>
{
T Current { get; } // get しかない = 出力のみ
bool MoveNext();
void Reset();
}
こうすることで、共変性が認められます。
IEnumerator<string> strEnum = new Enumerator<string>();
IEnumerator<object> objEnum = strEnum;
一方、入力(メソッドの引数、プロパティの set)にしか使わない型には in という修飾子を指定します。 例えば、IComparer の型引数に in が付きました。
public interface IComparer<in T>
{
int Compare(T a, T b); // T は引数としてしか使われない
}
こうすることで、今度は反変性が認められます。
IComparer<object> objComp = new Comparer<object>();
IComparer<string> strComp = objComp;
当然、in/out の組み合わせもあり得ます。
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
Func<object, object, string> f1 = (x, y) => string.Format("({0}, {1})", x, y);
Func<string, string, object> f2 = f1;
余談1: in/out の内部実装
型引数の in/out のような仕組みの実現には 「IL」 レベルでの対応が必要になります。 というか、IL レベルでは、.NET Framework 2.0 の時点で in/out 相当のフラグを設定する機能がありました。 (今回、C# からそのフラグを立てれるようになっただけ。)
例えば、C# 4.0 で以下のようなソースを書いて、
namespace ConsoleApplication1
{
public interface IEnumerator<out T>
{
T Current { get; }
bool MoveNext();
}
public interface IComparable<in T>
{
int CompareTo(T x);
}
}
一度コンパイルしたものを .NET Framework 2.0 付属の IL Disasm(.NET Framework 付属の IL 逆アセンブラー)で開いてみると、 型引数 T の前に + や - が付いていることを確認できます。
仕組みとしては .NET Framework 2.0 の頃からあったので、 IL アセンブラーを使ってこの +/- フラグを立ててやれば、 C# 3.0 以前でも共変性・反変性を使えたりします。 (一度 object にしてから無理やりキャストする必要はある。)
余談2: 値型は invariant
ちなみに、値型(int とかの組み込み整数型や、struct、enum)には共変性・反変性は使えません。 (「IL」 の実装上の制約。)
IEnumerable<object> e1 = new[] { "abc", "def" }; // こっちは OK。
IEnumerable<object> e2 = new[] { 1, 2 }; // でも、これは不可。int が値型だから。