どのようにしてIDisposableがすべてのクラスに広がるのを防ぎますか?


138

これらの単純なクラスから始めます...

次のような単純なクラスのセットがあるとします。

class Bus
{
    Driver busDriver = new Driver();
}

class Driver
{
    Shoe[] shoes = { new Shoe(), new Shoe() };
}

class Shoe
{
    Shoelace lace = new Shoelace();
}

class Shoelace
{
    bool tied = false;
}

AにBusはがありDriver、にDriverは2つShoeのがあり、それぞれにShoeがありShoelaceます。すべて非常にばかげています。

IDisposableオブジェクトをShoelaceに追加する

後で、上のいくつかの操作をShoelaceマルチスレッド化できると判断したのでEventWaitHandle、スレッドが通信するためのを追加します。したがってShoelace、次のようになります。

class Shoelace
{
    private AutoResetEvent waitHandle = new AutoResetEvent(false);
    bool tied = false;
    // ... other stuff ..
}

IDisposableをShoelaceに実装する

しかし、今度はMicrosoftのFxCopが文句を言うだろう:「IDisposableを次のIDisposable型のメンバーを作成するため、「Shoelace」にIDisposableを実装する:「EventWaitHandle」。

さて、私は実装IDisposableShoelace、私のきちんとした小さなクラスはこの恐ろしい混乱になります:

class Shoelace : IDisposable
{
    private AutoResetEvent waitHandle = new AutoResetEvent(false);
    bool tied = false;
    private bool disposed = false;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    ~Shoelace()
    {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            if (disposing)
            {
                if (waitHandle != null)
                {
                    waitHandle.Close();
                    waitHandle = null;
                }
            }
            // No unmanaged resources to release otherwise they'd go here.
        }
        disposed = true;
    }
}

または(コメント投稿者が指摘したように)それShoelace自体にはアンマネージリソースがないため、Dispose(bool)およびDestructor を必要とせずに、より簡単な破棄の実装を使用できます。

class Shoelace : IDisposable
{
    private AutoResetEvent waitHandle = new AutoResetEvent(false);
    bool tied = false;

    public void Dispose()
    {
        if (waitHandle != null)
        {
            waitHandle.Close();
            waitHandle = null;
        }
        GC.SuppressFinalize(this);
    }
}

IDisposableが広がっていくのを恐怖で見る

正解です。しかし、FxCopはをShoe作成すると文句を言うShoelaceので、そうでShoeなければなりIDisposableません。

そしてDriver作成ShoeするDriver必要がありますIDisposable。そしてBusDriverそうでBusなければなりませんIDisposable

突然、私の小さな変更によってShoelace多くの作業が発生し、上司はなぜにBus変更するためにチェックアウトする必要があるのか​​疑問に思っていShoelaceます。

質問

どのようにしてこのの拡散を防ぎIDisposableながら、管理されていないオブジェクトが適切に破棄されるようにしますか?


9
非常に良い質問です。答えはそれらの使用を最小限に抑え、高レベルのIDisposablesを使用して短命に保つようにすることですが、これは常に可能であるとは限りません(特に、これらのIDisposablesがC ++ dllとの相互運用が原因である場合など)。答えを見てください。
アナカタ2009年

Okay Dan、質問を更新して、IDisposableをShoelaceに実装する両方の方法を示しました。
GrahamS 2009年

私は通常、他のクラスの実装の詳細に依存して私を保護することに警戒しています。簡単に防ぐことができればリスクはありません。たぶん私は用心深いか、多分私はCプログラマーとして過ごしすぎたかもしれませんが、アイルランドのアプローチを採用したいと思います。「確かに、確かに」:)
GrahamS 2009年

@Dan:オブジェクト自体がnullに設定されていないことを確認するためにnullチェックが引き続き必要です。この場合、waitHandle.Dispose()への呼び出しはNullReferenceExceptionをスローします。
スコットドーマン

