returnステートメントはロックの内部または外部のどちらにあるべきですか?


142

コードのどこかに、returnステートメントがロックの内側と外側のどこかにあることに気づきました。どれが一番いいですか?

1)

void example()
{
    lock (mutex)
    {
    //...
    }
    return myData;
}

2)

void example()
{
    lock (mutex)
    {
    //...
    return myData;
    }

}

どちらを使用すればよいですか?


Reflectorを起動してILの比較を行ってみませんか;-)。
Pop Catalin、

6
@ポップ:完了-ILのどちらも優れていません-C#スタイルのみが適用されます
マークグラベル

1
非常に興味深い、すごい今日私は何かを学びます!
Pokus 2008年

回答:


192

基本的に、どちらを使用してもコードが単純になります。出口の単一点は理想的ですが、それを実現するためだけにコードを変形させたりはしません...そして、代替手段がローカル変数(ロック外)を宣言している場合は、初期化(ロック内)およびそれを(ロックの外で)返すと、ロック内の単純な "return foo"の方がはるかに簡単です。

ILの違いを示すために、コードを記述します。

static class Program
{
    static void Main() { }

    static readonly object sync = new object();

    static int GetValue() { return 5; }

    static int ReturnInside()
    {
        lock (sync)
        {
            return GetValue();
        }
    }

    static int ReturnOutside()
    {
        int val;
        lock (sync)
        {
            val = GetValue();
        }
        return val;
    }
}

