web-dev-qa-db-ja.com

例外安全性のために「スコープされた動作」を取得する手段としてIDisposableを使用し、「使用する」ことは悪用されますか?

私が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が使用されることwithusingなので、現実は追いつきます。これはコードレビューの問題と考えることができますが、何かが私を悩ませています。

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

110
Johann Gerell

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

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

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

これを行うには他にもたくさんの選択肢がありますが、最終的には、これほどきちんとしたものは考えられないので、それを試してみてください!

32
Andras Zoltan

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

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

まず、「使用する」はリソースを使用するおよび使い終わったら破棄するに使用されることを期待しているためです。プログラムの状態を変更することはリソースを使用するではなく、元に戻すことは破棄する何でもありません。したがって、状態を変更および復元するために「使用する」ことは悪用です。コードは、カジュアルな読者には誤解を招くものです。

第二に、私は「使用する」が使用されることを期待しているので礼儀正しさから、必要ではない。使い終わったときにファイルを破棄するために「using」を使用する理由は、それがnecessaryであるためではなく、polite-であるためです。他の誰かがそのファイルの使用を待っている可能性があるため、「今すぐ完了」と言うのは道徳的に正しいことです。 「使用中」をリファクタリングして、使用済みリソースをより長く保持し、後で破棄できるようにする必要があります。そうすることによる唯一の影響は、他のプロセスに少し不便をかけるプログラム状態へのセマンティックな影響を持つ「using」ブロックは、必要ではなく、利便性と礼儀正しさのために存在するように見える構造内のプログラム状態の重要で必要な突然変異を隠すため、悪用されます。

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

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

多分そうです、多分そうではありません。私のポイントは、最初に書かれたコードでは、コードレビューアは質問をすることを知っているということです。 「using」を使用するコードでは、質問するかどうかわかりません。 「using」ブロックはリソースを割り当て、少しの間それを使用し、それが完了すると丁寧に破棄すると仮定します。「using」ブロックの終了中括弧ではありません任意に多くのプログラム状態の整合性条件に違反した場合、例外的な状況でプログラム状態を変更します。

「using」ブロックを使用してセマンティック効果を持たせると、このプログラムはフラグメントになります。

}

非常に意味があります。その単一の閉じたブレースを見ると、「そのブレースには、私のプログラムのグローバルな状態に広範囲にわたる影響を与える副作用がある」とすぐには思いません。しかし、このように「使用」を乱用すると、突然そうなります。

元のコードを見たかどうかを2番目に尋ねるのは、「ロック解除後、試行が開始される前に例外がスローされた場合はどうなるか」です。最適化されていないアセンブリを実行している場合、コンパイラは試行の前に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-protected領域に入る前に、スレッドアボート例外がスローされる可能性があります。しかし、「使用」バージョンでは、これは「だから何?」だと思います。状況。それが起こった場合は残念ですが、「使用」は、非常に重要なプログラムの状態を変更するのではなく、礼儀正しくするためだけにあると思います。ひどいスレッドアボート例外が正確に間違った時間に発生した場合、ガベージコレクターは最終的にファイナライザーを実行してそのリソースをクリーンアップすると思います。

70
Eric Lippert

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

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

ここで、.SafeExecute(Action fribbleAction)メソッドはtry --catch --finallyブロックをラップします。

27
herzmeister

C#言語設計チームに所属していたEric Gunnersonは、ほぼ同じ質問に この回答 を与えました。

ダグは尋ねます:

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

私は対処する前にこのトリックをしました 一般的なパターン 多くの方法で。通常 ロック取得、しかしあります 他の何人か。問題は、オブジェクトが実際には使い捨てではないため、常にハックのように感じることです。スコープの終わりでコールバック可能"。

ダグ、

Usingステートメントを決定したとき、まさにこのシナリオで使用できるように、オブジェクトの破棄に固有の名前ではなく、「using」という名前を付けることにしました。

24
Samuel Jack

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

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

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

ただし、投稿には、これをアンチパターンに危険なほど近づける詳細があります。これらのメソッドは例外をスローする可能性があるとおっしゃいました。これは、発信者が無視できるものではありません。彼はそれについて3つのことをすることができます:

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

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

11
Hans Passant

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

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

または

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

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

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

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

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

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

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

this.Frobble.Fiddle();

FrobbleJanitorがFrobbleを「所有」しているので、代わりにFiddleを管理人のFrobbleに呼び出す方がよいのではないかと思いますか?

7
Michael Stum

これについてのチャイム:これは壊れやすいが便利であるという点で、ここのほとんどの人に同意します。 System.Transaction.TransactionScope クラスを紹介したいと思います。これは、あなたがやりたいことを実行します。

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

4

コードベースではこのパターンを多用していますが、これまでに至る所で見たことがあります。ここでも説明したに違いありません。一般的に、これを行うことの何が問題なのかはわかりません。これは有用なパターンを提供し、実際に害を及ぼすことはありません。

4
Simon Steele

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

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

引用 C#言語仕様

8.13usingステートメント

[...]

資源 System.IDisposableを実装するクラスまたは構造体であり、Disposeという名前の単一のパラメーターなしのメソッドが含まれています。リソースを使用しているコードは、Disposeを呼び出して、リソースが不要になったことを示すことができます。 Disposeが呼び出されない場合、ガベージコレクションの結果として最終的に自動廃棄が発生します。

ザ・ リソースを使用しているコード もちろん、これはusingキーワードで始まり、スコープがusingに接続されるまで続くコードです。

ロックはリソースであるため、これはOk)だと思います。

おそらく、キーワードusingが正しく選択されていません。おそらくそれはscopedと呼ばれるべきでした。

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

スレッド???

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

それでしょうか ピカピカ (ab)usingキーワードを使用して、スコープを終了する前にスレッドの作業が終了していることを確認しますか?

ハーブサッターは ピカピカ、彼は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#は、C++バージョンがデストラクタに含まれているコードのIDisposeパターンを使用して記述されています。また、C++バージョンを見ると、この記事の他の抜粋に示されているように、内部スレッドが終了するのを待ってから終了するデストラクタがあります。

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

だから、ハーブに関する限り、それは ピカピカ

4
paercebal

あなたの質問に対する答えはノーだと思います。これはIDisposableの乱用ではありません。

私がIDisposableインターフェースを理解する方法は、オブジェクトが破棄されると、オブジェクトを操作することは想定されていないということです(ただし、オブジェクトのDisposeメソッドを何度でも呼び出すことができます。 )。

FrobbleJanitorステートメントに到達するたびにnewusingを明示的に作成するため、同じFrobbeJanitorオブジェクトを2回使用することはありません。また、別のオブジェクトを管理することが目的であるため、この(「管理対象」)リソースを解放するタスクにはDisposeが適切であるように思われます。

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

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

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

1
ata

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

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

1