と書きましたが、もうちょい詳しく書いときます。
# 今回の記事は 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 つのケースでそれぞれ異なる動作をします。
- tisType が callvirt 命令で指定されているメソッドを実装していない場合
- 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)
Type type = a.GetType();
Console.WriteLine(type.Name);
を実行すると、
Int32
って表示されるんだ。
type = a.GetType();
だと、
「オブジェクト参照がオブジェクト インスタンスに設定されていません。」
と、例外が発生する。
へー、そんな動きするんですね。
ちょっと調べてみましたところ、CLR の動作によるもののようでした。
まず、GetType メソッドは Object クラスで定義されている非仮想メソッドなので、GetType メソッドを値型に対して呼び出す際はボックス化が必要になります (Nullable 型は値型です) 。
そして、Nullable 型をボックス化する際、CLR は特別な動作をします。
Nullable 型が (論理的に) null である場合、ボックス化を行わず (正真正銘の) null を返します。
Nullable 型が (論理的に) null でない場合、Nullable 型が保持している生 (?) の値をボックス化して返します。
ということで、Nullable 型に対して GetType メソッドを呼び出しても、生の値、あるいは null に対して呼び出されてしまうというわけです。
# ちなみに、C# コンパイラが原因と予想して調べ始めたのですが、見事に外れました ^^;
ガベージコレクションを開始する際、CLR はまず、全てのスレッドを一時停止します。そして、それぞれのスレッドが実行中だったコードを確認し、「セーフポイント」に到達しているかどうかを調べます。
セーフポイントに到達しているスレッドは、ガベージコレクションが完了するまでそのまま一時停止にしておいて問題ありません。
セーフポイントに到達していないスレッドはそのまま一時停止にしておくわけにはいきません。しかし、ガベージコレクションの実行中は全てのスレッドを一時停止 しておく必要があります。そのため CLR は、スレッドの乗っ取りを行おうとします。簡単に言うと、現在実行中のメソッドの戻り先を書き換えてスレッドを再開させます。これにより、メソッドの完了後に特殊な関数を実行するようにし、この特殊な関数内でスレッドを一時停止するわけです。
ただし、乗っ取りを行おうとしてから 250 ミリ秒経過しても乗っ取れなかった (=メソッドが未完了) 場合、CLR は再びスレッドを一時停止し、再びセーフポイントに到達しているかどうかを調べます。到達している場合は、そのまま一時停止します。到達していない場合はスレッドの再開となりますが、前回乗っ取りを行おうとした時とは違うメソッドを実行していた場合は、再開前にそのメソッドの戻り先を書き換えます。再開後数ミリ秒待機しても乗っ取れなかった場合は、再びこの動作を繰り返します。
セーフポイントはコストが高いため、必要最低限の箇所にしか用意されません。具体的には、メソッド呼び出しを含まないループ構造にのみ用意されます。メソッド呼び出しを含まないループ構造では、いつまで経っても乗っ取りが出来ない可能性があるためです。
と、ここまでが前置きです。
これらを踏まえると、オブジェクトの型を破壊的に変換 のメソッドは、アンマネージポインタを取得してから型ハンドルの書き換えを行うまでの間にメソッド呼び出しを行わないようにすれば、ガベージコレクションによって問題を引き起こすことがなくなると思わます。
アンマネージポインタ取得してから型ハンドルの書き換えを行うまでの間のメソッド呼び出しは、Type.TypeHandle プロパティの呼び出しと、RuntimeTypeHandle.Value プロパティの呼び出しのみです。つまり、newType 引数から型ハンドルを取得する処理をアンマネージポインタ取得処理より前で行うように修正すれば良いわけです。
/// <summary>
/// 指定したオブジェクトを強制的に別の型に変換します。
/// このメソッドはオブジェクトの型を破壊的に変換するため、予期しない動作を引き起こす可能性があります。
/// </summary>
/// <param name="target">対象オブジェクト。</param>
/// <param name="newType">新しい型。</param>
static unsafe void ConvertType(object target, Type newType)
{
// 型ハンドルを取得
IntPtr newTypeHandle = newType.TypeHandle.Value;
// 対象オブジェクトに対する GC ハンドルを生成
GCHandle targetGCHandle = GCHandle.Alloc(target, GCHandleType.Normal);
try
{
// GC ハンドルの実際のエントリへのポインタを取得
void* entryPointer = (void*)GCHandle.ToIntPtr(targetGCHandle);
// エントリにはオブジェクトへの参照が格納されている
void* targetPointer = *((void**)entryPointer);
// オブジェクトへのポインタは、オブジェクトヘッダーの型ハンドル部分を参照している
IntPtr* typeHandlePointer = (IntPtr*)targetPointer;
// 型ハンドルを書き換えることで型を強制的に変換する
*typeHandlePointer = newTypeHandle;
}
finally
{
// GC ハンドルの解放
targetGCHandle.Free();
}
}
これで、アンマネージポインタ取得してから型ハンドルの書き換えを行うまでの間に、ガベージコレクションによってオブジェクトが移動することはなくなったはずです。
次の検証用コードで検証した結果、問題ありませんでした。
static void Main(string[] args)
{
bool run = true;
Action action =
delegate
{
while (run)
{
object o = new object();
Console.WriteLine("collect.");
GC.Collect();
}
};
action.BeginInvoke(null, null);
while (true)
{
object o = new object();
ConvertType(o, typeof(void));
Console.WriteLine(o.GetType());
if (o.GetType() != typeof(void))
{
Console.WriteLine("failed.");
run = false;
break;
}
}
Console.ReadLine();
}
ちなみに、修正前のメソッドで検証してみても、問題は中々発生しません。簡単に発生させるためには、オブジェクトの型ハンドルを書き換えている箇所にブレークポイントを貼ってデバック実行します。ブレークと再開を何回か繰り返せば簡単に発生します。
もちろん、修正後のメソッドならこの方法で検証しても問題が発生しません。
通常、オブジェクトに結びついている型を変更することはできません。例えば String オブジェクトを Object クラスにキャストしても、オブジェクト自体は String オブジェクトのままです。
オブジェクトはメモリ上のデータです。このデータには、オブジェクトヘッダーと各フィールドの値が含まれます。オブジェクトヘッダーには、同期ブロックインデックスと型ハンドルが含まれます。
この型ハンドルが、オブジェクトと結びついている型を示します。型ハンドルは、ローダーヒープ (アプリケーションドメイン内にロードされた型情報を格納している領域) 内にある型情報 (Type クラスのインスタンスとは別物) へのポインタです。Object.GetType メソッドは、この型ハンドルを元に Type クラスのインスタンスを取得して返却しています。
つまり、オブジェクトヘッダー内の型ハンドルを書き換えると、オブジェクトに結びついている型を変換することができるわけです。
オブジェクトヘッダーへの直接のアクセスはサポートされていません。ただし、アンマネージドコードを用いれば、アンマネージポインタからメモリを直接参照することができますので、これでアクセスすることが可能です。ちなみに、オブジェクトの参照 (マネージポインタ) は、オブジェクトヘッダー内の型ハンドルのアドレスを示すようになっています。
下記のメソッドは、オブジェクトに結びついている型を変換します。(ビルドするにはアンセーフコードの許可が必要です。)
/// <summary>
/// 指定したオブジェクトを強制的に別の型に変換します。
/// このメソッドはオブジェクトの型を破壊的に変換するため、予期しない動作を引き起こす可能性があります。
/// </summary>
/// <param name="target">対象オブジェクト。</param>
/// <param name="newType">新しい型。</param>
static unsafe void ConvertType(object target, Type newType)
{
// 対象オブジェクトに対する GC ハンドルを生成
GCHandle targetGCHandle = GCHandle.Alloc(target, GCHandleType.Normal);
try
{
// GC ハンドルの実際のエントリへのポインタを取得
void* entryPointer = (void*)GCHandle.ToIntPtr(targetGCHandle);
// エントリにはオブジェクトへの参照が格納されている
void* targetPointer = *((void**)entryPointer);
// オブジェクトへのポインタは、オブジェクトヘッダーの型ハンドル部分を参照している
IntPtr* typeHandlePointer = (IntPtr*)targetPointer;
// 型ハンドルを書き換えることで型を強制的に変換する
*typeHandlePointer = newType.TypeHandle.Value;
}
finally
{
// GC ハンドルの解放
targetGCHandle.Free();
}
}
以下は使用例です。
static void Main(string[] args)
{
object obj = new object();
Console.WriteLine("obj is a {0}", obj.GetType());
ConvertType(obj, typeof(IDisposable));
Console.WriteLine("obj is a {0}", obj.GetType());
Console.ReadLine();
}
以下は実行結果です。
obj is a System.Object
obj is a System.IDisposable
このメソッドを使えば、オブジェクトに結びついている型をどんな型にでも変換できます。抽象クラスやインターフェイスに変換することもできます。ただし、その結果予期せぬ動作を引き起こす可能性があります。例えば、変換前の型より変換後の型の方がサイズが大きいと、他のオブジェクトを破壊してしまう場合があります。
また、このメソッドには一つ欠点があります。オブジェクトへのポインタを取得してから型ハンドルの書き換えを行うまでの間に、ガベージコレクションによってオブジェクトが移動されてしまうと、型変換が正常に行われず、全く無関係なデータを型ハンドルで上書きしてしまう可能性があります。
Blittable 型と呼ばれる型に関しては、GC 時にオブジェクトが移動してしまわないよう固定化することができますが、非 Blittable 型では固定化ができません。
この問題を解決する方法は不明 こちら です。
言うまでもないと思いますが、これはとても危険でトリッキーなメソッドです。普通はまず使うことのないメソッドです。
このメソッドで何か面白いことができたら、是非教えてください。
でも実は、コンストラクタを実行せずにインスタンスを生成することができます。
方法はとても簡単です。
生成したいインスタンスの型情報を渡して FormatterServices クラス (System.Runtime.Serialization) の GetUninitializedObject メソッド を呼び出すだけです。
このクラスは、通常シリアル化で使用されるクラスですが、インスタンス化したいクラスに SerializableAttribute クラス (System) が付加されている必要はありません。
それと面白いことに、Void 構造体 (System) をインスタンス化することができます。 (Activator じゃできないのに)
ただし、String クラス (System)、 ContextBoundObject クラス (System) はこの方法でインスタンス化することができません。
下記のコードでは、コンストラクタの処理中に例外をスローするクラスのインスタンスを生成できることが確認できます。
using System;
using System.Runtime.Serialization;
class Program
{
static void Main()
{
Hoge h = (Hoge)FormatterServices.GetUninitializedObject(typeof(Hoge));
}
}
public class Hoge
{
public Hoge()
{
throw new NotSupportedException();
}
}
で、配列が定義される際、どうも 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 命令が使用されるため、パフォーマンスが向上するようです。
Visual Basic でなんとなくイベントとデリゲート その3。 - イベントに関連することをちょっとだけ追記。 -
とりこびとさんの記事を読んでいて、そういえば VB のイベント定義って C# と違うんだよなぁと思い出したことがきっかけで、久々に VB のコードを書いてみました。そしたら、ちょっとした問題に気づきました。
とりあえず、C# でのイベント定義から。
C# でイベントを定義する場合、通常は以下のように記述します。
public class Class1
{
private int _field1;
public int Property1
{
get
{
return this._field1;
}
set
{
if ((this._field1 != value) && (this.Event1 != null))
{
this.Event1(this, EventArgs.Empty);
}
this._field1 = value;
}
}
public event EventHandler Event1;
}
この書き方は、実はイベント定義のシンタックスシュガーであり、以下のように add アクセサと remove アクセサを自分で実装することも可能です。
public class Class1
{
private int _field1;
public int Property1
{
get
{
return this._field1;
}
set
{
if ((this._field1 != value) && (this._event1 != null))
{
this._event1(this, EventArgs.Empty);
}
this._field1 = value;
}
}
private EventHandler _event1;
public event Eventhandler Event1
{
add
{
this._event1 += value;
}
remove
{
this._event1 -= value;
}
}
}
Visual Basic の場合、以下のように書くようです。
Public Class Class1
Private _field1 As Integer
Public Property Property1() As Integer
Get
Return Me._field1
End Get
Set(ByVal value As Integer)
If (Not Me._field1 = value) Then
RaiseEvent Event1(Me, EventArgs.Empty)
End If
Me._field1 = value
End Set
End Property
Private _event1 As EventHandler
Public Custom Event Event1 As EventHandler
AddHandler(ByVal value As EventHandler)
Me._event1 = CType(System.Delegate.Combine(Me._event1, value), EventHandler)
End AddHandler
RemoveHandler(ByVal value As EventHandler)
Me._event1 = CType(System.Delegate.Remove(Me._event1, value), EventHandler)
End RemoveHandler
RaiseEvent(ByVal sender As Object, ByVal e As EventArgs)
If (Not Me._event1 Is Nothing) Then
Me._event1(sender, e)
End If
End RaiseEvent
End Event
End Class
C# では add アクセサと remove アクセサの2つを定義すれば良かったのですが、VB の場合、AddHandler アクセサと RemoveHandler アクセサの他に、RaiseEvent アクセサというものも定義しなければなりません。
アクセサを自前で実装せずにイベントを定義した場合は、コンパイルされた MSIL に RaiseEvent アクセサに相当するコード ( MSIL では、RaiseEvent アクセサは fire アクセサとなる模様 ) は出力されません。しかし、自前で実装する場合はなぜか必須です。
なぜ必須とするのか、考えてみました。
VB ではイベントを発生させるのに RaiseEvent ステートメントを使用します。もし、自前で実装したイベントに RaiseEvent アクセサがなかった場合、RaiseEvent ステートメントでのイベント発生ができなくなります。代わりに C# のようにデリゲートの Invoke メソッドを呼び出せばいいだけの話なのですが、それを嫌ったのかなぁと。
で、これ実は結構問題なんじゃないかと思うんです。イベントのアクセサを自前で実装するのって、他のオブジェクトのイベントへ委譲する場合が主だと思うんです。 ( 僕はこれ以外の理由でわざわざ自前実装したことがありません。 )
例えば、ユーザーコントロールを開発するとします。このユーザーコントロールには Enter ボタンを配置します。
ユーザーコントロールの外部からは Enter ボタンにアクセスできないようにしたい、でも Enter ボタンのクリックイベントは外部に公開したい。そんな時、ユーザーコントロールに EnterClick イベントを定義してイベントのアクセサを自前で実装するわけです。
AddHandler アクセサは Enter ボタンのクリックイベントにハンドラを登録します。
RemoveHandler アクセサは Enter ボタンのクリックイベントからハンドラを削除します。
では、RaiseEvent アクセサは?
ボタンのクリックイベントの公開が目的なのだから、ユーザーコントロール内のコードでイベント発生なんてさせませんし、できません。でも RaiseEvent アクセサを定義しないといけません。恐らく、何の処理もしない、あるいは例外を発生させるという実装になるかと思います。そして、ユーザーコントロール内のコードに
RaiseEvent EnterClick(Me, EventArgs.Empty)
って書くことは許されてしまうわけです。これをコンパイル時に検出することはできません。
ちなみに、RaiseEvent アクセサに相当するものは C# にはありません。
個人的には、あってもなくてもどっちでもいいって感じですが、これを必ず定義しなきゃいけないってのはよろしくないよなぁと思いました。
[ 参考 ]
Event ステートメント ( Visual Basic )
それぞれ違うユーザー情報を返してきた時は驚いた。
・WindowsIdentity クラス (System.Security.Principal) の GetCurrent メソッド
現在実行中の Win32 スレッドのセキュリティ コンテキストの ID 情報を返します。
・Thread クラス (System.Threading) の CurrentPrincipal プロパティ
Win32 スレッドの上位で、現在実行されている .NET スレッドのプリンシパルを返します。
なるほど、型の違いと、Thread.CurrentPrincipal だけが設定可能なのはこういう理由か。
[ 参考 ]
ASP.NET ID マトリックス
.NET Framework のユーザー操作の基礎
英語版の SDK を既にインストールしてある場合は、アンインストールしてからのインストールとなります。 ( 詳しくは参考リンク先を参照してください。 )
ダウンロードの詳細 : Microsoft Windows SDK for Windows Vista
これでやっと、WCF の日本語ドキュメントが読めます^^
[ 参考 ]
Software Cafe : Windows SDK for Windows Vista and .NET Framework 3.0 日本語版
[ 関連記事 ]
C#と諸々 - .NET Framework 3.0 正式版 リリース
Windows SDK for Windows Vista を既にインストールしてある場合は、先にアンインストールする必要があります。
Download details: Windows SDK Update for Windows Vista
[ 過去の関連記事 ]
C#と諸々 .NET Framework 3.0 正式版 リリース



