web-dev-qa-db-ja.com

別のオブジェクトに渡されたときに、誰がIDisposableオブジェクトでDisposeを呼び出す必要がありますか?

使い捨てオブジェクトが別のオブジェクトのメソッドまたはコンストラクターに渡されたときに、誰がDispose()を呼び出す必要があるかについてのガイダンスまたはベストプラクティスはありますか?

これが私が何を意味するかについてのいくつかの例です。

IDisposableオブジェクトがメソッドに渡されます(完了したら破棄する必要がありますか?):

_public void DoStuff(IDisposable disposableObj)
{
    // Do something with disposableObj
    CalculateSomething(disposableObj)

    disposableObj.Dispose();
}
_

IDisposableオブジェクトがメソッドに渡され、参照が保持されます(MyClassが破棄されたときに破棄する必要がありますか?):

_public class MyClass : IDisposable
{
    private IDisposable _disposableObj = null;

    public void DoStuff(IDisposable disposableObj)
    {
        _disposableObj = disposableObj;
    }

    public void Dispose()
    {
        _disposableObj.Dispose();
    }
}
_

私は現在、最初の例では、DoStuff()の-​​callerは、おそらくオブジェクトを作成したので、オブジェクトを破棄する必要があると考えています。しかし、2番目の例では、オブジェクトへの参照を保持しているため、MyClassがオブジェクトを破棄する必要があるように感じます。これに伴う問題は、呼び出し側クラスがMyClassが参照を保持していることを知らない可能性があるため、MyClassがオブジェクトの使用を終了する前にオブジェクトを破棄することを決定する可能性があることです。この種のシナリオの標準的なルールはありますか?ある場合、使い捨てオブジェクトがコンストラクターに渡されるときにそれらは異なりますか?

57
Jon Mitchell

原則として、オブジェクトを作成した(または所有権を取得した)場合は、オブジェクトを破棄するのはユーザーの責任です。つまり、メソッドまたはコンストラクターのパラメーターとして使い捨てオブジェクトを受け取った場合、通常はそれを破棄しないでください。

.NET Frameworkの一部のクラスdoは、パラメーターとして受け取ったオブジェクトを破棄することに注意してください。たとえば、StreamReaderを破棄すると、基になるStreamも破棄されます。

35
Mark Byers

PS:私は 新しい答え を投稿しました(Disposeを呼び出す必要があるルールの簡単なセットと、 IDisposableオブジェクトを処理するAPI)。現在の回答には貴重なアイデアが含まれていますが、その主な提案は実際には機能しないことが多いと私は信じるようになりました。「粗い」オブジェクトにIDisposableオブジェクトを隠すことは、多くの場合、それらがIDisposableになる必要があることを意味します。そのため、開始した場所に行き着き、問題が残ります。


使い捨てオブジェクトが別のオブジェクトのメソッドまたはコンストラクターに渡されたときに、誰がDispose()を呼び出す必要があるかについてのガイダンスまたはベストプラクティスはありますか?

短い答え:

はい、このトピックについては多くのアドバイスがあります。私が知っている最善の方法は、 Eric Evans 'の概念 Aggregates in Domain-Driven Design 。 (簡単に言えば、IDisposableに適用される中心的な考え方は、次のとおりです。IDisposableを、外部から見えず、コンポーネントコンシューマーに渡されないように、より粗いコンポーネントにカプセル化します。)

さらに、IDisposableオブジェクトの作成者もオブジェクトの破棄を担当する必要があるという考えは、制限が厳しく、実際には機能しないことがよくあります。

私の答えの残りの部分は、同じ順序で、両方の点についてより詳細に説明します。同じトピックに関連するさらなる資料へのいくつかのポインタで答えを締めくくります。

より長い回答—この質問の広義の意味:

このトピックに関するアドバイスは、通常、IDisposableに固有のものではありません。人々がオブジェクトの存続期間と所有権について話すときはいつでも、彼らはまったく同じ問題に言及しています(しかしより一般的な用語で)。