1
何があっても、「靴紐にIDisposableを実装する」セクションに示すように、実際にはDispose(bool)メソッドを使用する必要があります(ファイナライザを除いたもの)が完全なパターンであるためです。クラスがIDisposableを提供するからといって、ファイナライザが必要であるとは限りません。
スコットドーマン

回答:


36

IDisposableが拡散するのを実際に「防ぐ」ことはできません。など、一部のクラスは破棄する必要があります。AutoResetEvent最も効率的な方法はDispose()、ファイナライザのオーバーヘッドを回避するためにメソッドで破棄することです。ただし、このメソッドは何らかの方法で呼び出す必要があります。そのため、例のように、IDisposableをカプセル化または含むクラスはこれらを破棄する必要があるため、それらも破棄する必要があります。これを回避する唯一の方法は、次のとおりです。

  • 可能な場合はIDisposableクラスの使用を避け、単一の場所でイベントをロックまたは待機する、高価なリソースを単一の場所に保持するなど
  • 必要なときにのみ作成し、直後に破棄します(usingパターン)

IDisposableはオプションのケースをサポートするため、無視できる場合があります。たとえば、WaitHandleはIDisposableを実装して、名前付きミューテックスをサポートしています。名前が使用されていない場合、Disposeメソッドは何もしません。MemoryStreamは別の例で、システムリソースを使用せず、そのDispose実装も何もしません。アンマネージリソースが使用されているかどうかを慎重に検討することは、教育に役立ちます。したがって、.netライブラリの利用可能なソースを調べたり、逆コンパイラを使用したりできます。


4
今日の実装では何もしないので無視できるというアドバイスは悪いです。将来のバージョンではDisposeで重要なことが実際に行われる可能性があり、リークを追跡するのが困難になっています。
アンディ

1
「高価なリソースを保持する」、IDisposableは必ずしも高価ではありません。実際、偶数の待機ハンドルを使用するこの例は、ご想像どおり「軽量」です。
markmnl 2016年

20

正確さの点では、親オブジェクトが使い捨てにする必要のある子オブジェクトを作成して本質的に所有している場合、オブジェクト関係を介したIDisposableの拡散を防ぐことはできません。この状況ではFxCopは正しく、親はIDisposableである必要があります。

オブジェクト階層のリーフクラスにIDisposableを追加しないようにすることができます。これは必ずしも簡単な作業ではありませんが、興味深い演習です。論理的な観点から、ShoeLaceが使い捨てである必要がある理由はありません。ここにWaitHandleを追加する代わりに、ShoeLaceとWaitHandleの間の関連付けを、それが使用される時点で追加することもできます。最も簡単な方法は、Dictionaryインスタンスを使用することです。

WaitHandleが実際に使用される時点で、マップを介してWaitHandleを緩い関連付けに移動できる場合は、このチェーンを解除できます。


2
そこで連想するのはとても不思議です。このAutoResetEventはShoelace実装にプライベートであるため、それを公開することは私の意見では間違っているでしょう。
GrahamS 2009年

@GrahamS、私はそれを公開することを言っているのではありません。靴ひもが結ばれるところまで動かせますか?おそらく靴ひもを結ぶことがそのような複雑な仕事であるならば、それらは靴ひもティアクラスであるべきです。
JaredPar 2009年

1
いくつかのコードスニペットJaredParを使用して、回答を拡張できますか?私の例を少し伸ばしているかもしれませんが、Shoelaceが作成して、Tyerスレッドを開始し、waitHandle.WaitOne()で辛抱強く待機していることを想像しています。
GrahamS 2009年

1
これを+1します。Shoelaceだけが同時に呼び出されて他の呼び出しは呼び出されないのは奇妙だと思います。何が集約ルートになるべきか考えてください。この場合、私はドメインに慣れていませんが、バス、私見である必要があります。したがって、その場合、バスにはウェイトハンドルとバスに対するすべての操作が含まれている必要があり、そのすべての子が同期されます。
Johannes Gustafsson、2010年