(私は喜んでそれReturnInsideがC#のより単純でよりクリーンなビットであると主張することに注意してください)

そして、IL(リリースモードなど)を見てください。

.method private hidebysig static int32 ReturnInside() cil managed
{
    .maxstack 2
    .locals init (
        [0] int32 CS$1$0000,
        [1] object CS$2$0001)
    L_0000: ldsfld object Program::sync
    L_0005: dup 
    L_0006: stloc.1 
    L_0007: call void [mscorlib]System.Threading.Monitor::Enter(object)
    L_000c: call int32 Program::GetValue()
    L_0011: stloc.0 
    L_0012: leave.s L_001b
    L_0014: ldloc.1 
    L_0015: call void [mscorlib]System.Threading.Monitor::Exit(object)
    L_001a: endfinally 
    L_001b: ldloc.0 
    L_001c: ret 
    .try L_000c to L_0014 finally handler L_0014 to L_001b
} 

method private hidebysig static int32 ReturnOutside() cil managed
{
    .maxstack 2
    .locals init (
        [0] int32 val,
        [1] object CS$2$0000)
    L_0000: ldsfld object Program::sync
    L_0005: dup 
    L_0006: stloc.1 
    L_0007: call void [mscorlib]System.Threading.Monitor::Enter(object)
    L_000c: call int32 Program::GetValue()
    L_0011: stloc.0 
    L_0012: leave.s L_001b
    L_0014: ldloc.1 
    L_0015: call void [mscorlib]System.Threading.Monitor::Exit(object)
    L_001a: endfinally 
    L_001b: ldloc.0 
    L_001c: ret 
    .try L_000c to L_0014 finally handler L_0014 to L_001b
}

ILレベルでは、それらは[与えられるか、いくつかの名前を付ける]同一です(私は何かを学びました;-p)。そのため、唯一の賢明な比較は、ローカルのコーディングスタイルの(非常に主観的な)法則です...私ReturnInsideは単純さのために好んでいますが、どちらにもワクワクしません。


15
私は(無料で優れた)Red Gateの.NETリフレクター(以前はLutz Roederの.NETリフレクター)を使用しましたが、ILDASMも使用します。
マークグラベル

1
Reflectorの最も強力な側面の1つは、ILを好みの言語(C#、VB、Delphi、MC ++、Chromeなど)に実際に逆アセンブルできることです
Marc Gravell

3
簡単な例では、ILは同じままですが、おそらく定数値しか返さないためでしょうか。実際のシナリオでは結果が異なる可能性があり、並列スレッドは、returnステートメントがロックブロックの外に返される前に値を変更することにより、相互に問題を引き起こす可能性があると考えています。危険な!
Torbjørn

@MarcGravell:同じことを考えているときにあなたの投稿に出くわしましたが、あなたの答えを読んだ後でも、次のことについてはまだわかりません:外部のアプローチを使用するとスレッドセーフなロジックが壊れる可能性のある状況はありますか?私は単一のリターンポイントを好み、そのスレッドセーフ性について「気分が良くない」ので、これを尋ねます。ILが同じである場合でも、私の懸念はとにかく議論の余地がないはずです。
Raheel Khan、2012

1
@RaheelKhanいいえ、なし。彼らは同じです。ILレベルでは、リージョン内にいることはできませんret.try
Marc Gravell

42

違いはありません。どちらもコンパイラによって同じものに変換されます。

明確にするために、どちらかは次のセマンティクスで何かに効果的に変換されます:

T myData;
Monitor.Enter(mutex)
try
{
    myData= // something
}
finally
{
    Monitor.Exit(mutex);
}

return myData;

1
最後に試し/の真実であること、まあ- ...と、より多くのコードを取る-しかし、ロック外の復帰はまだ離れて最適化することができない余分な地元の人々が必要です
マルクGravell

3
tryブロックから戻ることはできません。「.leave」オペコードで終わる必要があります。したがって、どちらの場合も、発行されるCILは同じになるはずです。
グレッグビーチ、

3
あなたは正しいです-私はILを見ました(更新された投稿を参照)。私は何かを学びました;-p
Marc Gravell

2
クール、残念なことに、tryブロックで.retオペコードを発行しようとする苦痛な時間と、CLRに動的メソッドの読み込みを拒否させることから学んだ:-(
Greg Beech

私は関連付けることができます; Reflection.Emitをかなり実行しましたが、怠惰です。何かについて非常に確信がない限り、C#で代表的なコードを記述してから、ILを調べます。しかし、ILの観点から考え始める(つまり、スタックの順序付け)のがどれほど早く始まるかは驚くべきことです。
マークグラベル

28

私は間違いなくロックの中に戻りを入れます。そうしないと、別のスレッドがロックに入り、returnステートメントの前に変数が変更される危険性があるため、元の呼び出し元が予想とは異なる値を受け取るようになります。


4
これは正しい、他のレスポンダーが欠落しているように見える点です。彼らが作成した単純なサンプルは同じILを生成する可能性がありますが、これはほとんどの実際のシナリオではそうではありません。
トルビョルン

4
私は他の答えがこれについて話してはいけないことに驚いています
Akshat Agarwal

5
このサンプルでは、​​スタック変数を使用して戻り値、つまりロック外のreturnステートメントと変数宣言のみを格納することについて話し合っています。別のスレッドには別のスタックが必要なので、害を及ぼすことはできませんでしたか?
Guillermo Ruffino 2016

3
別のスレッドがreturn呼び出しとメインスレッドの変数への戻り値の実際の割り当てとの間の値を更新する可能性があるため、これは有効なポイントではないと思います。返される値を変更したり、現在の実際の値との一貫性を保証したりすることはできません。正しい?
ウロスJoksimović

この答えは間違っています。別のスレッドはローカル変数を変更できません。ローカル変数はスタックに格納され、各スレッドには独自のスタックがあります。ところで、スレッドのスタックのデフォルトサイズは1 MBです。
Theodor Zoulias

5

場合によります、

ここで穀物に逆らうつもりです。私は通常、ロックの内側に戻ります。

通常、変数mydataはローカル変数です。初期化中にローカル変数を宣言するのが好きです。ロックの外で戻り値を初期化するデータがほとんどありません。

だからあなたの比較は実際に欠陥があります。理想的には、2つのオプションの違いは、あなたが書いたとおりであり、ケース1にうなずくようですが、実際には少し醜いです。

void example() { 
    int myData;
    lock (foo) { 
        myData = ...;
    }
    return myData
}

void example() { 
    lock (foo) {
        return ...;
    }
}

特に短いスニペットの場合、ケース2はかなり読みやすく、失敗しにくいと思います。


4

外のロックの方が見栄えが良いと思うが、コードを次のように変更する場合は注意してください。

return f(...)

ロックを保持したままf()を呼び出す必要がある場合は、ロック内に置く必要があります。一貫性を保つためにロック内に戻り値を保持することには意味があります。


1

価値があることについては、MSDNドキュメントにロックの内側から戻る例が含まれています。ここの他の回答から、それはかなり似たILのように見えますが、私にとっては、別のスレッドによって戻り変数が上書きされるリスクを負わないため、ロックの内側から戻る方が安全に思えます。



0

lock() return <expression> ステートメントは常に:

1)ロックを入力してください

2)指定されたタイプの値のローカル(スレッドセーフ)ストアを作成します。

3)によって返された値をストアに入力します<expression>