このトピックが.NETエコシステムでほとんど発生しないのはなぜですか? .NETのランタイム環境(CLR)は自動ガベージコレクションを実行するため、すべての作業が自動的に行われます。オブジェクトが不要になった場合は、オブジェクトを忘れることができ、ガベージコレクターは最終的にメモリを再利用します)。

では、なぜ質問はIDisposableオブジェクトを思い付くのでしょうか。 IDisposableは、(多くの場合、スパースまたは高価な)リソースの存続期間の明示的で決定論的な制御に関するものであるため、IDisposableオブジェクトは、不要になるとすぐに解放されることになっています—そしてガベージコレクターの不確定な保証( "I'll 最終的にメモリあなたが使用します! ")を再利用するだけでは十分ではありません。

あなたの質問は、オブジェクトの存続期間と所有権のより広い用語で言い換えられます:

どのオブジェクトO)が(使い捨て)オブジェクトDの存続期間を終了する責任を負う必要があります。これは、オブジェクトX,Y,Zにも渡されますか?

いくつかの仮定を確立しましょう:

  • IDisposableオブジェクトに対してD.Dispose()を呼び出すと、Dは基本的にその存続期間が終了します。

  • 論理的には、オブジェクトの存続期間は1回だけ終了できます。 (これは、IDisposableへの複数の呼び出しを明示的に許可するDisposeプロトコルに反対していることを今のところ気にしないでください。)

  • したがって、簡単にするために、1つのオブジェクトODを破棄する責任を負う必要があります。Oを所有者と呼びましょう。

ここで、問題の核心に到達します。C#言語もVB.NETも、オブジェクト間の所有権関係を強制するためのメカニズムを提供していません。したがって、これは設計上の問題になります。別のオブジェクトへの参照を受け取るすべてのオブジェクトO,X,Y,ZD)は、Dの所有権を誰が持つかを正確に規制する規則に従う必要があります。

集計の問題を単純化してください!

このトピックに関して私が見つけた唯一の最良のアドバイスは、 Eric Evans '2004年の本、 ドメイン駆動設計 から来ています。本から引用させてください:

データベースからPersonオブジェクトを削除するとします。Personオブジェクトと一緒に、名前、生年月日、職務内容を入力します。ただし、住所はどうですか?同じ住所に他の人がいる可能性があります。住所を削除すると、それらのPersonオブジェクトは、削除されたオブジェクトへの参照を持ちます。そのままにしておくと、データベースにジャンクアドレスが蓄積されます。自動ガベージコレクションによってジャンクアドレスが削除される可能性がありますが、その技術的な修正は、データベースシステムで利用できる場合でも、基本的なものを無視します。モデリングの問題(p。125)

これがあなたの問題にどのように関連しているかわかりますか?この例のアドレスは使い捨てオブジェクトと同等であり、質問は同じです。誰がそれらを削除する必要がありますか?誰がそれらを「所有」していますか?

Evansは、この設計問題の解決策としてAggregatesを提案し続けています。再び本から:

アグリゲートは、データ変更の目的で1つの単位として扱う、関連付けられたオブジェクトのクラスターです。各アグリゲートにはルートと境界があります。境界は、アグリゲート内にあるものを定義します。ルートは、含まれる単一の特定のエンティティです。ルートは、外部オブジェクトが参照を保持できる唯一のアグリゲートのメンバーですが、境界内のオブジェクトは相互に参照を保持できます。(pp。126-127)

ここでの中心的なメッセージは、IDisposableオブジェクトの受け渡しを、他のオブジェクトの厳密に制限されたセット(「集約」)に制限する必要があるということです。その集合境界の外側にあるオブジェクトは、IDisposableへの直接参照を取得してはなりません。これにより、すべてのオブジェクトの大部分、つまり集合体の外側にあるオブジェクトがオブジェクトをDisposeする可能性があるかどうかを心配する必要がなくなるため、作業が大幅に簡素化されます。あなたがする必要があるのは、オブジェクトinside境界のすべてがそれを処分する責任がある人を知っていることを確認することです。これは、通常一緒に実装し、維持するように注意するので、解決するのに十分簡単な問題です。集合体の境界は適度に「タイト」です。

