web-dev-qa-db-ja.com

イベントハンドラーの参照をクリーンアップするためのベストプラクティスは何ですか?

私はしばしば次のようなコードを書いています。

        if (Session != null)
        {
            Session.KillAllProcesses();
            Session.AllUnitsReady -= Session_AllUnitsReady;
            Session.AllUnitsResultsPublished -= Session_AllUnitsResultsPublished;
            Session.UnitFailed -= Session_UnitFailed;
            Session.SomeUnitsFailed -= Session_SomeUnitsFailed;
            Session.UnitCheckedIn -= Session_UnitCheckedIn;
            UnattachListeners();
        }

ターゲット(セッション)で登録したすべてのイベントサブスクリプションをクリーンアップして、GCがセッションを自由に破棄できるようにすることを目的としています。ただし、IDisposableを実装するクラスについて同僚と話し合ったところ、これらのクラスは次のようにクリーンアップを実行する必要があると彼は信じていました。

    /// <summary>
    /// Disposes the object
    /// </summary>
    public void Dispose()
    {
        SubmitRequested = null; //frees all references to the SubmitRequested Event
    }

どちらか一方を優先する理由はありますか?これを完全に回避するためのより良い方法はありますか? (どこでも弱い参照イベントは別として)

私が本当に見たいのは、イベントを発生させるための安全な呼び出しパターンに似た、安全で再現可能なものです。簡単にクリーンアップできるように、イベントにアタッチするたびに覚えておくこと。

32
Firoso

ハンドラをSessionイベントから登録解除すると、GCがSessionオブジェクトを収集できるようになるというのは誤りです。以下は、イベントの参照チェーンを示す図です。

--------------      ------------      ----------------
|            |      |          |      |              |
|Event Source|  ==> | Delegate |  ==> | Event Target |
|            |      |          |      |              |
--------------      ------------      ----------------

したがって、あなたの場合、イベントソースはSessionオブジェクトです。しかし、どのクラスがハンドラーを宣言したかについては言及していないので、イベントターゲットが誰であるかはまだわかりません。 2つの可能性を考えてみましょう。イベントターゲットは、ソースを表す同じSessionオブジェクトにすることも、完全に別のクラスにすることもできます。どちらの場合でも、通常の状況では、Sessionは、そのイベントへのハンドラーが登録されたままであっても、別の参照がない限り収集されます。これは、デリゲートにイベントソースへの参照が含まれていないためです。イベントターゲットへの参照のみが含まれます。

次のコードを検討してください。

public static void Main()
{
  var test1 = new Source();
  test1.Event += (sender, args) => { Console.WriteLine("Hello World"); };
  test1 = null;
  GC.Collect();
  GC.WaitForPendingFinalizers();

  var test2 = new Source();
  test2.Event += test2.Handler;
  test2 = null;
  GC.Collect();
  GC.WaitForPendingFinalizers();
}

public class Source()
{
  public event EventHandler Event;

  ~Source() { Console.WriteLine("disposed"); }

  public void Handler(object sender, EventArgs args) { }
}

「disposed」がコンソールに2回出力され、イベントの登録を解除せずに両方のインスタンスが収集されたことを確認できます。 test2によって参照されるオブジェクトが収集される理由は、イベントを介してそれ自体への参照があったとしても、参照グラフ内の分離されたエンティティのままであるためです(test2がnullに設定されると)。 。

ここで、問題が発生するのは、イベントターゲットの有効期間をイベントソースよりも短くしたい場合です。その場合、イベントの登録を解除するする必要があります。これを示す次のコードを検討してください。

public static void Main()
{
  var parent = new Parent();
  parent.CreateChild();
  parent.DestroyChild();
  GC.Collect();
  GC.WaitForPendingFinalizers();
}

public class Child
{
  public Child(Parent parent)
  {
    parent.Event += this.Handler;
  }

  private void Handler(object sender, EventArgs args) { }

  ~Child() { Console.WriteLine("disposed"); }
}