4)出口ロック

5)店舗に返品してください。

つまり、lockステートメントから返された値は、返される前に常に「調理」されます。

心配しないでくださいlock() return、ここで誰かに耳を傾けないでください))


-2

注:この回答は事実として正しいと信じており、それが役に立てば幸いですが、具体的なフィードバックに基づいて改善できることを常に嬉しく思います。

既存の回答を要約して補足するには:

  • 受け入れられた回答にかかわらず、あなたの中で選択した構文形式の、ということを示してC#の -ため、実行時に- ILコード内のコード、returnまでは発生しません後にロックが解除されます。

    • したがって、厳密に言えば、ブロックのreturn 内側lockブロックを配置すると、制御の流れが誤って表示されます[1]が、戻り値をauxに格納する必要がなくなるため、構文的には便利です。ローカル変数(ブロックの外側で宣言できるため、ブロックの外側で使用できますreturn-Edward KMETTの回答を参照してください。
  • 個別に-そしてこの側面は質問に付随的ですが、それでも興味があるかもしれません(Ricardo Villamilの答えはそれを解決しようとしますが、間違っていると思います)- lockステートメントをステートメントと組み合わせるreturn-つまり、returnから保護されたブロックで同時アクセス- 一度取得したものを実際に保護する必要がない場合にのみ、呼び出し側のスコープの戻り値を意味のある方法で「保護」します。これは、次のシナリオに適用されます。

    • 戻り値がコレクションからの要素であり、要素自体の変更や要素の変更ではなく、要素の追加と削除に関してのみ保護が必要な場合

    • ...返される値が値型または文字列のインスタンスである場合

      • この場合、呼び出し元は値のスナップショット(コピー)[2]を受け取ることに注意してください。これは、呼び出し元が検査するときには、それがoriginのデータ構造の現在の値ではなくなっている可能性があります。
    • それ以外の場合は、ロックはメソッド内で(単に)ではなく、呼び出し元が実行する必要があります。


[1] テオドールZouliasはそれが配置するためにも技術的に真であることを指摘するreturn内部をtrycatchusingifwhilefor文、...。ただし、lockこの質問が尋ねられ、多くの注目を集めたことからも明らかなように、真の制御フローの精査を招く可能性が高いのは、ステートメントの特定の目的です。

[2]値型のインスタンスにアクセスすると、常にスレッドローカルのスタック上のコピーが作成されます。文字列は技術的には参照型のインスタンスですが、値のような型のインスタンスとして効果的に動作します。


回答の現在の状態(リビジョン13)については、が存在する理由について推測しlock、returnステートメントの配置から意味を導き出します。これは私見のこの質問とは無関係の議論です。また、「不当表示」の使用はかなり気になるものです。戻った場合lock詐称制御の流れを、同じから戻すため言えるtrycatchusingifwhilefor、および言語の任意の他の構築物。これは、C#が制御フローの不正確な表現に悩まされていると言っているようなものです。イエス...
22:01にテオドール・

「C#は制御フローの不実表示に悩まされていると言っているようなものです」-ええと、それは技術的には真実です。とtryif私は個人的にはそれについて考えることすらしませんがlock、特に、私にとって質問が生じました-そしてそれが他の人にも起こらなければ、この質問は決して尋ねられなかっただろう、そして受け入れられた答えは、真の振る舞いを調査するためにそれほど長くは及ばなかっただろう。
mklement0
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.