例外の安全性のために「スコープ付きの動作」を取得する手段としてIDisposableおよび「using」を使用することは悪用ですか?


112

私がC ++でよく使用したのは、コンストラクターとデストラクターを使用して、AクラスBに別のクラスの状態の開始条件と終了条件を処理させ、Aそのスコープ内の何かが例外をスローした場合にBが既知の状態になるようにすることでした。スコープが終了しました。これは頭​​字語に関する限り、純粋なRAIIではありませんが、それでも確立されたパターンです。

C#では、よくやりたい

class FrobbleManager
{
    ...

    private void FiddleTheFrobble()
    {
        this.Frobble.Unlock();
        Foo();                  // Can throw
        this.Frobble.Fiddle();  // Can throw
        Bar();                  // Can throw
        this.Frobble.Lock();
    }
}

このようにする必要があります

private void FiddleTheFrobble()
{
    this.Frobble.Unlock();

    try
    {            
        Foo();                  // Can throw
        this.Frobble.Fiddle();  // Can throw
        Bar();                  // Can throw
    }
    finally
    {
        this.Frobble.Lock();
    }
}

返品Frobble時の状態を保証したい場合FiddleTheFrobble。コードはより良いでしょう

private void FiddleTheFrobble()
{
    using (var janitor = new FrobbleJanitor(this.Frobble))
    {            
        Foo();                  // Can throw
        this.Frobble.Fiddle();  // Can throw
        Bar();                  // Can throw
    }
}

どこFrobbleJanitorルックスおおよそのような

class FrobbleJanitor : IDisposable
{
    private Frobble frobble;

    public FrobbleJanitor(Frobble frobble)
    {
        this.frobble = frobble;
        this.frobble.Unlock();
    }

    public void Dispose()
    {
        this.frobble.Lock();
    }
}

それが私がやりたいことです。私はので、今の現実は、追いつくたい使用する必要があることFrobbleJanitorに使用され using。私はこれをコードレビューの問題と考えることができましたが、何かが私を悩ませています。

質問:上記はusingandの乱用と見なされIDisposableますか?


23
クラス名とメソッド名だけの場合は+1 :-)。まあ、公平にするために、そして実際の質問。
Joey

2
@ヨハネス:ええ、私はその理由を見ることができます-ほとんどの人は自分のフィドルを揺さぶるだけですが、私はそれに抗議しなければなりません。;-)ちなみに、あなたの名前の類似性のために+1。
Johann Gerell、2010年

おそらく、この例では、lock(this){..}スコープを使用して同じ結果を得ることができますか?
oɔɯǝɹ

1
@oɔɯǝɹ:lock()を使用して、異なるスレッドから何かへの同時アクセスを防止します。私が説明するシナリオとは何の関係もありません。
Johann Gerell、2010年

1
C#の次のバージョンでファイナライザなしのIScopeable :)
Jon Raynor

回答:


33

必ずしもそうは思いません。IDisposableは、技術的に非管理対象リソースを持つものに使用することを目的としていますが、usingディレクティブは、の一般的なパターンを実装するためのきちんとした方法ですtry .. finally { dispose }

純粋主義者は「はい-それは虐待的です」と主張し、純粋主義的な意味ではそうです。しかし、私たちのほとんどは、純粋主義的な観点からではなく、半芸術的な観点からコーディングを行っています。私の意見では、このように「using」コンストラクトを使用することは、非常に芸術的です。

おそらく、IDisposableの上に別のインターフェイスを貼り付けて、少し離れて、そのインターフェイスがIDisposableを意味する理由を他の開発者に説明する必要があります。

これを実行する方法は他にもたくさんありますが、結局のところ、これほど優れた方法は考えられません。


2
「使う」という芸術的な使い方だと思います。Eric GunnersonからのコメントにリンクしているSamual Jackの投稿は、この「使用」の実装を検証していると思います。これに関して、AndrasとEricの両方の優れた点を見ることができます。
IAbstract 2010

