Assignment compatibility

Reflection 3 部作*1完結編.最終回にあたる今回は「キャスト」について扱います.
C# はキャスト演算子のオーバーロードを許しているものの,これらは単なるシンタックスシュガーで,MSIL レベルではメソッド呼び出しに過ぎません.真の意味での「キャスト」は CLR の役割です.
さて,今回扱う「キャスト」ですが,具体的には次のようなコードを考えます.

public static T Cast<T>(object src)
{
    return (T)src;
}

まず src が object 型であることから,格納されているのは,参照型のインスタンスboxing された値型のインスタンス,または null に限られていることに注意してください.ちなみに .NET 2.0 で新たに追加された unbox.any OpCode を用いれば,このキャストを一命令で表現することが出来ます.
さて,このキャストが InvalidCastException を発生させるか否かは,C# であれば is 演算子を用いて判定可能です.

public static bool CanCast<T>(object src)
{
    if (src == null) throw new NullReferenceException();
    return src is T;
}

また,src に格納されたインスタンスの型からもキャスト可能性を判断することが出来ます*2

public static bool CanCast<T>(object src)
{
    return typeof(T).IsAssignableFrom(src.GetType());
}

これは,src に格納されたインスタンスの型を U = src.GetType() とすれば,T と U さえ決まればキャスト可能かどうかが決定できるということを意味しています.もちろん皆さんこの事実はご存じだったでしょうが,さてでは『CLR がキャストを許す T と U の関係を分類説明せよ』という質問はいかがでしょうか? 意外と正確で完全な説明というのは見かけないので,我こそはと思う人は挑戦してみてください.
ちなみに今回解答は用意しませんでした.ただしヒントは以下に書いておきます.ヒントといっても知識問題な部分については部分回答みたいなものですので,自分で考えたい/調べたいという人は満足してからどうぞ.


まず参考資料の紹介から.ECMA のページ.
ECMA and ISO/IEC C# and Common Language Infrastructure Standards
上に書いた「キャスト」の説明は私が適当に書いたものですが,CLI での正式な定義が必要な場合は Working Draft 2.9.1 「Partition I Architecture.doc」の以下の章が参考になります.

  • 8.7 Assignment compatibility
    • 8.7.1 Directly assignable
    • 8.7.2 Assignment Compatible
  • 8.8 Type safety and verification

さて,では本題に移りましょう.まずは有名どころから.

  • T と U が同じ型
  • U は T のサブタイプ
  • T が interface で,U が T を実装している

以上の条件を満たすときはキャスト可能というものです.この辺りは割と教科書的ですね.というかこれらのパターンしか書いていない参考書もあるかと思います.
さてここで先ほどの ECMA のページの ECMA-335: CLI Partition III - CIL を参照してみましょう.castclass OpCode の説明には以下のような注意があります.

  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

実際には,このように配列型の場合には注意が必要です.若干補足しておくと,2番目のルールは参照型の配列にのみ適応され,Fooが値型の場合はキャストはできません.以下にサンプルコードを示します.特に後の enum の例は,CLR 環境下ではこのキャストが常に成功することを C# コンパイラは知らないので*3,キャストを明示的に行う必要があります.

object objs = new string[10]; // OK
objs[0] = "Hello"; // objs は string 配列なので OK
objs[1] = 1; // objs は srting 配列なので実行時例外が発生 (type safe)

int a = (int[])(object)new ConsoleKey[10]; // OK

配列のこのような性質は covariant と呼ばれています.
さらに .NET 2.0 で導入された Generics も同じような性質をサポートしています.CLI はタイプパラメータを修飾する covariant と contravariant というメタデータを定義されています.CLR は Generics 型のキャストについて,これらのメタデータの有無を考慮します.
詳細については,最初に挙げたWorking Draft 2.9.1 の「Partition I Architecture.doc」に加え,「Partition II Metadata.doc」内の以下の項目も詳しく書かれています.

  • 9.5 Generics variance
  • 9.6 Assignment compatibility of instantiated types
  • 9.7 Validity of member signatures