IDisposableオブジェクトの作成者もそれを破棄する必要があるという提案はどうですか?

このガイドラインは合理的に聞こえ、魅力的な対称性がありますが、それだけでは実際には機能しないことがよくあります。おそらく、「IDisposableオブジェクトへの参照を他のオブジェクトに渡さないでください」と言うのと同じ意味です。そうするとすぐに、受信オブジェクトの所有権を引き受け、知らないうちに破棄するリスクがあるためです。 。

この経験則に明らかに違反している.NET基本クラスライブラリ(BCL)の2つの主要なインターフェイスタイプであるIEnumerable<T>IObservable<T>を見てみましょう。どちらも基本的にIDisposableオブジェクトを返すファクトリです。

  • IEnumerator<T> IEnumerable<T>.GetEnumerator()
    IEnumerator<T>IDisposableから継承することを忘れないでください。)

  • IDisposable IObservable<T>.Subscribe(IObserver<T> observer)

どちらの場合も、callerは返されたオブジェクトを破棄することが期待されます。おそらく、オブジェクトファクトリの場合はガイドラインが意味をなさないでしょう...おそらくrequesterIDisposableの直接の作成者)ではなく、それを解放します。

ちなみに、この例は、上記で概説した集約ソリューションの制限も示しています。IEnumerable<T>IObservable<T>はどちらも本質的に一般的すぎて、集約の一部にはなりません。集合体は通常、非常にドメイン固有です。

その他のリソースとアイデア:

  • UMLでは、オブジェクト間の「has a」関係は、集約(空のひし形)または構成(塗りつぶされたひし形)の2つの方法でモデル化できます。構成は、含まれている/参照されているオブジェクトの存続期間がコンテナ/参照元の存続期間で終わるという点で集約とは異なります。あなたの最初の質問は集約(「譲渡可能な所有権」)を暗示していましたが、私は主に合成を使用するソリューション(「固定所有権」)に向けました。 「オブジェクトコンポジション」に関するウィキペディアの記事 を参照してください。

  • Autofac (。NET IoC コンテナ)は、この問題を2つの方法で解決します。通信するか、いわゆる 関係タイプ を使用するか、- Owned<T>IDisposableの所有権を取得します。または、Autofacではライフタイムスコープと呼ばれる作業単位の概念を介して。

  • 後者に関して、Autofacの作成者であるNicholas Blumhardtは、「IDisposableandownership」のセクションを含む "Autofac Lifetime Primer" を書いています。記事全体は、.NETの所有権と存続期間の問題に関する優れた論文です。 Autofacに興味がない人にも読むことをお勧めします。

  • C++では、 Resource Acquisition Is Initialization(RAII) イディオム(一般)および スマートポインタータイプ (特に)は、プログラマーがオブジェクトの存続期間と所有権の問題を正しく理解するのに役立ちます。残念ながら、これらは.NETに転送できません。これは、.NETには決定論的なオブジェクト破壊に対するC++のエレガントなサポートがないためです。

  • スタックオーバーフローに関する質問への この回答 も参照してください 「異なる実装ニーズをどのように説明するか?」 、これは(私が正しく理解している場合)私のと同様の考えに従います集計ベースの回答:IDisposableの周囲に粗粒度のコンポーネントを構築して、その中に完全に含まれる(そしてコンポーネントのコンシューマーから隠される)ようにします。

36
stakx

一般に、Disposableオブジェクトを処理すると、生涯の所有権が重要なポイントであるマネージコードの理想的な世界にはいなくなります。したがって、どのオブジェクトが使い捨てオブジェクトを論理的に「所有」しているか、またはその寿命に責任があるかを考慮する必要があります。

一般に、メソッドに渡されたばかりの使い捨てオブジェクトの場合、いいえと言います。あるオブジェクトが別のオブジェクトの所有権を引き継いで、それを使用して実行されることは非常にまれであるため、メソッドはオブジェクトを破棄しないでください。同じ方法。このような場合、発信者は処分の責任を負う必要があります。