1
私はこのことについてエリックガンナーソンと少し前に話しました。彼によると、C#設計チームの意図は、を使用してスコープを表すことでした。実際、ロックステートメントの前に使用するように設計されていれば、ロックステートメントさえ存在しない可能性があると彼は仮定しました。それは、Monitor呼び出しなどを使用したusingブロックでした。更新:次の答えは、言語チームにも所属しているEric Lippertによるものです。;)C#チーム自体がこれについて完全に合意していないのでしょうか?Gunnersonとの私のディスカッションのコンテキストは、TimedLockクラスでした:bit.ly/lKugIO
ハッキング

2
@Haacked-面白いですよね。あなたのコメントは私の主張を完全に証明しています。あなたはチームの1人の人に話しかけたが、彼はそのためにすべてだった。次に、同じチームの以下のエリック・リッペルトを指摘します。私の視点から; C#は、インターフェイスを悪用するキーワードを提供し、非常に多くのシナリオで機能する見栄えの良いコードパターンを生成することもあります。それを乱用することになっているのでなければ、C#はそれを強制する別の方法を見つけるはずです。いずれにせよ、おそらくデザイナーはここでアヒルを一列に並べる必要があります!それまでの間、using(Html.BeginForm())サー?よろしくお願いします!:)
Andras Zoltan

71

これは、usingステートメントの乱用だと思います。私はこの立場で少数派であることを知っています。

私はこれを3つの理由で虐待だと考えています。

1つ目は、「using」はリソースの使用に使用され、使い終わったらそれ破棄することを期待しているためです。プログラムの状態を変更してもリソース使用され、リソースを元に戻しても何も処理されません。したがって、状態を変更および復元するために「使用」することは不正行為です。コードはカジュアルな読者を誤解させます。

第二に、私は「使用する」ことは礼儀正しからではなく、必要ではないことを期待しているからです。使い終わったときに「を使用して」ファイルを破棄する理由は、ファイルを破棄する必要あるからではなく、礼儀正しいためです。誰かがそのファイルの使用を待っている可能性があるため、「完了」と言います今」は道徳的に正しいことです。私は、「使用」をリファクタリングして、使用されたリソースをより長く保持し、後で破棄できるようにすべきであり、そうすることの唯一の影響は、他のプロセスわずかに不便をかけることですプログラムの状態に意味的な影響を与える「using」ブロック 必要とせずに、便利で丁寧にあるように見える構造にプログラム状態の重要で必要な変更を隠すため、乱用されます。

そして3番目に、プログラムのアクションはその状態によって決定されます。状態を注意深く操作する必要があるため、そもそもこの会話をしているのです。元のプログラムを分析する方法を考えてみましょう。

これを私のオフィスのコードレビューに持ち込んだ場合、私が最初に尋ねる質問は、「例外がスローされた場合に、フロブルをロックすることは本当に正しいのか?」です。何が起こっても、これが積極的にフロブルを再ロックすることは、プログラムから明らかに明白です。そうですか? 例外がスローされました。プログラムは不明な状態です。Foo、Fiddle、またはBarがスローしたかどうか、なぜスローしたか、クリーンアップされなかった他の状態に対してどのミューテーションを実行したかはわかりません。その恐ろしい状況では、再ロックすることは常に正しいことだ私に納得させることができますか?

たぶんそうかもしれませんし、そうでないかもしれません。私の要点は、コードが最初に書かれたとおりの場合、コードレビューアは質問をすることを知っているということです。"using"を使用するコードでは、質問をするかどうかわかりません。私はそれが行われたとき、それのブロックは、リソースを割り当て、ビットのためにそれを使用し、丁寧に処分「を使用して」と仮定し、いないよう閉じ括弧ブロック「を使用する」の際に任意の数の例外cirumstanceに変異する私のプログラムの状態をプログラム状態の整合性条件に違反しています。

「using」ブロックを使用してセマンティック効果を持たせると、このプログラムが断片化されます。

}

非常に意味があります。その単一の閉じ波括弧を見るとき、「その波括弧には私のプログラムのグローバルな状態に大きな影響を与える副作用がある」とすぐには思いません。しかし、このように「使用」を乱用すると、突然使用されます。