なお,Draft に書かれているように,Generisc における covariant/contravariant のサポートは generic interfaces と generic delegates に制限されています.全ての Generics 型でサポートされるわけではありません.
さらに C# 2.0 は言語仕様としてタイプパラメータにcovariant/contravariant メタデータを付加することにも,covariant/contravariant な generics 型の存在も考慮していません.しかし,上の enum 配列のように,一旦 object 型にキャストした後さらに目的の型へキャストしても例外は発生しません.
さて C# 2.0 コンパイラが covariant/contravariant メタデータの付加に対応していないとしたら,どうやってテスト用の generic interfaces または generic delegates を作成するのがよいでしょうか?
最も簡単な方法は,MSIL で直接記述する方法です.ただし最初から全てを手書きするのは面倒なので,ひな形は C# で作り,出来たアセンブリを ildasm.exe でディスアセンブルすれば良いでしょう.そして Generics のタイプパラメータの前に,Covariant であれば "+" を,Contravariant であれば "-" を書き加えます.
それすらも面倒という方のために,サンプルを用意しておきました.
http://www.dwahan.net/nyaruru/hatena/variance.zip
(追記2005/11/20:Covariance と Contravariant の指定が逆となった意図しない IL 及びアセンブリになっていたのを修正しました)
以下のような仮想的な C# のコードに対応する IL ファイル及びアセンブリが入っています.

namespace Variance
{
    interface IEnumerator<+T>
    {
        T Current { get; }
        bool MoveNext();
    }
    interface IEnumerable<+T>
    {
        IEnumerator<T> GetEnumerator();
    }
    interface IComparer<-T>
    {
        bool Compare(T x, T y);
    }
    public delegate A Function<+A>();
    public delegate A Function<+A, -B>(B b);
    public delegate A Function<+A, -B, -C>(B b, C c);
}

試しに covariant return type を持つ generic delegate を使ってみましょう.

using System;
using Variance;

static class VarianceTest
{
    public static object Test1()
    {
        return 1;
    }
    public static string Test2()
    {
        return "One";
    }

    delegate object MyFunc1();
    delegate string MyFunc2();

    static void Main(string[] args)
    {
        Function<object> f1 = Test1;
        Function<string> f2 = Test2;

        // covariant な type parameter を持つ generic delegate なのでキャスト可能
        f1 = (Function<object>)(object)f2;
        Console.WriteLine(f1());

        MyFunc1 myf1 = Test1;
        MyFunc2 myf2 = Test2;

        // CLI は戻り値型について covariant な delegate の“作成”をサポート
        MyFunc1 myf1x = Test2;
        // しかしキャストはサポートしない
        myf1 = (MyFunc1)(object)myf2;
        Console.WriteLine(f1());
    }
}

なお,MyFunc2 から MyFunc1 へのキャストが例外を出力することによく注意してください.Working Draft 2.9.1 の「Partition II Metadata.doc」の次の項目に書かれているのは,あくまで delegate インスタンス作成時の話です.

  • 14.6.1 Delegate signature compatibility

delegate インスタンス作成時には,戻り値について covariant / 引数について contravariant なメソッドへバインドすることができます.しかし,一旦作成してしまった delegate を他の delegate にキャストする際には,covariant/contravariant は基本的にサポートされません.ただし,そのように定義された generic delegate であれば可能という話です.
最後に,nullable についても触れておきましょう.以下の呼び出しは,現在 Visual Studio 2005 beta2 をやそれ以前のベータ製品をお使いの方の環境では,false が返されるかと思います.

typeof(int?).IsAssignableFrom(typeof(int))

しかし,つい3ヶ月前に行われた仕様変更で,この呼び出しは true を返すようになりました.こうやって他の仕様とあわせて見ると,波村さんが以前おっしゃっていた「CLR の型システムに手を入れる大きな変更」という意味が実感できますね.
とまあ私が思い付く「キャスト」絡みのトピックはこんな感じです.後は綺麗にまとめれば私の回答にはなるのですが,それが解答かどうかはちょっと自信ないんですよね.



参考資料.
「Daigo Hamura's Weblog」より

「青柳臣一 blog : .NET や C# がメインの blog」より

*1:私の脳内設定では,第1部が(id:NyaRuRu:20051105#p3),第2部が(id:NyaRuRu:20051109#p2)とその解答編(id:NyaRuRu:20051112#p2)という扱いです.

*2:実際には Type.IsAssignableFrom の結果と castclass/isinst/unbox.any 等の結果が異なってしまう可能性が存在することは,第2回解答編(id:NyaRuRu:20051112#p2)で紹介した通りです

*3:ただし,全ての enum 配列が int 配列にキャストできるというわけではありません.どのような underlying type を持つ enum 型かが重要です.