コードを書いていてよく使用するオブジェクト初期化子の話です。
整理できるので便利ですが、注意して使わないと痛い目に遭います。
こんなコードを書きました。
// メイン処理
try
{
Debug.WriteLine( "Try" );
using ( new MyClass( ) { Member = ThrowableMethod( ) } );
}
catch
{
Debug.WriteLine( "Catch" );
}
// ThrowableMethod
static int ThrowableMethod( ) { throw new InvalidOperationException( ); }
// MyClass
class MyClass : IDisposable
{
public int Member { get; set; }
public MyClass( ) { Debug.WriteLine( "Constructor" ); }
public void Dispose( ) { Debug.WriteLine( "Dispose" ); }
}
MyClass インスタンスを、オブジェクト初期化子を使用して初期化しています。
これを実行すると、こうなります。
Try
Constructor
'Example.vshost.exe' (マネージ (v4.0.30319)): 'C:\...\mscorlib.resources.dll' が読み込まれました
'System.InvalidOperationException' の初回例外が Example.exe で発生しました。
Catch
ヾ('ω')ノ゛ ('ω')えっ
using を指定し、コンストラクタを最後まで処理したにもかかわらず、
MyClass オブジェクトが解放されませんでした。
こんな動作になるわけは、オブジェクト初期化子を使用するしないの違いを
ILで比較してみるとわかります。
// MethodA
static void MethodA( )
{
var value = new MyClass( );
value.Member = ThrowableMethod( );
}
// MethodB
static void MethodB( )
{
var value = new MyClass( ) { Member = ThrowableMethod( ) };
}
MethodA ではオブジェクト初期化子を使用せず、
MethodB ではオブジェクト初期化子を使用しています。
ILにすると、こうなります。
// MethodA
.method private hidebysig static
void MethodA () cil managed
{
.maxstack 2
.locals init (
[0] class Example.Program/MyClass 'value'
)
IL_0000: nop
IL_0001: newobj instance void Example.Program/MyClass::.ctor()
IL_0006: stloc.0 // ここで value にセットする
IL_0007: ldloc.0
IL_0008: call int32 Example.Program::ThrowableMethod()
IL_000d: callvirt instance void Example.Program/MyClass::set_Member(int32)
IL_0012: nop
IL_0013: ret
}
// MethodB
.method private hidebysig static
void MethodB () cil managed
{
.maxstack 2
.locals init (
[0] class Example.Program/MyClass 'value',
[1] class Example.Program/MyClass '<>g__initLocal1'
)
IL_0000: nop
IL_0001: newobj instance void Example.Program/MyClass::.ctor()
IL_0006: stloc.1
IL_0007: ldloc.1
IL_0008: call int32 Example.Program::ThrowableMethod()
IL_000d: callvirt instance void Example.Program/MyClass::set_Member(int32)
IL_0012: nop
IL_0013: ldloc.1
IL_0014: stloc.0 // ここで value にセットする
IL_0015: ret
}
MethodA と MethodB は同じ処理 と 思いきや 全然違いました。
Member プロパティの set である set_Member を実行するタイミングが、
MethodA では ”参照を value に代入した後” になっていますが、
MethodB では ”参照を value に代入する前” になっています。
要するに最初のコードは、using 変数が初期化されない(null)まま例外が発生し、
using 変数の Dispose 呼び出しがスキップされたという事のようです。
オブジェクト初期化子の中で例外が一度でも発生すると途端にどうしようも無くなるので、
指定できるだけ全て使うのではなく、安全なメンバに限って使った方が良さそうです。
IDisposable
な型ではオブジェクト初期化子を使用しないようにします。
// メイン処理
while ( true )
{
try
{
using ( new FileStream( "FILE", FileMode.Create ) { Position = -1 } );
}
catch ( Exception ex )
{
Console.WriteLine( ex.Message );
}
Console.ReadKey( );
}
これも実行すると最初は ArgumentOutOfRangeException ですが、
2回目からは IOException になります。