元のコードを見た場合に2番目に尋ねるのは、「Unlockの後で、tryに入る前に例外がスローされた場合はどうなるのか」ということです。最適化されていないアセンブリを実行している場合、コンパイラーは試行の前にno-op命令を挿入した可能性があり、no-opでスレッドアボート例外が発生する可能性があります。これはまれですが、実際には、特にWebサーバーで発生します。その場合、ロック解除は発生しますが、試行の前に例外がスローされたため、ロックは発生しません。このコードはこの問題に対して脆弱である可能性があり、実際に作成する必要があります

bool needsLock = false;
try
{
    // must be carefully written so that needsLock is set
    // if and only if the unlock happened:

    this.Frobble.AtomicUnlock(ref needsLock);
    blah blah blah
}
finally
{
    if (needsLock) this.Frobble.Lock();
}

繰り返しますが、そうかもしれませんし、そうでないかもしれませんが、私は質問することを知っています。"using"バージョンでは、同じ問題の影響を受けやすくなります。Frobbleがロックされた後、usingに関連付けられたtry保護領域に入る前に、スレッドアボート例外がスローされる可能性があります。しかし、「使用中」バージョンでは、これは「だから何だ」と思います。状況。それが起こった場合は残念ですが、「使用する」ことは、非常に重要なプログラムの状態を変更するのではなく、丁寧にするためだけにあると思います。ひどいスレッドアボート例外がまったく間違ったタイミングで発生した場合、ガベージコレクターはファイナライザーを実行して最終的にそのリソースをクリーンアップすると思います。


18
私は別の方法で質問に答えますが、それは十分に議論された投稿だと思います。ただし、using正確性の問題ではなく、礼儀正しさというあなたの主張には問題があります。リソースリークは正確性の問題であると私は思いますが、優先度の高いバグではないほど小さい場合も少なくありません。したがって、usingブロックはすでにプログラムの状態に意味的な影響を与えおり、ブロックによって防止されるのは、他のプロセスに対する「不便」だけ​​でなく、環境の破壊です。とはいえ、質問が示唆するさまざまな種類の意味的影響も適切であるとは限りません。
kvb 2010年

13
はい、リソースリークは正確性の問題です。しかし、「using」を使用しないと、リソースはリークされません。最終的にファイナライザが実行されると、リソースはクリーンアップされます。クリーンアップはあなたが望むほど積極的でタイムリーではないかもしれませんが、それは起こります。
Eric Lippert、2010年

7
素晴らしい投稿、これが私の2セントです:)多くの「乱用」はusingC#の貧乏人のアスペクト指向ウィーバーであるためです。開発者が本当に望んでいるのは、いくつかの一般的なコードがそのコードを複製する必要なしにブロックの最後で実行されることを保証する方法です。C#は現在AOPをサポートしていません(MarshalByRefObjectとPostSharpはこれに耐えられません)。ただし、コンパイラーによるusingステートメントの変換により、確定的なリソースの破棄のセマンティクスを転用することで、単純なAOPを実現できます。良い習慣はないが、それは.....渡すことが時々魅力的だ
LBushkin

11
私は一般的にあなたの立場に同意しますが、転用の利点がusing魅力的であるために放棄できない場合があります。私が考えることができる最良の例は、コードタイミングを追跡して報告することですusing( TimingTrace.Start("MyMethod") ) { /* code */ } 。これもAOP です。Start()ブロックが始まる前に開始時間Dispose()をキャプチャし、終了時間をキャプチャして、アクティビティをログに記録します。プログラムの状態は変更されず、例外にとらわれません。それはまたかなりの配管を回避し、パターンが混乱する意味を軽減するので頻繁に使用されるので魅力的です。
LBushkin、2010年

6
おかしい、私はいつもをRAIIをサポートする手段として考えていました。ロックを一種のリソースと考えることは、私にはかなり直感的に思えます。
ブライアン

27

クリーンでスコープのあるコードが必要な場合は、ラムダを使用することもできます。

myFribble.SafeExecute(() =>
    {
        myFribble.DangerDanger();
        myFribble.LiveOnTheEdge();
    });

ここで、.SafeExecute(Action fribbleAction)本方法は、ラップtry- catch- finallyブロック。


