別の質問でこのヒントを見たのですが、これが一体どのように機能するのかを誰かが説明してくれるかどうか疑問に思っていました。
try { return x; } finally { x = null; }
つまり、ステートメントの後finally
に句が本当に実行されるのでしょうか? このコードはどの程度スレッドセーフでないのでしょうか? このハックに関して実行できる追加のハッキングを考えられますか?return
try-finally
別の質問でこのヒントを見たのですが、これが一体どのように機能するのかを誰かが説明してくれるかどうか疑問に思っていました。
try { return x; } finally { x = null; }
つまり、ステートメントの後finally
に句が本当に実行されるのでしょうか? このコードはどの程度スレッドセーフでないのでしょうか? このハックに関して実行できる追加のハッキングを考えられますか?return
try-finally
finally ステートメントが実行されますが、戻り値は影響を受けません。実行順序は次のとおりです。
デモ用の短いプログラムを次に示します。
using System;
class Test
{
static string x;
static void Main()
{
Console.WriteLine(Method());
Console.WriteLine(x);
}
static string Method()
{
try
{
x = "try";
return x;
}
finally
{
x = "finally";
}
}
}
これは "try" (それが返されるため) を出力し、次に "finally" を出力します。これが x の新しい値だからです。
もちろん、変更可能なオブジェクト (StringBuilder など) への参照を返す場合、finally ブロック内のオブジェクトに加えられた変更は戻り時に表示されます - これは戻り値自体には影響しません (これは単なる参照)。
いいえ - IL レベルでは、例外処理ブロック内から戻ることはできません。基本的に変数に格納し、後で返します
つまり、次のようになります。
int tmp;
try {
tmp = ...
} finally {
...
}
return tmp;
例(リフレクターを使用):
static int Test() {
try {
return SomeNumber();
} finally {
Foo();
}
}
コンパイルすると:
.method private hidebysig static int32 Test() cil managed
{
.maxstack 1
.locals init (
[0] int32 CS$1$0000)
L_0000: call int32 Program::SomeNumber()
L_0005: stloc.0
L_0006: leave.s L_000e
L_0008: call void Program::Foo()
L_000d: endfinally
L_000e: ldloc.0
L_000f: ret
.try L_0000 to L_0008 finally handler L_0008 to L_000e
}
これは基本的に、ローカル変数 ( CS$1$0000
) を宣言し、値を変数 (処理されたブロック内) に配置し、ブロックを終了した後、変数をロードしてから返します。Reflector はこれを次のようにレンダリングします。
private static int Test()
{
int CS$1$0000;
try
{
CS$1$0000 = SomeNumber();
}
finally
{
Foo();
}
return CS$1$0000;
}
finally 句は、return ステートメントの後、関数から実際に戻る前に実行されます。スレッドセーフとはほとんど関係がないと思います。これはハックではありません。try ブロックまたは catch ブロックで何を行っても、finally は常に実行されることが保証されています。
Marc Gravell と Jon Skeet による回答に加えて、返されたときにオブジェクトやその他の参照型が同様に動作するが、いくつかの違いがあることに注意することが重要です。
返される "What" は、単純型と同じロジックに従います。
class Test {
public static Exception AnException() {
Exception ex = new Exception("Me");
try {
return ex;
} finally {
// Reference unchanged, Local variable changed
ex = new Exception("Not Me");
}
}
}
返される参照は、最終ブロックでローカル変数に新しい参照が割り当てられる前に、既に評価されています。
実行は基本的に次のとおりです。
class Test {
public static Exception AnException() {
Exception ex = new Exception("Me");
Exception CS$1$0000 = null;
try {
CS$1$0000 = ex;
} finally {
// Reference unchanged, Local variable changed
ex = new Exception("Not Me");
}
return CS$1$0000;
}
}
違いは、オブジェクトのプロパティ/メソッドを使用して変更可能な型を変更することが可能であり、注意しないと予期しない動作が発生する可能性があることです。
class Test2 {
public static System.IO.MemoryStream BadStream(byte[] buffer) {
System.IO.MemoryStream ms = new System.IO.MemoryStream(buffer);
try {
return ms;
} finally {
// Reference unchanged, Referenced Object changed
ms.Dispose();
}
}
}
try-return-finally について考慮すべき 2 番目のことは、「参照によって」渡されたパラメーターは、返された後でも変更できるということです。戻り値のみが評価され、返されるのを待っている一時変数に格納されます。他の変数は通常の方法で変更されます。このように、out パラメータの契約は、finally ブロックまで満たされないことさえあります。
class ByRefTests {
public static int One(out int i) {
try {
i = 1;
return i;
} finally {
// Return value unchanged, Store new value referenced variable
i = 1000;
}
}
public static int Two(ref int i) {
try {
i = 2;
return i;
} finally {
// Return value unchanged, Store new value referenced variable
i = 2000;
}
}
public static int Three(out int i) {
try {
return 3;
} finally {
// This is not a compile error!
// Return value unchanged, Store new value referenced variable
i = 3000;
}
}
}
他のフロー コンストラクトと同様に、「try-return-finally」には適切な場所があり、実際にコンパイルされる構造を記述するよりもクリーンなコードを作成できます。ただし、落とし穴を避けるために慎重に使用する必要があります。
がローカル変数である場合、メソッドが終了し、戻り値の値が null ではない場合に効果的に null に設定されるためx
、ポイントがわかりません(set の呼び出しの前にレジスタに配置されたため) nullに)。x
x
返されたとき (および戻り値が決定された後) にフィールドの値の変更を保証したい場合にのみ、これが行われるのを見ることができます。