メンバーデータについて話すときに、「はい、常に破棄します」または「いいえ、決して破棄しません」という自動応答はありません。むしろ、それぞれの特定の場合のオブジェクトについて考え、「このオブジェクトは使い捨てオブジェクトの寿命に責任があるのか​​」と自問する必要があります。

経験則では、使い捨ての作成を担当するオブジェクトがそれを所有しているため、後で廃棄する責任があります。所有権の譲渡がある場合、これは当てはまりません。例えば:

public class Foo
{
    public MyClass BuildClass()
    {
        var dispObj = new DisposableObj();
        var retVal = new MyClass(dispObj);
        return retVal;
    }
}

Fooは明らかにdispObjの作成に責任がありますが、所有権をMyClassのインスタンスに渡します。

8
Greg D

これは 私の前の答え へのフォローアップです。私が別の投稿をしている理由については、最初のコメントを参照してください。

私の以前の答えは1つ正しかった:IDisposableには、そのDispose- ingを1回だけ担当する排他的な「所有者」が必要です。IDisposableの管理オブジェクトは、アンマネージコードシナリオでのメモリ管理に非常に匹敵するようになります。

.NETの前身のテクノロジであるコンポーネントオブジェクトモデル(COM)は、次を使用しました メモリ管理のプロトコル オブジェクト間の責任:

  • 「パラメータ内は、呼び出し元が割り当てて解放する必要があります。
  • 「アウトパラメータは、呼び出された人が割り当てる必要があります。呼び出し元が解放します[…]。
  • 「In-outパラメータは最初に呼び出し元によって割り当てられ、次に必要に応じて呼び出されたものによって解放および再割り当てされます。outパラメータの場合と同様に、呼び出し元は最終的な戻り値を解放する責任があります。」

(エラーの場合には追加のルールがあります。詳細については、上記のリンク先のページを参照してください。)

これらのガイドラインをIDisposableに適合させるとしたら、次のように規定できます…

IDisposableの所有権に関するルール:

  1. IDisposableが通常のパラメーターを介してメソッドに渡される場合、所有権の譲渡はありません。呼び出されたメソッドはIDisposableを使用できますが、それをDisposeしてはなりません(所有権を譲渡することもできません。以下のルール4を参照してください)。
  2. IDisposableoutパラメーターまたは戻り値を介してメソッドから返されると、所有権はメソッドからその呼び出し元に転送されます。呼び出し元はそれをDisposeする必要があります(または同じ方法でIDisposableの所有権を譲渡します)。
  3. IDisposablerefパラメータを介してメソッドに与えられると、その所有権はそのメソッドに譲渡されます。このメソッドは、IDisposableをローカル変数またはオブジェクトフィールドにコピーしてから、refパラメーターをnullに設定する必要があります。

上記から、おそらく重要なルールが1つあります。

  1. 所有権がない場合は、譲渡してはなりません。つまり、通常のパラメーターを介してIDisposableオブジェクトを受け取った場合は、同じオブジェクトを_ref IDisposable_パラメーターに入れたり、戻り値またはoutパラメーターを介して公開したりしないでください。

例:

_sealed class LineReader : IDisposable
{
    public static LineReader Create(Stream stream)
    {
        return new LineReader(stream, ownsStream: false);
    }

    public static LineReader Create<TStream>(ref TStream stream) where TStream : Stream
    {
        try     { return new LineReader(stream, ownsStream: true); }
        finally { stream = null;                                   }
    }

    private LineReader(Stream stream, bool ownsStream)
    {
        this.stream = stream;
        this.ownsStream = ownsStream;
    }

    private Stream stream; // note: must not be exposed via property, because of rule (2)
    private bool ownsStream;

    public void Dispose()
    {
        if (ownsStream)
        {
            stream?.Dispose();
        }
    }

    public bool TryReadLine(out string line)
    {
        throw new NotImplementedException(); // read one text line from `stream` 
    }
}
_