その例では、エントリー状態を逃しています。さらに、あなたが最後に/トライをラップしていますが、最終的には、コールは何もないので、ラムダで何かはあなたがでているものの状態見当がつかないスローした場合。
ヨハンGerell

1
ああ![OK]を、私はあなたがその意味を推測SafeExecuteされFribbleた呼び出し方法Unlock()Lock()最後に包まれたのtry /インチ それでは私の謝罪。私はすぐにSafeExecute一般的な拡張メソッドとして見たので、開始状態と終了状態の欠如について言及しました。私の悪い。
Johann Gerell、2010年

1
このアプローチには十分注意してください。地元住民を捕まえた場合、危険で微妙な意図しない寿命延長が発生する可能性があります!
ジェイソン

4
ゆく。なぜtry / catch / finallyを非表示にするのですか?他の人が読めるようにコードを非表示にします。
フランクSchwieterman

2
@Yukky Frank:何かを「隠す」というのは私の考えではなく、質問者の要求でした。:-P ...そうは言っても、要求はようやく「自分を繰り返さない」の問題です。何かをきれいに取得/解放する同じボイラープレート「フレーム」を必要とする多くのメソッドがあり、呼び出し側にそれを課したくない場合があります(カプセル化と考えてください)。また、.SafeExecute(...)のようなメソッドにもっと明確に名前を付けて、それらが何をするのかを十分に伝えることができます。
ヘルツマイスター、2010年

25

C#言語設計チームに所属していたエリックガナーソンは、ほぼ同じ質問に対してこの答えを出しました。

ダグは尋ねます:

re:タイムアウトのあるロックステートメント...

数多くのメソッドで共通のパターンを処理するために、以前にこのトリックを実行しました。通常はロック取得ですが、他にもいくつかあります。問題は、オブジェクトが「スコープの終わりでのコールバック」ほど実際には使い捨てではないため、常にハックのように感じられることです。

ダグ、

usingステートメントを決定したとき、このシナリオに正確に使用できるように、オブジェクトの配置に特定のものではなく、「using」という名前を付けることにしました。


4
まあ、その引用は、彼が「まさにこのシナリオ」と言ったときに彼が何を参照していたかを述べるべきです。
フランクSchwieterman

4
@Frank Schwieterman:見積もりを完了しました。どうやら、C#のチームからの人々がやったと思うusing機能はしていないリソースの処分に限定されます。
paercebal 2010

11

滑りやすい坂道です。IDisposableには契約があり、ファイナライザによってバックアップされています。ファイナライザはあなたの場合には役に立ちません。クライアントにusingステートメントの使用を強制することはできません。使用するように勧めるだけです。次のような方法で強制できます。

void UseMeUnlocked(Action callback) {
  Unlock();
  try {
    callback();
  }
  finally {
    Lock();
  }
}

しかし、それはラムダなしでは少し厄介になる傾向があります。そうは言っても、私はあなたがしたようにIDisposableを使用しました。

しかし、あなたの投稿には、これを危険なほどアンチパターンに近づける詳細があります。あなたはそれらのメソッドが例外を投げることができると述べました。これは発信者が無視できるものではありません。彼はそれに関して3つのことをすることができます:

  • 何もしないでください。例外は回復できません。通常の場合。Unlockの呼び出しは重要ではありません。
  • 例外をキャッチして処理する
  • 彼のコードで状態を復元し、例外に呼び出しチェーンを渡させます。

後者の2つでは、呼び出し元が明示的にtryブロックを書き込む必要があります。これで、usingステートメントが邪魔になります。クライアントが昏睡状態に陥り、あなたのクラスが国家の面倒を見ていて、追加の作業を行う必要がないと彼に思わせるかもしれません。それはほとんど正確ではありません。


強制訴訟は上記の「ヘルツマイスターデルウェルテン」によって与えられたと思います。たとえOPが例を好まなかったとしても、
Benjamin Podszun、2010年

はい、私は彼SafeExecuteを一般的な拡張メソッドとして解釈しました。私はそれはおそらくとしてのものだったことを今見Fribble呼び出す方法Unlock()Lock()、最後に包まれたのtry /で。私の悪い。
Johann Gerell

