113

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

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

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

4

12 に答える 12

72

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

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

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

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

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

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

多分そうです、多分そうではありません。私のポイントは、元々書かれたコードで、コードレビューアは質問をすることを知っているということです。「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領域に入る前に、スレッドアボート例外がスローされる可能性があります。しかし、「使用」バージョンでは、これは「だから何?」だと思います。状況。それが起こった場合は残念ですが、「使用」は、非常に重要なプログラムの状態を変更するのではなく、丁寧に行うためだけにあると思います。ひどいスレッドアボート例外が正確に間違ったタイミングで発生した場合、ガベージコレクターは最終的にファイナライザーを実行してそのリソースをクリーンアップすると思います。

于 2010-01-20T16:45:52.397 に答える
35

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

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

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

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

于 2010-01-20T13:22:25.010 に答える
27

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

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

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

于 2010-01-20T13:31:59.440 に答える
27

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

ダグは尋ねます:

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

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

ダグ、

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

于 2010-01-20T17:00:37.070 に答える
11

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

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

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

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

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

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

于 2010-01-20T13:45:49.730 に答える
8

実際の例は、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を「所有」しているので、代わりに管理人のFrobbleでFiddleを呼び出す方がよいのではないかと思います。

于 2010-01-20T20:13:15.493 に答える
4

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

于 2010-01-20T13:22:01.410 に答える
4

これについてのチャイム:これは壊れやすいが有用であるという点で、私はここで最も同意します。System.Transaction.TransactionScopeクラスを紹介します。これは、あなたがやりたいことを実行します。

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

于 2010-01-20T13:53:38.293 に答える
4

注:私の視点はおそらくC ++のバックグラウンドから偏っているので、私の答えの値はその考えられる偏りに対して評価する必要があります...

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

C#言語仕様の引用:

8.13usingステートメント

[...]

リソースは、System.IDisposableを実装するクラスまたは構造体であり、Disposeという名前の単一のパラメーターなしのメソッドが含まれています。リソースを使用しているコードは、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#は、C++バージョンがデストラクタに含まれているコードのIDisposeパターンを使用して記述されています。そして、C ++バージョンを見ると、この記事の他の抜粋に示されているように、終了する前に内部スレッドが終了するのを待つデストラクタがわかります。

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

だから、ハーブに関する限り、それは光沢があります。

于 2010-08-27T13:26:11.443 に答える
3

私はあなたの質問への答えはノーだと信じています、これはの乱用ではないでしょうIDisposable

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

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

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

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

于 2010-01-20T13:30:47.160 に答える
1

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

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

于 2010-01-20T19:07:51.740 に答える
1

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

于 2010-01-20T13:54:45.753 に答える