C#と諸々

C#がメインで他もまぁ諸々なブログです
おかしなこと書いてたら指摘してくれると嬉しいです(´・∀・`)
つーかコメント欲しい(´・ω・`)

--/--/-- --:--
上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。
タグ:
トラックバック(-) | コメント(-) | このエントリーを含むはてなブックマーク
2008/06/21 03:07
C#と諸々 Nullable 型のボックス化


GetType メソッドは Object クラスで定義されている非仮想メソッドなので、GetType メソッドを値型に対して呼び出す際はボックス化が必要


と書きましたが、もうちょい詳しく書いときます。
# 今回の記事は Nullable 型に限らない話です。

[ Object クラスに定義されている非仮想メソッド ]
Object クラスには他にも非仮想メソッドとして MemberwiseClone メソッドが定義されていますが、このメソッドを呼び出す場合も同様にボックス化が発生します。
ポイントは次の 2 点です。

・ インスタンスメソッドは自分自身を取得できる
インスタンスメソッドでは、this キーワードにて自分自身を取得できます。しかしこれは、C# の話であって、IL レベルでは、this キーワードに相当する機能は用意されていません。
ではどうやって自分自身を取得するかというと、実はメソッドの 0 番目の引数として自分自身が渡されます。

・ 非仮想メソッドはオーバーライドできない
非仮想メソッドを派生クラスがオーバーライドすることはできません。つまり、GetType メソッドはオーバーライドされていないわけです。GetType メソッドは Object クラスに定義されています。

この 2 点を踏まえて考えてみてください。
GetType() メソッドの 0 番目の引数の型、つまり GetType メソッドにおける this の型は何型でしょう?
これがボックス化の理由です。
答えは Object 型ですね。