16

IDisposable拡散を防ぐには、使い捨てオブジェクトの使用を1つのメソッド内にカプセル化するようにしてください。Shoelace別の方法で設計してみてください。

class Shoelace { 
  bool tied = false; 

  public void Tie() {

    using (var waitHandle = new AutoResetEvent(false)) {

      // you can even pass the disposable to other methods
      OtherMethod(waitHandle);

      // or hold it in a field (but FxCop will complain that your class is not disposable),
      // as long as you take control of its lifecycle
      _waitHandle = waitHandle;
      OtherMethodThatUsesTheWaitHandleFromTheField();

    } 

  }
} 

待機ハンドルのスコープはTieメソッドに制限されており、クラスには使い捨てフィールドが必要ないため、それ自体が使い捨てである必要はありません。

待機ハンドルは内の実装の詳細であるためShoelace、宣言に新しいインターフェースを追加するなど、パブリックインターフェースを変更する必要はありません。使い捨てフィールドが不要になった場合はどうなりますIDisposableか?宣言を削除しますか?Shoelace 抽象化について考えると、のようなインフラストラクチャの依存関係によって汚染されるべきではないことがすぐにわかりますIDisposableIDisposable決定論的なクリーンアップを必要とするリソースを抽象化してカプセル化するクラスのために予約する必要があります。つまり、使い捨てが抽象化の一部であるクラスの場合です。


1
Shoelaceの実装の詳細によってパブリックインターフェースが汚染されるべきではないことに完全に同意しますが、私の考えは、回避するのが難しい場合があるということです。ここで提案することは常に可能とは限りません。AutoResetEvent()の目的はスレッド間で通信することなので、そのスコープは通常、単一のメソッドを超えて拡張されます。
GrahamS

@GrahamS:それが私がその方法でそれを設計しようとするように言った理由です。使い捨てを他のメソッドに渡すこともできます。クラスへの外部呼び出しが使い捨てのライフサイクルを制御するときだけ、それは故障します。それに応じて答えを更新します。
ジョルダン

2
申し訳ありませんが、使い捨てを回すことができるのはわかっていますが、それでも機能しません。私の例でAutoResetEventは、は同じクラス内で動作する異なるスレッド間の通信に使用されるため、メンバー変数でなければなりません。スコープをメソッドに制限することはできません。(たとえば、1つのスレッドがをブロックすることで何らかの作業を待機していると想像してくださいwaitHandle.WaitOne()。メインスレッドは次にshoelace.Tie()メソッドを呼び出します。このメソッドは、a waitHandle.Set()を実行してすぐに戻ります)。
GrahamS

3

これは、基本的に、CompositionまたはAggregationとDisposableクラスを混在させるとどうなるかです。前述のように、最初の方法は、waitHandleを靴紐からリファクタリングすることです。

そうは言っても、アンマネージリソースがない場合は、Disposableパターンを大幅に削減できます。(私はまだこれの公式リファレンスを探しています。)

ただし、デストラクタとGC.SuppressFinalize(this);は省略できます。仮想ボイドのDispose(bool disposing)を少しクリーンアップします。


ヘンクに感謝します。私がwaitHandleの作成をShoelaceから削除した場合、誰かがまだそれをどこかで破棄する必要があるので、誰かがShoelaceがそれで終わったこと(およびShoelaceが他のクラスに渡していないこと)をどのように知るのでしょうか?
GrahamS 2009年

作成するだけでなく、ウェイトハンドルの「責任」も移動する必要があります。jaredParの回答には、興味深いアイデアの始まりがあります。
Henk Holterman、

このブログでは、IDisposableの実装について説明し、特に、管理されたリソースと管理されていないリソースが1つのクラスに混在し
2009/08 /

3

興味深いことに、Driver上記のように定義されている場合:

class Driver
{
    Shoe[] shoes = { new Shoe(), new Shoe() };
}

次に、Shoeが作成されるとIDisposable、FxCop(v1.36)はDriverIDisposable

ただし、次のように定義されている場合:

class Driver
{
    Shoe leftShoe = new Shoe();
    Shoe rightShoe = new Shoe();
}

それから文句を言うでしょう。

これはソリューションではなくFxCopの制限にすぎないのではないかと思います。最初のバージョンではShoeインスタンスはまだによって作成されてDriverおり、何らかの方法で破棄する必要があるためです。


2
ShowがIDisposableを実装している場合、フィールド初期化子でそれを作成することは危険です。興味深いことに、FXCopはそのような初期化を許可しません。C#では安全に初期化する唯一の方法はThreadStatic変数(実に恐ろしい)を使用するためです。vb.netでは、ThreadStatic変数なしでIDisposableオブジェクトを安全に初期化できます。コードはまだ醜いですが、それほどひどいものではありません。MSがそのようなフィールド初期化子を安全に使用するための素晴らしい方法を提供してくれることを願っています。
supercat

1
@supercat:ありがとう。なぜ危険なのか説明てもらえますか?Shoeコンストラクターの1つで例外がスローされた場合、shoes困難な状態のままになると思いますか?
GrahamS 2011

3
特定のタイプのIDisposableを安全に破棄できることを知らない限り、作成されたすべてのオブジェクトが確実にDisposeされるようにする必要があります。IDisposableがオブジェクトのフィールド初期化子で作成されたが、オブジェクトが完全に構築される前に例外がスローされた場合、オブジェクトとそのすべてのフィールドは破棄されます。オブジェクトの構築が、「try」ブロックを使用して構築の失敗を検出するファクトリメソッドでラップされている場合でも、廃棄が必要なオブジェクトへの参照をファクトリが取得することは困難です。
スーパーキャット

3
C#でこれに対処するために知っている唯一の方法は、ThreadStatic変数を使用して、現在のコンストラクターがスローした場合に破棄が必要なオブジェクトのリストを保持することです。次に、各フィールド初期化子が各オブジェクトを作成時に登録し、正常に完了した場合はリストをクリアし、「finally」句を使用してリストがクリアされていない場合はリストからすべての項目を破棄します。それは実行可能なアプローチですが、ThreadStatic変数は醜いです。vb.netでは、ThreadStatic変数を必要とせずに、同様の手法を使用できます。
スーパーキャット、2011

3

デザインを非常に緊密に結合している場合、IDisposableが拡散するのを防ぐ技術的な方法はないと思います。次に、設計が正しいかどうか疑問に思う必要があります。

あなたの例では、靴に靴ひもを所有させることは理にかなっていると思います。おそらく、ドライバーが自分の靴を所有する必要があります。ただし、バスはドライバーを所有するべきではありません。通常、バスの運転手はスクラップヤードまでバスを追いません:)運転手と靴の場合、運転手が自分の靴を作ることはめったにありません。

代替設計は次のとおりです。

class Bus
{
   IDriver busDriver = null;
   public void SetDriver(IDriver d) { busDriver = d; }
}

class Driver : IDriver
{
   IShoePair shoes = null;
   public void PutShoesOn(IShoePair p) { shoes = p; }
}

class ShoePairWithDisposableLaces : IShoePair, IDisposable
{
   Shoelace lace = new Shoelace();
}

class Shoelace : IDisposable
{
   ...
}

新しいデザインは、残念ながら、インスタンスを作成して靴とドライバーの具体的なインスタンスを破棄するために追加のクラスを必要とするため、より複雑になりますが、この複雑さは、解決される問題に固有のものです。良い点は、靴ひもを処分するためだけにバスを使い捨てにする必要がなくなったことです。


2
ただし、これは元の問題を実際に解決するものではありません。それはFxCopを静かに保つかもしれませんが、何かがまだ靴ひもを処分するべきです。
AndrewS 2010年