public class Parent
{
  public event EventHandler Event;

  private Child m_Child;

  public void CreateChild()
  {
    m_Child = new Child(this);
  }

  public void DestroyChild()
  {
    m_Child = null;
  }
}

「disposed」がコンソールに出力されず、メモリリークの可能性があることがわかります。これは特に対処が難しい問題です。 IDisposableChildを実装しても問題は解決しません。これは、呼び出し元が正しく再生して実際にDisposeを呼び出す保証がないためです。

答え

イベントソースがIDisposableを実装している場合、実際には新しいものを購入していません。これは、イベントソースがルート化されなくなった場合、イベントターゲットもルート化されなくなるためです。

イベントターゲットがIDisposableを実装している場合、イベントソースからそれ自体をクリアできますが、Disposeが呼び出される保証はありません。

Disposeからのイベントの登録解除が間違っていると言っているのではありません。私のポイントは、クラス階層がどのように定義されているかを実際に調べて、メモリリークの問題が存在する場合でも、それをどのようにして最善の方法で回避できるかを検討する必要があるということです。

42
Brian Gideon

IDisposableの実装には、手動による方法よりも2つの利点があります。

  1. それは標準であり、コンパイラはそれを特別に扱います。つまり、コードを読み取るすべての人が、IDisposableが実装されているのを見た瞬間にそれが何であるかを理解しているということです。
  2. .NET C#およびVBは、usingステートメントを介してIDisposableを操作するための特別な構成を提供します。

それでも、これがシナリオで役立つかどうかは疑問です。オブジェクトを安全に破棄するには、try/catch内のfinallyブロックでオブジェクトを破棄する必要があります。あなたが説明しているように見える場合、オブジェクトの削除時に(つまり、そのスコープの最後:最終ブロックで)、Sessionがこれを処理するか、Sessionを呼び出すコードが必要になる場合があります。その場合、セッションもIDisposableを実装する必要があり、これは一般的な概念に従います。 IDisposable.Disposeメソッド内では、使い捨てであるすべてのメンバーをループし、それらを破棄します。

編集する

あなたの最新のコメントは私に私の答えを考え直させ、いくつかの点をつなげることを試みます。セッションがGCによって使い捨て可能であることを確認したいとします。デリゲートへの参照が同じクラス内からのものである場合は、デリゲートを解除する必要はまったくありません。それらが別のクラスからのものである場合、それらの購読を解除する必要があります。上記のコードを見ると、Sessionを使用するすべてのクラスにそのコードブロックを記述し、プロセスのある時点でクリーンアップしているようです。

セッションを解放する必要がある場合は、より直接的な方法があります。これは、クラスの呼び出しが、サブスクライブ解除プロセスを正しく処理する必要がないためです。単純なリフレクションを使用してすべてのイベントをループし、すべてをnullに設定します(同じ効果に到達するための代替アプローチを検討できます)。

「ベストプラクティス」を求めるため、このメソッドをIDisposableと組み合わせ、IDisposable.Dispose()内にループを実装する必要があります。このループに入る前に、もう1つのイベントDisposingを呼び出します。リスナーは、何かを自分でクリーンアップする必要がある場合に使用できます。 IDisposableを使用するときは、その警告に注意してください。その この簡単に説明されたパターン が一般的な解決策です。

5
Abel

私の好みは使い捨てで寿命を管理することです。 Rxには、以下を実行できるいくつかの使い捨て拡張機能が含まれています。

Disposable.Create(() => {
                this.ViewModel.Selection.CollectionChanged -= SelectionChanged;
            })

次に、これをある範囲のGroupDisposableに保存し、正しいスコープまで存続させると、準備は完了です。

使い捨てとスコープで寿命を管理していない場合は、.netで非常に普及しているパターンになるため、調査する価値があります。

3
DanH

