配列をジェネリックリストにキャストするとおかしいことになる


Array クラスArray クラスは IList インターフェイスを実装していますが, IList<T> インターフェイスは実装しません。したがって次のコードは実行できないように思われます。

string[] array = new string[1];
IList list = (IList)array;
IList<string> genericList = (IList<string>)array;

ところが実際に動かしてみると何のエラーも起こらずに普通に動きました。リフレクションでインターフェイスを調べてみると,ジェネリックリストのインターフェイスが実装されていたので,コンパイル時にそういうことが行われているんだろうと納得しました。というかちゃんと MSDN に書いてありました。しかも重要って書いてありました。

.NET Framework Version 2.0 では、 Array クラスは System.Collections.Generic.IList(T)、 System.Collections.Generic.ICollection(T)、および System.Collections.Generic.IEnumerable(T) の各ジェネリック インターフェイスを実装します。

反省しつつも,もう少し調べてみると不思議なことが起こりました。

Console.WriteLine(array.IsReadOnly);  // False
Console.WriteLine(list.IsReadOnly);  // False
Console.WriteLine(genericList.IsReadOnly);  // True

上の 2 つは期待通りの動作をするのですが,最後の挙動は不思議です。じゃあ IList<string> にキャストしたら本当に読み込み専用になるのかというとそうでもないようです。

genericList[0] = "hoge";
Console.WriteLine(array[0]);  // hoge
Console.WriteLine(list[0]);  // hoge
Console.WriteLine(genericList[0]);  // hoge

でもやはりジェネリックか否かで IsReadOnly の結果は違うのです。コレクションで実装されるかリストで実装されるかの違いが原因なのでしょうか。

// ICollection.IsReadOnly は存在しない。
// Console.WriteLine(genericList.IsReadOnly == ((ICollection)genericList).IsReadOnly);
Console.WriteLine(genericList.IsReadOnly == ((IList)genericList).IsReadOnly);  // False
Console.WriteLine(genericList.IsReadOnly == ((ICollection<string>)genericList).IsReadOnly);  // True

同じ EXE ファイル (Visual Studio でコンパイル) を mono で実行すると, genericList.IsReadOnlyFalse を返しました。これ妥当な結果を返していると思います。逆に mono でコンパイルした EXE ファイルを .NET Framework で動かすと,上で述べてきたような変な結果になります。つまりは .NET Framework ランタイムのバグということでしょうか。

問題になるのは IsReadOnly で条件判定している場合ですね。

public void InitializeList<T>(IList<T> list)
{
	if (list == null)
	{
		throw new ArgumentNullException();
	}
	if (list.IsReadOnly)
	{
		throw new ArgumentException();
	}

	for (int index = 0; index < list.Count; index++)
	{
		list[index] = default(T);
	}
}

あまりあるケースだとは思いませんが,一応ジェネリックリストを引数にとるメソッドは引数が配列であるかどうかをチェックした方が良いかもしれません。