余談3: C#の配列は共変

C#の配列には共変性があります。つまり、以下のコードがコンパイルできます。

string[] derivedItems = { "Aleph", "Beth", "Gimel" };
object[] baseItems = derivedItems;

// 読み出し(戻り値側、out、共変)は常に安全
for (int i = 0; i < baseItems.Length; i++)
{
    Console.WriteLine(baseItems[i]);
}

逆向き(反変な代入)はできません。

object[] baseItems = { 1, 2, 3 };
string[] derivedItems = baseItems; // コンパイル エラー

C#の配列が共変なのは、ジェネリックがなかった時代(C# 1.0の頃)の名残です。 本当は認めるべきではありません。

共変性は、本来、出力(読み出し)になる型にしか認められません。 しかし、配列は、同じ型に対して入力(書き込み)もできます。 配列に対して特別に共変性を認めてしまっているので、以下のような問題が起きます。

string[] derivedItems = { "Aleph", "Beth", "Gimel" };
object[] baseItems = derivedItems;

// 書き込み(引数側、in、反変)は本当はやっちゃいけない
// でも、コンパイルが成功する。実行時エラーが出る
baseItems[1] = 100;

本当はコンパイル自体できてはいけないコードですが、実行してみるまでエラーになりません。 IEnumerable<T>IReadOnlyCollection<T>などのジェネリックなインターフェイスを介してのアクセスであれば、こういう問題のあるコードは書けません。

引数でインターフェイスやデリゲートを受け取る場合

ジェネリックなインターフェイスやデリゲートを引数として渡す場合、in/outの向きが逆転します。 (戻り値の場合は逆転しません。) 例えば以下のようになります。

// 標準ライブラリの System.Func
public delegate TResult Func<in T, out TResult>(T arg);

// 引数の Func の TIn と TOut が逆
delegate Func<TIn, TOut> F<in TIn, out TOut>(Func<TOut, TIn> x);

in/out 注釈は、値を受け取る(in)か渡す(out)かの区別です。 引数で受け取ったインターフェイスやデリゲートの場合、「戻り値から値を受け取る」、「引数に値を渡す」ということになるので、こういう逆転が起きます。

interface INestedVariance<in TIn, out TOut>
{
    TOut F(TIn x, Func<TOut, TIn> f);
}

class NestedVariance<TIn, TOut> : INestedVariance<TIn, TOut>
{
    public TOut F(TIn x, Func<TOut, TIn> f)
    {
        // f の戻り値から値を受け取る = in
        TIn in1 = f(default(TOut));

        // f の引数にはこちらから値を渡す = out
        TOut out1 = default(TOut);
        var r = f(out1);

        // 引数から受け取る = in
        TIn in2 = x;

        // 戻り値を返す = out
        TOut out2 = default(TOut);
        return out2;
    }
}

実用例の代表は、IObserver<T>インターフェイスとIObservable<T>インターフェイス(どちらも標準ライブラリのSystem名前空間に含まれるインターフェイス)でしょう。 以下のようなインターフェイスになっています。

public interface IObserver<in T>
{
    void OnCompleted();
    void OnError(Exception error);
    void OnNext(T value);
}

public interface IObservable<out T>
{
    IDisposable Subscribe(IObserver<T> observer);
}

更新履歴

ブログ