Vb.net WithEventsキーワードを使用して自動的に生成されるイベント処理パターンは、かなり適切なパターンです。 VBコード(大まかに):

 WithEvents myPort As SerialPort 
 
 Sub GotData(Sender As Object、e as DataReceivedEventArgs)Handles myPort.DataReceived 
 Sub SawPinChange(Sender As Object、e as PinChangedEventArgs) myPort.PinChanged 
を処理します

同等のものに翻訳されます:

 SerialPort _myPort; 
 SerialPort myPort 
 {get {return _myPort; } 
セット{
 if(_myPort!= null)
 {
 _myPort.DataReceived-= GotData; 
 _myPort.PinChanged-= SawPinChange; 
} 
 _myPort = value; 
 if(_myPort!= null)
 {
 _myPort.DataReceived + = GotData; 
 _myPort.PinChanged + = SawPinChange; 
} 
} 
} 

これは従うべき妥当なパターンです。このパターンを使用する場合、Disposenullに、関連するイベントを持つすべてのプロパティを設定し、イベントのサブスクライブ解除を処理します。

処理が少しでも自動化されて、処理が確実に処理されるようにするには、プロパティを次のように変更します。

 Action <myType> myCleanups; //クラス全体で一度だけ
 SerialPort _myPort; 
 static void cancel_myPort(myType x){x.myPort = null;} 
 SerialPort myPort 
 {get {_myPortを返す; } 
セット{
 if(_myPort!= null)
 {
 _myPort.DataReceived-= GotData; 
 _myPort.PinChanged-= SawPinChange; 
 myCleanups-= cancel_myPort; 
} 
 _myPort = value; 
 if(_myPort!= null)
 {
 myCleanups + = cancel_myPort; 
 _myPort.DataReceived + = GotData; 
 _myPort.PinChanged + = SawPinChange; 
} 
} 
} 
 //後で、Disposeで... 
 myCleanups(this); //エンキューされたクリーンアップを実行します

静的デリゲートをmyCleanupsにフックするということは、myClassのインスタンスが多数ある場合でも、システム全体で各デリゲートのコピーが1つあれば十分であることを意味します。おそらくインスタンス数の少ないクラスでは大したことではないかもしれませんが、クラスが何千回もインスタンス化される場合は重要になる可能性があります。

3
supercat

独自のクラスで最も一般的に使用されるイベントをグローバル化し、それらのインターフェイスを継承するなどの単純なタスクを実行すると、開発者がイベントの追加や削除にイベントプロパティなどのメソッドを使用できるようになります。クラス内で、カプセル化が発生するかどうか、クリーンアップするかどうかは、以下の例のようなものを使用して開始できます。

例えば。

#region Control Event Clean up
private event NotifyCollectionChangedEventHandler CollectionChangedFiles
{
    add { FC.CollectionChanged += value; }
    remove { FC.CollectionChanged -= value; }
}
#endregion Control Event Clean up

これは、プロパティADD REMOVEの他の用途への追加フィードバックを提供する記事です。 http://msdn.Microsoft.com/en-us/library/8843a9ch.aspx

1

DanHからの答えはほとんどありますが、1つの重要な要素が欠けています。

これが常に正しく機能するには、変数が変更された場合に備えて、まず変数のローカルコピーを取得する必要があります。基本的に、暗黙的にキャプチャされたクロージャを適用する必要があります。

List<IDisposable> eventsToDispose = new List<IDisposable>();

var handlerCopy = this.ViewModel.Selection;
eventsToDispose.Add(Disposable.Create(() => 
{
    handlerCopy.CollectionChanged -= SelectionChanged;
}));

後で、これを使用してすべてのイベントを破棄できます。

foreach(var d in eventsToDispose)
{ 
    d.Dispose();
}

短くしたい場合:

eventsToDispose.ForEach(o => o.Dispose());

さらに短くしたい場合は、IListをCompositeDisposableで置き換えることができます。これは、裏でまったく同じことです。

次に、これですべてのイベントを破棄できます。

eventsToDispose.Dispose();
0
Contango