読者です 読者をやめる 読者になる 読者になる

Array Covariance

.NET

修論審査も終わってひと段落なので,ここ半月ぐらいで気になっていたエントリをぼちぼち消化していこうかと.というわけで「soutaroにっき」より.

List<string>を、ComboBoxに.Items.AddRangeを使って追加しようと思ったら、これが大変。


List<string> list = ...;
comboBox1.Items.AddRange(list.ToArray());

とすると、(AddRangeの引数の型である)object と(list.ToArray()の型である)string の間に継承関係がないためコンパイルできないのだ。これはhttp://www.hyuki.com/d/200508.html#i20050819104841の問題であり、完全に解決することは不可能である。


とりあえず System.Object と System.String の間にはれっきとした継承関係がありますが,これままあ「はてな記法の罠」で元々は System.Object[ ] と System.String[ ] と書かれていたと推測.
んで,このあたりについては C# の built-in conversion と CLI の型検査は分けて知っておいた方が良いかもしれません.(id:NyaRuRu:20051115#p1)でも書きましたが,CLI はいわゆる「クラスの継承関係」と「インターフェイスの実装関係」以外にもいくつかの型変換を許しています.そして C# コンパイラはそのいくつかを知っていて,いくつかを知りません.例えば以下は(参照型の)配列に関する Covariance は知っているよ(コンパイル通すよ),という例.

object[ ] array = new string[ ]{ "One", "Two", "Three" };
array[0] = "0";
array[1] = (object)1; // Runtime-Error (System.ArrayTypeMismatchException)

System.String 型の配列に整数が代入されてしまうコードもコンパイルが通ってしまいますが,これについては CLR が実行時検査で防ぐという形をとっています.
一方,C# コンパイラCLI が許す次の変換は知らん振りです.そのためこの例では一旦 object 型にキャストしてやる必要があります.

DayOfWeek[ ] enumArray = new DayOfWeek[ ]{DayOfWeek.Monday};
int[ ] intArray1 = (int[ ]) enumArray; // Compile Error
int[ ] intArray2 = (int[ ]) (object) enumArray; // Succeed
bool  ret1 = enumArray is int[ ]; // false
bool  ret2 = (object)enumArray is int[ ]; // true

ちなみにこの enumArray is int[ ] が false を返すのは色々まずいだろうというのが昨年末から延々とやりあっているFDBK40806なわけですが,とりあえずその話はおいておいて次に進みます.
これら CLI が定める配列型のキャスト可能性については,CLI 仕様の castclass opcode の項目にまとまっています.

2 4.3 castclass – cast an object to a class

Description:

The castclass instruction attempts to cast obj (an O) to the class. Class is a metadata token (a typeref or typedef), indicating the desired class. If the class of the object on the top of the stack does not implement class (if class is an interface), and is not a subclass of class (if class is a regular class), then an InvalidCastException is thrown.

Note that:

  1. Arrays inherit from System.Array.
  2. If Foo can be cast to Bar, then Foo[ ] can be cast to Bar[ ].
  3. For the purposes of 2., enums are treated as their undertlying type: thus E1[ ] can cast to E2[ ] if E1 and E2 share an underlying type If obj is null, castclass succeeds and returns null. This behavior differs from isInst.

System.String は System.Object にキャストできるので System.String[ ] 型のオブジェクトは System.Object[ ] にキャストできます.System.DayOfWeek と System.ConsoleKey は同じ System.Int32 を undertlying type を持つので,System.DayOfWeek[ ] 型のオブジェクトは System.ConsoleKey[ ] にも System.Int32[ ] にもキャスト可能です.さらにこれらを組み合わせて,System.DayOfWeek[ ][ ] 型のオブジェクトが System.ConsoleKey[ ][ ] 型にキャストできることになります.

Covariance/Contravariance と言えば,Generic Delegates と Generic Interfaces の特例も触れておく必要があるでしょう.これは C# 2.0 Generics がサポートしない CLI の仕様なので,試すには一旦 IL に落とす必要があります.ゼロから書くのは面倒なので,雛型として次のような C# コードをコンパイルし,それを ildasm で IL に変換することにしましょう.

namespace Variant
{
    public delegate A Function<A>();
    public delegate B Function<A, B>(A argA);
    public delegate C Function<A, B, C>(A argA, B argB);
}

IL に落としたら,次のような Function<A> の宣言部分を探し出します.

.class public auto ansi sealed Variant.Function`1<A>
       extends [mscorlib]System.MulticastDelegate

ここで型パラメータ A の前に "+" を付けると,A について Covariant になります.

.class public auto ansi sealed Variant.Function`1<+ A>
       extends [mscorlib]System.MulticastDelegate

