Unity & C# 学習教材

共変・反変

Container<Derived>Container<Base> に代入しようとするとコンパイルエラーになります。しかし、インターフェイスの型パラメータに outin を付けると、継承関係に応じた代入ができるようになります。この性質を変性(variance)と呼びます。

学習目標

前提知識


1. 不変(invariant)

ジェネリッククラスは既定で不変です。T に継承関係があっても、そのジェネリック型同士に代入互換はありません。

1
2
class Base { }
class Derived : Base { }
1
2
3
4
class Container<T>
{
    public T Value { get; set; }
}
1
2
3
4
Container<Derived> d = new Container<Derived>();

// ❌ コンパイルエラー: Container<Derived> は Container<Base> ではない
Container<Base> b = d;

Container<T> は読み書き両用のため、Base を書き込んだコンテナを Derived として読み出すのは型安全でないからです。


2. 共変(covariant)— out T

読み取り専用のインターフェイスなら、派生型のインスタンスを基底型として扱っても安全です。型パラメータに out を付けると共変になります。

書式:共変インターフェイス

1
2
3
4
interface インターフェイス名<out T>
{
    T メソッド名();  // T は戻り値にのみ使える
}
要素 説明
out T T を出力(戻り値)にしか使えない制約。共変であることを宣言する
1
2
3
4
5
6
7
8
9
10
11
interface IReader<out T>
{
    T Read();
}

class DataReader<T> : IReader<T>
{
    private T _data;
    public DataReader(T data) { _data = data; }
    public T Read() { return _data; }
}
1
2
3
4
5
IReader<Derived> derivedReader = new DataReader<Derived>(new Derived());

// ✅ OK: out T があるので IReader<Derived> → IReader<Base> に代入できる
IReader<Base> baseReader = derivedReader;
Base result = baseReader.Read();

読み取りしか行わないため、DerivedBase として読み出しても常に安全です。


3. 反変(contravariant)— in T

書き込み専用のインターフェイスなら、基底型を受け付けるハンドラを派生型用として使っても安全です。型パラメータに in を付けると反変になります。

書式:反変インターフェイス

1
2
3
4
interface インターフェイス名<in T>
{
    void メソッド名(T 引数);  // T は引数にのみ使える
}
要素 説明
in T T を入力(引数)にしか使えない制約。反変であることを宣言する
1
2
3
4
5
6
7
8
9
interface IWriter<in T>
{
    void Write(T value);
}

class Logger<T> : IWriter<T>
{
    public void Write(T value) { Console.WriteLine(value); }
}
1
2
3
4
5
6
IWriter<Base> baseWriter = new Logger<Base>();

// ✅ OK: in T があるので IWriter<Base> → IWriter<Derived> に代入できる
// Base を受け付けるハンドラは Derived も受け付けられる(Derived は Base だから)
IWriter<Derived> derivedWriter = baseWriter;
derivedWriter.Write(new Derived());

直感的に捉えると:「Base を処理できるなら、Base の一種である Derived も処理できる」。


4. 共変・反変・不変のまとめ

変性 キーワード 代入の方向 T が使える位置
不変 なし なし 戻り値・引数どちらでも
共変 out T 派生 → 基底 戻り値のみ
反変 in T 基底 → 派生 引数のみ

よくあるミス

1
2
3
4
5
6
interface IReadWrite<out T>
{
    T Get();
    // ❌ コンパイルエラー: out T は引数には使えない
    void Set(T value);
}

out T を付けたインターフェイスで T を引数として使おうとするとコンパイルエラーになります。


まとめ


理解度チェック

  1. IReader<out T>void Write(T value) を追加するとどうなりますか?

  2. 次のコードはコンパイルエラーになりますか? 理由とともに答えてください。

    1
    2
    3
    4
    
    interface IWriter<in T>
    {
        T Read();  // 戻り値に T を使っている
    }
    
  3. (応用)IReader<Derived>IReader<Base> に代入できる理由を、型安全性の観点から説明してください。

解答を見る
  1. コンパイルエラーになります。out TT を戻り値にしか使えないため、引数として使う void Write(T value) は書けません。

  2. コンパイルエラーになります。in TT を引数にしか使えないため、戻り値として使う T Read() は書けません。

  3. IReader<out T> は読み取りしか行わないため、Derived のインスタンスを Base として読み出しても常に安全です(DerivedBase であることが保証されているため)。書き込みがないので型の不一致が起きる余地がありません。


次のステップ

デリゲートの基本 では、メソッドへの参照を変数として扱うデリゲートのしくみを学びます。