あなたがしたことは、問題を別の場所に移動することだけだと思います。ShoePairWithDisposableLacesがShoelace()を所有するようになったため、IDisposableにする必要もあります。つまり、誰が靴を処分するのでしょうか。これを処理するIoCコンテナーに任せていますか?
GrahamS、2010年

@AndrewS:確かに、ドライバー、靴、靴ひもはまだ何かを処分する必要がありますが、処分はバスによって開始されるべきではありません。@GrahamS:あなたは正しい、私は問題を別の場所に移動しています。通常、靴の処分を担当するクラスのインスタンス化靴があります。ShoePairWithDisposableLacesも使い捨てにできるようにコードを編集しました。
Joh

@ジョー:ありがとう。あなたが言うように、それを他の場所に移動することの問題は、あなたがより多くの複雑さを作り出すことです。ShoePairWithDisposableLacesが他のクラスによって作成された場合、ドライバーが靴を使い終わったときに、そのクラスに通知して、適切にDispose()できるようにする必要がありますか?
GrahamS、2010年

1
@Graham:はい、何らかの通知メカニズムが必要になります。たとえば、参照カウントに基づく廃棄ポリシーを使用できます。いくつかの複雑さが追加されていますが、おそらくご存知のように、「無料の靴のようなものはありません」:)
Joh

1

制御の反転を使用するのはどうですか?

class Bus
{
    private Driver busDriver;

    public Bus(Driver busDriver)
    {
        this.busDriver = busDriver;
    }
}

class Driver
{
    private Shoe[] shoes;

    public Driver(Shoe[] shoes)
    {
        this.shoes = shoes;
    }
}

class Shoe
{
    private Shoelace lace;

    public Shoe(Shoelace lace)
    {
        this.lace = lace;
    }
}

class Shoelace
{
    bool tied;
    private AutoResetEvent waitHandle;

    public Shoelace(bool tied, AutoResetEvent waitHandle)
    {
        this.tied = tied;
        this.waitHandle = waitHandle;
    }
}

class Program
{
    static void Main(string[] args)
    {
        using (var leftShoeWaitHandle = new AutoResetEvent(false))
        using (var rightShoeWaitHandle = new AutoResetEvent(false))
        {
            var bus = new Bus(new Driver(new[] {new Shoe(new Shoelace(false, leftShoeWaitHandle)),new Shoe(new Shoelace(false, rightShoeWaitHandle))}));
        }
    }
}

これで、呼び出し側の問題になりました。Shoelaceは、必要なときに利用可能な待機ハンドルに依存できません。
アンディ

@andy「Shoelaceは、必要なときに利用可能な待機ハンドルに依存できない」とはどういう意味ですか?それはコンストラクターに渡されます。
ダラグ

他の何かがShoelaceに渡す前にautoresetevents状態を台無しにしている可能性があり、悪い状態のAREで始まる可能性があります。Shoelaceが処理を実行している間に、AREの状態に他の何かが干渉し、Shoelaceが間違った処理を実行する可能性があります。プライベートメンバーのみをロックするのと同じ理由です。「一般的には、...あなたのコードを越えたインスタンス上の回避のロックを制御」docs.microsoft.com/en-us/dotnet/csharp/language-reference/...
アンディ・

プライベートメンバーのみをロックすることに同意します。ただし、待機ハンドルが異なるようです。実際、同じインスタンスを使用してスレッド間で通信する必要があるため、それらをオブジェクトに渡す方が便利なようです。それにもかかわらず、IoCはOPの問題に対する有用な解決策を提供すると思います。
Darragh

OPの例では、プライベートメンバーとしてウェイトハンドルがあったので、オブジェクトがそれへの排他的アクセスを期待することが安全な仮定です。インスタンスの外部でハンドルを表示すると、違反になります。通常、IoCはこのような場合に適していますが、スレッドに関しては適していません。
アンディ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.