同様に "-"を付けるとその型について Contravariant になります.

.class public auto ansi sealed Variant.Function`3<- A,- B,+ C>
       extends [mscorlib]System.MulticastDelegate

実際にこれを使ってみましょう.ilasm で再コンパイルして,参照に追加してしまえば C# からも使うことは可能です.

public static DayOfWeek[ ] Test1()
{
    return new DayOfWeek[ ] { DayOfWeek.Monday };
}
public static int[ ] Test2(int[ ] a)
{
    return a;
}

Variant.Function<DayOfWeek[ ]> func1
   = new Variant.Function<DayOfWeek[ ]>(Test1);
Variant.Function<int[ ]> func1ex
   = (object) func1 as Variant.Function<int[ ]>;

Variant.Function<int[ ], int[ ]> func2
   = new Variant.Function<int[ ], int[ ]>(Test2);
Variant.Function<DayOfWeek[ ], int[ ]> func2ex
   = (object)func2 as Variant.Function<DayOfWeek[ ], int[ ]>;

これについても C# の built-in conversion 的には常に失敗するように見える型変換なので,コンパイラを黙らせるために一旦 object 型へのキャストが必要となっています.
とまああるならあるで使ってみようかという機能ではありますが,残念なことに C# コンパイラだけでなく標準ライブラリ Base Class Library (BCL) で全然使われていません.そのため BCL のクラスとの組み合わせは最悪を通り越して絶望に近いものがあります.派生させるときにきちんと整合性を取る必要があるので,System.Collections.Generic.IEnumerable<T> から IEnumerableEx<+T> を派生させるわけにも行きません.というわけで Covariant/Contravariant なコレクションライブラリが欲しければ,結局自分で再実装する羽目になるかと思います.
あと,紛らわしいのが通常のデリゲートの Covariance/Contravariance サポートです.C# コンパイラは,次のようなコードの実行を CLR がサポートしていることを知っていて,コンパイルを通します.

public delegate Stream MyFunc1();
public delegate void MyFunc2(MemoryStream ms);
public static MemoryStream MyFunc1Impl()
{
    return new MemoryStream();
}
public static void MyFunc2Impl(Stream s)
{
    ms.Flush();
}

static void Foo()
{
    MyFunc1 myfunc1 = MyFunc1Impl;
    MyFunc2 myfunc2 = MyFunc2Impl;
}

CLR はデリゲートインスタンスを生成するときに戻り値に関して Covariant,引数に関して Contravariant なメソッドを受け付けます.ただし生成後のデリゲートインスタンスについては特別な Assignment compatibility はありません.この点において Generic Delegates の時とは決定的に異なります.
なお,C# コンパイラenum に関する Covariance を想定しないのはここでも同じです.CLR 自体は DayOfWeek[ ] を返すメソッドから delegate int[ ] Foo() のインスタンス生成をサポートしますが,C# コンパイラはあくまで失敗することを前提にコンパイルを行います.

static class Test
{
    public delegate int[ ] Foo();
    public static DayOfWeek[ ] Bar()
    {
         return new DayOfWeek[ ]{ DayOfWeek.Monday };
    }
    static void Main(string[ ] args)
    {
        // Compile Error
        // Foo foo1 = Bar;

        // But CLR Supports this
        Foo foo2 = (Foo) Delegate.CreateDelegate(typeof(Foo), typeof(Test).GetMethod("Bar"));

        int[ ] ret = foo2();
        Console.WriteLine(ret[0]);
    }
}

最後に ComboBox.Items ですが,確かに ObjectCollection.AddRange メソッドは object[ ] を受け取るものしかなくて微妙にイケてない感じですね.とは言え string[ ] 型のインスタンスは object[ ] 型にキャスト可能ですので,次のコードはコンパイルが通ります(し,実行も出来ました).

List<string> list = new List<string>();
list.Add("One");
list.Add("Two");
list.Add("Three");
this.comboBox1.Items.AddRange(list.ToArray());

というわけでこの点はどうも何か勘違いがあるような気がしています.とは言え int[ ] などの値型の配列から object[ ] への変換は確かに新しくインスタンスを作ることになるでしょう.(id:NyaRuRu:20051013#p3) で触れた Sequence.cs (System.Query.dll) を使って,LINQ の流儀でコンボボックスに 0 から 9 までを追加するとしたらこんな感じでしょうか.

this.comboBox1.Items.AddRange(
    Sequence.ToArray<object>(
      Sequence.Select<int, object>(
        Sequence.Range(0, 10),
        delegate(int i) { return (object)i; } )));