てくメモ

trivial な notes

【C#】共変性と半変性 やさしく

用語を知らない人でも利用したことのありそうな仕組みランキング上位(独自調べ)、共変性と反変性について。
備忘のイントロとしてやさしく [要出典] 整理。


継承関係を持つクラス。

private class Animal { }
private class Cat : Animal { }


次のようなことができる。

IEnumerable<Cat> getCats() => Enumerable.Empty<Cat>();

// IEnumerable<Cat> を IEnumerable<Animal> で受け
IEnumerable<Animal> animals = getCats();


どうして? Cat が Animal を継承してるから?
でも、次のようにはできない。

// ❌できない
List<Animal> animalList = getCats().ToList();

// class Dog : Animal があるとして
animalList.Add(new Dog()); // ❌ 実体は List<Cat> なところに Dog はダメ


最初の例が成立するのは、Cat が Animal を継承するから自明に導かれるわけではなく、IEnumerable<T>共変性を持ってくれているから。

// IEnumerable<T> の定義を見ると、型引数に out が修飾されている。共変のサイン
interface IEnumerable<out T>


入力があるのでList<T>の例はダメ。
けれども、IEnumerable<T>のように型引数を出力にしか使わないのであれば、継承型でも安全になる。それなら、最初の例のような性質は成立してもいい。
それが共変性。




次に、以下の例を見てみる。

var catList = getCats().ToList();

// IComparer<Cat> な引数に対し IComparer<Animal> を渡し
catList.Sort(Comparer<Animal>.Default);

今度は型引数では継承型のところに基底型を渡している。
これは……?

// IComparer<T>の定義を見ると、型引数に in が修飾されている。反変のサイン
interface IComparer<in T>


こちらは、反変性という性質になっている。

こちらは共変性とは逆に、型引数が入力にしか使われない。
入力だけであれば、基底型でも安全(例なら IComparer<Animal> に Cat を渡すのは安全)だから、この性質は成立してもいい、ということ。




ここまでジェネリックインターフェースで共変性と半変性を見てきた。
また、

  • 総称して変性と呼ぶ
  • C#において、値型では変性は認められていない
  • C#において、デリゲートでも変性を利用できる


なお、C#において配列は共変であるが、入力がある以上、本来は認められないはずのもの。
これはジェネリックが存在しない最初期の C# の取り返しのつかない名残ということ。
次のような例は実行時例外なので頭に入れておきたい。

Animal[] animalArray = new Cat[] { new(), new() };
animalArray[0] = new Dog(); // ❌ ArrayTypeMismatchException の実行時(!)例外


共変はIEnumerable<T>のほかIObservable<T>など標準でも複数のインターフェースが備えていたり、反変はとりあえずIComparer<T>IEqualityComparer<T>が備えていたりと近くにいるので、遠くない概念にしておきたい。


参考:
ジェネリクスの共変性・反変性 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C
共変性と反変性 (C#) | Microsoft Learn