C#と諸々

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

2008/04/24 01:37
配列は特殊な型で、必要に応じて自動的に定義される型です。配列は必ず Array クラスを継承します。
で、配列が定義される際、どうも Address メソッドというものが定義されるようです。開発者がこのメソッドを呼び出すことは普通はできません。
PowerShell で以下のコードを実行すれば、この Address メソッドを確認できます。

$addressMethod = [Int32[]].GetMethod("Address");
$addressMethod.ToString(); # Int32& Address(Int32)


このメソッドは Int32 型のパラメータを一つ持っているようです。戻り値は、なんと Int32 の ByRef 型です。つまり、Int32 値への参照を返します。
動作は何となく予想が付きます。たぶん、指定したインデックス番号の位置にある要素への参照を返すメソッドでしょう。
一応検証してみます。
using System;
using System.Reflection;
using System.Reflection.Emit;

public static void Main()
{
    DynamicMethod addressInvoker = new DynamicMethod(string.Empty, typeof(int), null);
    ILGenerator invokerILGenerator = addressInvoker.GetILGenerator();

    invokerILGenerator.DeclareLocal(typeof(int[]));             // Int32[] 型のローカル変数を宣言
    invokerILGenerator.Emit(OpCodes.Ldc_I4, 2);                 // 2 を Int32 型としてスタックにプッシュ (要素数)
    invokerILGenerator.Emit(OpCodes.Newarr, typeof(int));       // 要素数 2 の Int32[] オブジェクトを生成
    invokerILGenerator.Emit(OpCodes.Stloc_0);                   // 0 番目のローカル変数に設定

    invokerILGenerator.Emit(OpCodes.Ldloc_0);                   // 0 番目のローカル変数をスタックにプッシュ
    invokerILGenerator.Emit(OpCodes.Ldc_I4, 0);                 // 0 を Int32 型としてスタックにプッシュ (配列のインデックス)
    invokerILGenerator.Emit(OpCodes.Ldc_I4, 1234);              // 1234 を Int32 型としてスタックにプッシュ (要素 0 の値)
    invokerILGenerator.Emit(OpCodes.Stelem_I4);                 // 配列の 0 番目の要素に 1234 を設定

    invokerILGenerator.Emit(OpCodes.Ldloc_0);                   // 0 番目のローカル変数をスタックにプッシュ
    invokerILGenerator.Emit(OpCodes.Ldc_I4, 1);                 // 1 を Int32 型としてスタックにプッシュ (配列のインデックス)
    invokerILGenerator.Emit(OpCodes.Ldc_I4, 5678);              // 1234 を Int32 型としてスタックにプッシュ (要素 0 の値)
    invokerILGenerator.Emit(OpCodes.Stelem_I4);                 // 配列の 0 番目の要素に 5678 を設定

    MethodInfo addressMethod = typeof(int[]).GetMethod("Address");
    invokerILGenerator.Emit(OpCodes.Ldloc_0);                   // 0 番目のローカル変数をスタックにプッシュ
    invokerILGenerator.Emit(OpCodes.Ldc_I4, 0);                 // 0 を Int32 型としてスタックにプッシュ (引数)
    invokerILGenerator.Emit(OpCodes.Callvirt, addressMethod);   // Int32[].Address メソッドを呼び出す

    invokerILGenerator.Emit(OpCodes.Ldind_I4);                  // 戻り値である参照が指す値をスタックにプッシュ
    invokerILGenerator.Emit(OpCodes.Ret);                       // 返却

    int result = (int)addressInvoker.Invoke(null, null);
}


なぜわざわざ動的メソッドを作って呼び出しを行っているのかというと、戻り値が ByRef 型のメソッドは MethodInfo.Invoke で呼び出すことができないからです。
この動的メソッド内では、まず、要素数が 2 の配列を生成し、0 番目に 1234、1 番目に 5678 を設定します。そして、Address メソッドに 0 を渡して呼び出します。最後にその戻り値である参照が示す値を取得して呼び出し元に返却します。
このコードを実行すると、予想通り 1234 が取得できます。


で、ここからが本題です。
このメソッドは何のためにあるのでしょうか?
次のコードを見てください。
static void Main()
{
    int[] array = { 0, 1, 2 };
    Method1(ref array[0]);
    Console.WriteLine(array[0]);
}

static void Method1(ref int arg1)
{
    arg1 = 10;
}


普通に考えると、プロパティやインデクサを参照パラメータにそのまま渡すことはできないので、このコードはコンパイルエラーになりそうです。しかし、このコードはコンパイルが通りますし、コンソールにはちゃんと 10 が出力されます。
ということで、答えはメソッドの ref パラメータに配列の要素の参照を渡すためです。
といっても、実は Address メソッドは上記のコードでは使われません。Address メソッドは下限が 0 の 1 次元配列に対しては使われないからです。このような配列のことを SZ 配列 (SZArray) と呼びます。SZ は「single-dimension, zero-base」の略です。

SZ 配列ではない配列の要素を参照渡するコードは、Address メソッドにて参照を取得するよう、コンパイルされます。
では、SZ 配列ではどのようにして参照を取得するのでしょうか?
SZ 配列には特別な IL 命令が使用できます。newarr, ldelem, ldelema, ldlen, stelem などの命令がそうです。これらの命令は SZ 配列にしか使用できません。この内の ldelema 命令は配列の要素の参照を取得するための命令です。つまり、SZ 配列の要素を参照渡しするコードは、ldelema 命令にて参照を取得するよう、コンパイルされるわけです。

Address メソッドの他にも、配列には Get メソッド (指定したインデックスの要素の値を取得) と Set メソッド (指定したインデックスの要素に値を設定) が自動的に定義されます。これらも SZ 配列ではない配列で使用され、SZ 配列では ldelem 命令と stelem 命令が使用されます。


SZ 配列では、このように、特別な IL 命令が使用されるため、パフォーマンスが向上するようです。
タグ: .NET C# CLR











トラックバックURL↓
http://csharper.blog57.fc2.com/tb.php/209-3064cc98