例外を無視しても、回復できないというわけではありません。これは、スタック内で上で処理されることを意味します。たとえば、例外を使用してスレッドを正常に終了できます。この場合、ロックされたロックを含め、リソースを正しく解放する必要があります。
paercebal 2010

7

実際の例は、ASP.net MVCのBeginFormです。基本的にあなたは書くことができます:

Html.BeginForm(...);
Html.TextBox(...);
Html.EndForm();

または

using(Html.BeginForm(...)){
    Html.TextBox(...);
}

Html.EndFormはDisposeを呼び出し、Disposeは</form>タグを出力するだけです。これの良い点は、{}ブラケットが目に見える「スコープ」を作成することです。これにより、フォーム内にあるものとないものを簡単に確認できます。

私はそれを使いすぎませんが、本質的にIDisposableは、「この関数を使い終わったら、この関数を呼び出さなければならない」と言うだけの方法です。MvcFormはそれを使用してフォームが閉じていることを確認し、Streamはそれを使用してストリームが閉じていることを確認します。これを使用してオブジェクトがロック解除されていることを確認できます。

個人的には、次の2つのルールが当てはまる場合にのみ使用しますが、これらは私が任意に設定します。

  • Disposeは常に実行する必要がある関数であるため、Null-Checks以外の条件はありません。
  • Dispose()の後、オブジェクトは再利用できません。再利用可能なオブジェクトが必要な場合は、破棄するのではなく、オープン/クローズメソッドを指定します。そのため、破棄されたオブジェクトを使用しようとすると、InvalidOperationExceptionがスローされます。

最後に、それは期待についてのすべてです。オブジェクトがIDisposableを実装している場合は、クリーンアップを行う必要があると思いますので、それを呼び出します。私は通常、「シャットダウン」機能を持っているよりも優れていると思います。

そうは言っても、この行は好きではありません。

this.Frobble.Fiddle();

FrobbleJanitorがFrobbleを「所有」しているので、代わりに管理人のFrobbleでFiddleを呼び出す方が良いのではないでしょうか。


あなたの「私はこのラインが好きではありません」について-質問を定式化するときに実際にそのようにすることを簡単に考えましたが、質問のセマンティクスを混乱させたくありませんでした。しかし、私はちょっとあなたに同意します。やや。:)
Johann

1
マイクロソフト自体からの例を提供することに賛成。あなたが言及した場合にスローされる特定の例外があることに注意してください:ObjectDisposedException。
Antitoon

4

このパターンはコードベースで十分に使用されており、以前から至る所で見たことがあります。きっとここでも議論されているはずです。一般的に、これを行うことの何が悪いのかわかりません。これは、有用なパターンを提供し、実際に害を及ぼすことはありません。


4

これについて詳しく説明します。これは壊れやすいのですが有用であるという点で、私はほとんど同意しています。私はあなたを指すようにたいSystem.Transaction.TransactionScopeのあなたがしたいような何かをしたクラス、。

一般的に私は構文が好きです、それは本当の肉から多くの混乱を取り除きます。ただし、ヘルパークラスに適切な名前を付けることを検討してください-おそらく上記の例のように... Scopeです。名前は、コードの一部をカプセル化していることを示す必要があります。* Scope、* Block、または同様のものがそれを行うはずです。


1
「用務員」は、ScopeGuardがLokiに到達する前でさえ、スコープガードについてのAndrei Alexandrescuによる古いC ++記事からのものです。
Johann