このクラスには2つの静的ファクトリメソッドがあるため、クライアントは所有権を保持するか渡すかを選択できます。

  • 通常のパラメータを介してStreamオブジェクトを受け入れます。これは、所有権が引き継がれないことを発信者に通知します。したがって、呼び出し元はDisposeする必要があります。

    _using (var stream = File.OpenRead("Foo.txt"))
    using (var reader = LineReader.Create(stream))
    {
        string line;
        while (reader.TryReadLine(out line))
        {
            Console.WriteLine(line);
        }
    }
    _
  • Streamパラメーターを介してrefオブジェクトを受け入れるもの。これは、所有権が譲渡されることを呼び出し元に通知するため、呼び出し元はDisposeする必要はありません。

    _var stream = File.OpenRead("Foo.txt");
    using (var reader = LineReader.Create(ref stream))
    {
        string line;
        while (reader.TryReadLine(out line))
        {
            Console.WriteLine(line);
        }
    }
    _

    興味深いことに、streamusing変数として宣言されている場合:using (var stream = …)using変数をrefパラメーターとして渡すことができないため、コンパイルは失敗します。したがって、C#コンパイラはこの特定の場合にルールを適用するのに役立ちます。

最後に、_File.OpenRead_は、戻り値を介してIDisposableオブジェクト(つまり、Stream)を返すメソッドの例であるため、返されたストリームの所有権は呼び出し元に転送されることに注意してください。

不利益:

このパターンの主な欠点は、AFAIKが(まだ)誰も使用していないことです。したがって、上記のルールに従わないAPI(たとえば、.NET Framework基本クラスライブラリ)を操作する場合でも、ドキュメントを読んで、DisposeオブジェクトでIDisposableを呼び出す必要があるユーザーを見つける必要があります。

7
stakx

.NETプログラミングについてよく知る前にやろうと決心したことの1つは、それでも良い考えのようです。IDisposableを受け入れるコンストラクターを用意し、オブジェクトの所有権が同様に転送されます。完全にusingステートメントのスコープ内に存在できるオブジェクトの場合、これは一般的にそれほど重要ではありません(外側のオブジェクトは、内側のオブジェクトのUsingブロックのスコープ内に配置されるため、外側のオブジェクトは必要ありません。内側のものを処分することに反対します;実際、そうしないことが必要かもしれません)。ただし、このようなセマンティクスは、外部オブジェクトがインターフェイスまたは基本クラスとして、内部オブジェクトの存在を知らないコードに渡される場合に不可欠になる可能性があります。その場合、内側のオブジェクトは外側のオブジェクトが破壊されるまで生きているはずであり、外側のオブジェクトが死ぬと内側のオブジェクトを知っているのは外側のオブジェクト自体であるため、外側のオブジェクトは破壊できる必要があります内側のもの。

それ以来、私はいくつかの追加のアイデアを持っていますが、それらを試していません。私は他の人がどう思うか興味があります:

  1. IDisposableオブジェクトの参照カウントラッパー。これを行うための最も自然なパターンを実際には理解していませんが、オブジェクトがインターロックされたインクリメント/デクリメントで参照カウントを使用し、(1)オブジェクトを操作するすべてのコードがそれを正しく使用し、(2)循環参照がない場合オブジェクトを使用して作成されている場合、最後の使用がさようならになると破棄される共有IDisposableオブジェクトを持つことが可能であると思います。おそらく、パブリッククラスはプライベート参照カウントクラスのラッパーであり、同じベースインスタンスの新しいラッパーを作成するコンストラクターまたはファクトリメソッドをサポートする必要があります(インスタンスの参照カウントを1つ増やす) )。または、ラッパーが破棄された場合でもクラスをクリーンアップする必要があり、クラスに定期的なポーリングルーチンがある場合、クラスはラッパーにWeakReferencesのリストを保持し、少なくともいくつかがそれらのうち、まだ存在しています。
  2. IDisposableオブジェクトのコンストラクターに、オブジェクトが最初に破棄されたときに呼び出すデリゲートを受け入れさせます(IDisposableオブジェクトは、isDisposedフラグでInterlocked.Exchangeを使用して、確実に破棄されるようにする必要があります)一度だけ)。そのデリゲートは、ネストされたオブジェクトの破棄を処理できます(おそらく、他の誰かがまだそれらを保持しているかどうかを確認するためのチェックを行います)。

それらのどちらかが良いパターンのように見えますか?

2
supercat