目次

キーワード

概要

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> strComp = new Comparer<object>();
IComparer<string> objComp = strComp;

当然、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 の前に + や - が付いていることを確認できます。

in/out 付きインターフェースのコンパイル結果
in/out 付きインターフェースのコンパイル結果

仕組みとしては .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 が値型だから。

更新履歴

ブログ