反変性インタフェースと共変性インタフェース in C#


はじめに

共変性,反変性インタフェースという言葉を知っていても知らなくても,このルールはよくよく考えればあたり前の話で,言葉を知らなくてもほとんどの人はその概念を理解しているのではないでしょうか.しかし職場の先輩などに言われて,言葉の意味を知っているのに言葉自体を知らずに悔しい思いをしないように,なるべく短くメモを残しておきました.

準備1(暗黙的代入)

共変性,反変性は「ある型をもつ変数へ別の型をもつインタフェースを実装したオブジェクトが代入できるかどうかの性質」なのですが,実際にはインタフェースを実装したオブジェクトが変数に代入される場面で問題になるのではなく,メソッド呼び出し時の引数としての暗黙的代入でエラーになったりならなかったりします.
int func(int a);
というシグネチャを持つメソッドを呼び出すときに
func(5);
とすると暗黙的に
int a=5と同じことになります.これを,ここでは暗黙的代入とします.代入をメソッドへの引数の引き渡しと同じこととみなして,
本稿では共変性,反変性は「ある型の引数をとるメソッドに別の型をもつインタフェースを実装したオブジェクトが引数として引き渡せるかどうかの性質」として,C#のIComparerとIEnumeratorで実験してみます.

準備2 親子クラス

AnimalとDogの2つのクラスが親子関係になっています.DogオブジェクトはAnimal変数に代入できますが,AnimalオブジェクトはDog変数に代入できません.

class Animal
    {
        public int id { get; set; }
        public int weight { get; set; }
    }
class Dog : Animal
    {
        public int speed { set; get; }
    }
Animal a =new Dog();//可能(アップキャスト)
//Dog d = new Animal();//コンパイルエラー(ダウンキャスト)

ここで,以下の実験のために2つの配列を用意しておきます.初期化子パラメータは適当です.

Dog[] ad = {
                new Dog(){id=1,weight=1,speed=10},
                new Dog(){id=2,weight=2,speed=9},
                new Dog(){id=3,weight=3,speed=8}
            };
Animal[] aa = {
                new Animal(){id=10,weight=3},
                new Animal(){id=20,weight=1},
                new Animal(){id=30,weight=2}
            };

反変性インタフェースの例 IComparer<T>

前で用意した配列をソートするためIComparer<T>を実装したクラスを作成します.

class Dcompare : IComparer<Dog>
{
        public int Compare(Dog x, Dog y)
        {
            return x.speed - y.speed;
        }
}
class Acompare : IComparer<Animal>
{
        public int Compare(Animal x, Animal y)
        {
            return x.weight - y.weight;
        }
}

この2つの比較クラスで,それぞれ親子クラスをたすき掛けでソートしてみるとどうなるでしょうか.

Array.Sort<Dog>(ad, new Dcompare());//speedでソート
Array.Sort<Dog>(ad, new Acompare());//weightでソート
//Array.Sort<Animal>(aa, new Dcompare());//compile error

本来Dog配列をソートするのにはDcompare()を使って,speedプロパティによって並べ替えられます(1行目).しかし,上記例のとおりDcompare()が入るべきIComparer<Dog>型引数位置にIComparer<Animal>型を実装したオブジェクトを引き渡してweightプロパティで並び替えることもできます(2行目).一方IComparer<Animal>型が来るべき位置にIComparer<Dog>型を引き渡せません(3行目).2行目と3行目を暗黙的代入に書き換えると,それぞれ以下のようになります.

IComparer<Dog> x =  new Acompare();//可能(ダウンキャスト風)
IComparer<Animal> y =  new Dcompare();//コンパイルエラー(アップキャスト風)

これはAnimal型変数へDog型オブジェクトの代入が許されたアップキャストの逆の関係にみえます.この性質からIComparer<T>は反変性インタフェースであると言われます.

共変性インタフェースの例 IEnumerable<T>

共変性の性質をもつインタフェースの例としてIEnumerable<T>があります.class Dogs: IEnumerable<Dog>とclass Animals : IEnumerable<Animal>の2種類のクラスがあるものとします.この2つの変数型とオブジェクト型を入れ替えて代入してみると,一方がエラーになります.

IEnumerable<Animal> ae = new Dogs();
//IEnumerable<Dog> de = new Animals();//compile error

これはAnimal型変数へDog型オブジェクトの代入が許されたアップキャストと同じ関係にみえます.この性質からIEnumerable<T>は共変性インタフェースであると言われます.
一般的にIEnumerable<T>はメソッドに代入されるのではなく,foreach 文のinの後に置かれます.しかし,foreachでの型不一致はコンパイルエラーではなくランタイム時のキャスト例外になります.この場合でもIEnumerable<Dog>を置くべきところにIEnumerable<Animal>を置けないという共変性ルールは同じです.

foreach (Animal a in new Dogs())
{
    Console.WriteLine(a.ToString());
}
foreach (Dog d in new Animals())//不正キャスト例外
{
    Console.WriteLine(d.ToString());
}