[ コンストラクタについて ]
値型は ValueType クラスを継承します。ValueType クラスはあくまでも参照型です。
値型にはコンストラクタを定義できます (C# の場合引数を持たないコンストラクタを値型に定義することはできません)。通常、コンストラクタは基底クラスのコンストラクタを呼び出さなければいけません。そしてコンストラクタは非仮想メソッド (正確には継承されないメソッド) です。ということは、コンストラクタが呼び出されるとボックス化が発生するのでしょうか?
答えは「発生しない」です。これは、値型のコンストラクタは基底クラスのコンストラクタを呼び出さないからです (これが CLR で決められているルールなのかはちょっとわからなかったです)。


[ ValueType クラスでオーバーライドされているメソッドについて ]
「プログラミング Microsoft .NET Framework 第2版」によると、ToString メソッドや Equals メソッド等の、ValueType クラスでオーバーライドされているメソッドに対してはボックス化が行われないと書いてあります。でも、ValueType クラスは参照型です。ホントにボックス化されないのでしょうか?
答えは「される」です。つまり、「プログラミング Microsoft .NET Framework 第2版」に書かれているのことは正しくないです。やはり、ValueType は参照型だからボックス化されるのです。

当然ですが、値型自身が更に ToString メソッドをオーバーライドしている場合は、ボックス化はされません。0 番目の引数の型は値型ですので。


以降、このことについて MSIL レベルで説明をします。
まず、Int32 の値に対して ToString メソッドを呼び出すコードを見てみます。

static void Main(string[] args)
{
    int a = 10;
    a.ToString();
}

このコードは次のような MSIL コードにコンパイルされます。

.method private hidebysig static void Main(string[] args) cil managed
{
    .entrypoint
    .maxstack 1
    .locals init (
        [0] int32 a)
    L_0000: ldc.i4.s 10
    L_0002: stloc.0
    L_0003: ldloca.s a
    L_0005: call instance string [mscorlib]System.Int32::ToString()
    L_000a: pop
    L_000b: ret
}

L_0003 と L_0005 をみると、ローカル変数 a のマネージポインタをプッシュして Int32.ToString メソッドを呼び出しています。これは特に変わったところもありません。ローカル変数 a に対して ToString メソッドを呼び出すごく普通の MSIL です。(ちなみに、マネージポインタをプッシュしている理由ですが、マネージポインタではなくローカル変数 a を直接プッシュしてしまうとローカル変数 a のコピーがプッシュされてしまうためです。)


では、次のようなコードはどうでしょうか。

static void Main(string[] args)
{
    Hoge h = new Hoge();
    h.ToString();
}

struct Hoge
{
}

このコード (の Main メソッド) は次のような MSIL コードにコンパイルされます。

.method private hidebysig static void Main(string[] args) cil managed
{
    .entrypoint
    .maxstack 1
    .locals init (
        [0] valuetype Program/Hoge h)
    L_0000: ldloca.s h
    L_0002: initobj Program/Hoge
    L_0008: ldloca.s h
    L_000a: constrained Program/Hoge
    L_0010: callvirt instance string [mscorlib]System.Object::ToString()
    L_0015: pop
    L_0016: ret
}

L_0008, L_000a, L_0010 を見てください。
L_0008 については、先ほどのコードの L_0003 と同様ローカル変数 h のマネージポインタをプッシュしています。問題は次の L_000a と L_0010 です。

まず L_0010 ですが、Hoge.ToString を call 命令で呼び出すのではなく、 Object.ToString を callvirt 命令で呼び出しています。
もし、Hoge.ToString を call 命令で呼び出していれば、CLR は最初から Hoge.ToString を使用することを知ることができます。
しかし、Object.ToString を callvirt 命令で呼び出しているということは、遅延バインディング (実行時に変数 h の型を調べて適切なメソッドを判断) するということになります。

次に (一行戻って) L_000a ですが、この constrained が鍵を握っています。これは callvirt 命令にだけ付加することができるプリフィックスです。このプリフィックスでは、一緒に型を指定します (以降、thisType と記述)。
thisType が値型の場合、次の 2 つのケースでそれぞれ異なる動作をします。

  1. tisType が callvirt 命令で指定されているメソッドを実装していない場合
  2. tisType が callvirt 命令で指定されているメソッドを実装している場合

今見ている Hoge 構造体は、ToString をオーバーライドしていませんので 1 に該当します。
この場合、ローカル変数 h はボックス化されて ToString メソッドに渡されます。まぁ、先ほども言ったように、ValueType クラスは参照型ですから当然です。そして、Object.ToString を callvirt 命令で呼び出したことになります。つまり、メソッドの遅延バインディングが発生します。


では、Hoge 構造体で ToString メソッドをオーバーライドして再コンパイルしたとします。その場合も Main メソッドの MSIL コードは変わりません。すると 2 に該当することになります。
この場合、ローカル変数 h はボックス化されずに ToString メソッドに渡されます。これも適切な動作です。また、Hoge.ToString メソッドを call 命令で呼び出した場合と同じ動作をします。つまり、メソッドの遅延バインディングが発生しません。


なぜ、Int32 値の ToString メソッドを呼び出すコードでは、constrained プリフィックスが使用されなかったのでしょうか?
恐らくそれは、バージョン問題が発生しないからです。
今回の Hoge 構造体のように、ユーザー定義の構造体のインスタンスに対して、ValueType クラスでオーバーライドされているメソッドを呼び出す場合はバージョンの問題が発生する可能性があります。
たとえば、最初は Hoge.ToString を定義せずにコンパイルしたが、後から Hoge.ToString を定義 (オーバーライド) して再コンパイルした場合、利用側のコードはコンパイル前と後で異なる動作をしなければなりません。もし constrained プリフィックスがなければ、利用側のコードも再コンパイルしないといけなくなるわけです。
というわけで、constrained プリフィックスのおかげで、再コンパイルせずに動作を切り替えられるわけです。


# すごい疲れた


[ 参考 ]
OpCodes.Constrained フィールド (System.Reflection.Emit)
タグ: .NET C# CLR











トラックバックURL↓
http://csharper.blog57.fc2.com/tb.php/222-8228ff5c