反変性と共変性

ここ数日、こいつで引っかかったけど、ようやく理解できたので備忘録。

コトの発端

こんなコードを書いていた。

interface ISample<in T>
{
	void InvokeAction(Action<T> action);
}

そしたら、コンパイラに怒られた。

無効な分散: 型パラメーター 'T' は、'ConsoleApplication1.ISample<T>.InvokeAction(System.Action<T>)' で有効な 共変的 である必要があります。'T' は 反変 です。

これが直感的に理解できなかったってのがコトの発端でして。

何故これがダメなのか?

これが許容されると、こんなワケのわからないコードがコンパイル可能と言うことに帰結する。

internal class Program
{
	private static void Main(string[] args)
	{
		ISample<string> sample = new Sample<object>(200);

		//ちょっとマテ、元はobjectだし、実際入ってるのはintだし、これは拙くなイカ?
		sample.InvokeAction((string value) => Console.WriteLine(value.Substring(2)));
	}

}

internal interface ISample<in T>
{
	void InvokeAction(Action<T> action);
}

class Sample<T> : ISample<T>
{
	public Sample(T value)
	{
		Value = value;
	}

	public T Value { get; set; }

	public void InvokeAction(Action<T> action)
	{
		action(Value);
	}
}

Actionの型パラメタは反変だから、インターフェースの型パラメタも反変なら、このように書けるだろうと、直感的に思いがちだけど、さに非ず、実際は反変の型パラメタを持つActionを使いたいのなら、インターフェースの型パラメタは以下のように共変である必要がある。

internal class Program
{
	private static void Main(string[] args)
	{
		ISample<object> sample = new Sample<string>("hello world");

		//これなら、常にアップキャストが保障されるので、大丈夫
		sample.InvokeAction((object value) => Console.WriteLine(value));
	}

}

internal interface ISample<out T>
{
	void InvokeAction(Action<T> action);
}

internal class Sample<T> : ISample<T>
{
	public Sample(T value)
	{
		Value = value;
	}

	public T Value { get; set; }

	public void InvokeAction(Action<T> action)
	{
		action(Value);
	}
}

よくよく考えれば当たり前だけど、引っかかったので、備忘録的に。