@ベンジャミン:私はTransactionScopeクラスが好きです。多分私はそれが含まれていない.Net CFランドにいるからです... :-(
Johann Gerell

4

注:私の視点はおそらくC ++の背景から偏っているので、私の答えの値はその可能な偏りに対して評価する必要があります...

C#言語仕様とは何ですか?

C#言語仕様の引用:

8.13 usingステートメント

[...]

リソースは廃棄という単一のパラメータのない方法を含む器具System.IDisposable、ことクラスまたは構造体です。リソースを使用しているコードは、Disposeを呼び出して、リソースが不要になったことを示すことができます。Disposeが呼び出されない場合、ガベージコレクションの結果として最終的に自動破棄が行われます。

リソースを使用しているコードは、もちろん、コードがによって開始されusingたキーワードに取り付けられた範囲まで行きますusing

ロックはリソースなので、これは大丈夫だと思います。

おそらくキーワードのusing選択が悪かったのでしょう。多分それは呼ばれるべきscopedでした。

そうすれば、ほとんど何でもリソースと見なすことができます。ファイルハンドル。ネットワーク接続...スレッド?

スレッド???

usingキーワードを使用(または乱用)していますか?

それは次のようになり、光沢のある使用(AB)にusingしてくださいスレッドの仕事は、スコープを終了する前に終了させるためにキーワードを?

Herb Sutterは、スレッドの作業が終了するのを待つためのIDisposeパターンの興味深い使用法を提供しているため、それをピカピカだと思っているようです。

http://www.drdobbs.com/go-parallel/article/showArticle.jhtml?articleID=225700095

以下は、記事からコピーして貼り付けたコードです。

// C# example
using( Active a = new Active() ) {    // creates private thread
       
       a.SomeWork();                  // enqueues work
       
       a.MoreWork();                   // enqueues work
       
} // waits for work to complete and joins with private thread

ActiveオブジェクトのC#コードは提供されていませんが、C ++バージョンがデストラクタに含まれているコードのIDisposeパターンをC#が使用するように記述されています。そして、C ++バージョンを見ると、この記事の他の抜粋に示すように、終了する前に内部スレッドが終了するのを待つデストラクタが見つかります。

~Active() {
    // etc.
    thd->join();
 }

したがって、ハーブに関する限り、それは光沢があります。


3

私はあなたの質問に対する答えはノーだと思います、これはの乱用ではないでしょうIDisposable

IDisposableインターフェイスを理解する方法は、オブジェクトが破棄された後はオブジェクトを操作することはできないということです(ただし、必要なだけDispose頻繁にそのメソッドを呼び出すことが許可されている場合を除きます)。

ステートメントに到達するたびに明示的に新しい オブジェクトを作成するため、同じオブジェクトを2回使用することはありません。また、その目的は別のオブジェクトを管理することであるため、この(「管理された」)リソースを解放するタスクに適しているようです。FrobbleJanitorusingFrobbeJanitorDispose

(ところで、Disposeファイルシステムハンドルなどのアンマネージリソースだけでなく、マネージリソースも解放する必要があることをほとんどの場合、適切な実装を示す標準サンプルコードで提案されています。)

私が個人的に心配する唯一のことは、&操作が直接表示さusing (var janitor = new FrobbleJanitor())れるより明示的なtry..finallyブロックを使用するよりも、何が起こっているのかがはっきりしないことです。しかし、どちらのアプローチを取るかは、おそらく個人的な好みの問題に帰着します。LockUnlock


1

その虐待ではありません。あなたはそれらが作成された目的でそれらを使用しています。ただし、必要に応じて、次々と検討する必要があります。たとえば、 'artistry'を選択している場合は、 'using'を使用できますが、コードが何度も実行される場合は、パフォーマンス上の理由から、 'try' .. 'finally'構文を使用できます。「使用」には通常、オブジェクトの作成が含まれるためです。


1

あなたはそれを正しくやったと思います。Dispose()のオーバーロードは、同じクラスが後で実際に実行する必要があるクリーンアップの問題であり、そのクリーンアップのライフタイムは、ロックを保持する予定の時間とは異なるものに変更されました。しかし、Frobbleのロックとロック解除のみを担当する別のクラス(FrobbleJanitor)を作成したため、問題が発生しないように、十分に分離されています。

ただし、おそらくFrobbleJanitorの名前をFrobbleLockSessionのような名前に変更します。


名前の変更について-ロック/ロック解除を扱った主な理由の「管理人」を使用しました。私はそれが他の名前の選択と韻を踏むと思った。;-)コードベース全体でこのメソッドをパターンとして使用する場合、「セッション」で暗黙的に示したように、より一般的に説明的なものを確実に含めます。これが古いC ++の日だったとしたら、おそらくFrobbleUnlockScopeを使用するでしょう。
Johann Gerell
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.