9

編集: Joel Coehoornsの優れた回答の後、より具体的にする必要があることを理解したので、理解しようとしていることに近づけるようにコードを変更しました...

イベント:私が理解しているように、バックグラウンドでのイベントは、イベントが発生したときに実行されるEventHandlers(別名デリゲート)の「コレクション」です。つまり、私にとっては、オブジェクトYにイベントEがあり、オブジェクトXがイベントYEにサブスクライブしている場合、YはXを参照することになります。これは、 YがXにあるメソッドを実行する必要があるため、Xを収集できないためです。私が理解していること。

//Creates reference to this (b) in a.
a.EventHappened += new EventHandler(this.HandleEvent);

しかし、それはJoelCoehoornが言っていることではありません...

ただし、イベントには問題があり、イベントのあるタイプでIDisposableを使用したい場合があります。問題は、タイプXが別のタイプYのイベントをサブスクライブすると、XがYへの参照を持つようになることです。この参照により、Yが収集されなくなります。

XがYをどのように参照するのかわかりません???

私の例を少し修正して、私のケースをより詳しく説明しました。

class Service //Let's say it's windows service that must be 24/7 online
{       
    A _a;

    void Start()
    {
       CustomNotificationSystem.OnEventRaised += new EventHandler(CustomNotificationSystemHandler)
       _a = new A();

       B b1 = new B(_a);
       B b2 = new B(_a);
       C c1 = new C(_a);
       C c2 = new C(_a);
    }

    void CustomNotificationSystemHandler(args)
    {

        //_a.Dispose(); ADDED BY **EDIT 2***
        a.Dispose();

        _a = new A();
        /*
        b1,b2,c1,c2 will continue to exists as is, and I know they will now subscribed
        to previous instance of _a, and it's OK by me, BUT in that example, now, nobody
        references the previous instance of _a (b not holds reference to _a) and by my
        theory, previous instance of _a, now may be collected...or I'm missing
        something???
        */
    }

}  

class A : IDisposable
        {
           public event EventHandler EventHappened;
        }

        class B
        {          
           public B(A a) //Class B does not stores reference to a internally.
           {
              a.EventHappened += new EventHandler(this.HandleEventB);
           }

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

        class C
        {          
           public C(A a) //Class B not stores reference to a internally.
           {
              a.EventHappened += new EventHandler(this.HandleEventC);
           }

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

編集2:わかりました。サブスクライバーがパブリッシャーイベントをサブスクライブするときに、サブスクライバー内のパブリッシャーへの参照が作成されないことは明らかです。(EventHandlerを介して)作成されたパブリッシャーからサブスクライバーへの参照のみ...この場合、サブスクライバーの前にGCによってパブリッシャーが収集された場合(サブスクライバーの存続期間はパブリッシャーよりも長い)、問題はありません。

しかし...私が知っているように、GCがいつパブリッシャーを収集するかは保証されていないため、理論的には、サブスクライバーの有効期間がパブリッシャーよりも長い場合でも、サブスクライバーは収集に合法である可能性がありますが、パブリッシャーはまだ収集されません(私は最も近いGCサイクル内で、GCが最初にパブリッシャーを収集し、次にサブスクライバーを収集するのに十分スマートであるかどうかを確認します。

とにかく、そのような場合、私のサブスクライバーはパブリッシャーへの直接参照を持たず、イベントのサブスクライブを解除できないため、パブリッシャーにIDisposableを実装させて、彼へのすべての参照を削除する前にIDisposableを破棄したいと思います(私の例)。

AND AGAINサブスクライバーへのすべての参照をクリアするために、パブリッシャーのdisposeメソッドに何を書き込む必要がありますか?EventHappened-=nullである必要があります。またはEventHappened=null; またはそのような方法でそれを行う方法はありません、そして私は以下のようなものを作る必要があります???

public event EventHandler EventHappened
   {
      add 
      {
         eventTable["EventHappened"] = (EventHandler)eventTable["EventHappened"] + value;
      }
      remove
      {
         eventTable["EventHappened"] = (EventHandler)eventTable["EventHappened"] - value; 
      }
   }
4

7 に答える 7

13

オブジェクト B の存続期間は A よりも長いため、A はより早く破棄される可能性があります。

「廃棄」と「回収」を混同しているように聞こえますか?オブジェクトの破棄は、メモリやガベージ コレクションとは関係ありません。すべてが明確であることを確認するために、2 つのシナリオを分けて、最後にイベントに移ります。

コレクション:

A が親 B になる前に、A が収集されることは決してありません。B が到達可能である限り、A も同様です。A は非公開ですが、B 内の任意のコードから到達可能であり、B が到達可能である限り、 A は到達可能と見なされます。これは、ガベージ コレクターがユーザーが処理を完了したことを確実に認識せず、B を収集しても安全になるまで A を収集しないことを意味します。これは、明示的に GC.Collect() などを呼び出した場合でも当てはまります。オブジェクトが到達可能である限り、収集されません。

廃棄:

ここで IDisposable を実装している理由もわかりません (メモリやガベージ コレクションとは関係ありません) が、アンマネージ リソースが表示されないという点については、当面は疑っておいてください。

いつでも A を破棄することを妨げるものは何もありません。a.Dispose() を呼び出すだけで完了です。.Net フレームワークが自動的に Dispose() を呼び出す唯一の方法は、usingブロックの最後です。Dispose() は、オブジェクトのファイナライザーの一部として実行しない限り、ガベージ コレクション中には呼び出されません (ファイナライザーについては後で詳しく説明します)。

IDisposable を実装するときは、この型をすぐに破棄する必要がある (場合によっては "必須" でさえある) というメッセージをプログラマーに送信しています。IDisposable オブジェクトには 2 つの正しいパターンがあります (パターンには 2 つのバリエーションがあります)。最初のパターンは、型自体を using ブロックで囲むことです。これが不可能な場合 (たとえば、型が別の型のメンバーであるコードなど)、2 番目のパターンは、親型も IDisposable を実装する必要があるため、それ自体を using ブロックに含めることができます。 Dispose() は、型の Dispose() を呼び出すことができます。これらのパターンのバリエーションは、using ブロックの代わりに try/finally ブロックを使用することです。この場合、finally ブロックで Dispose() を呼び出します。

次に、ファイナライザーに進みます。ファイナライザーを実装する必要があるの、アンマネージ リソースを生成する IDisposable 型の場合だけです。したがって、たとえば、上記のタイプ A が SqlConnection のようなクラスをラップするだけの場合、SqlConnection 自体のファイナライザーが必要なクリーンアップを処理するため、ファイナライザーは必要ありません。ただし、タイプ A がまったく新しい種類のデータベース エンジンへの接続を実装している場合は、オブジェクトが収集されたときに接続が確実に閉じられるように、ファイナライザーが必要になります。ただし、タイプ B はタイプ A を管理/ラップしますが、タイプ A は接続のファイナライズを処理するため、ファイナライザーは必要ありません。

イベント:

技術的には、イベントは引き続きマネージ コードであり、破棄する必要はありません。ただし、イベントを持つタイプで IDisposable を使用したい場合があるなど、イベントには問題があります。問題は、タイプ X が別のタイプ Y のイベントをサブスクライブすると、Y が X への参照を持つようになることです。この参照により、X が収集されなくなる可能性があります。Y の存続期間が X よりも長いと予想した場合、ここで問題が発生する可能性があります。特に、Y の存続期間が多くの X と比較して非常に長い場合は、時間の経過とともに発生します。

これを回避するために、プログラマーは Y 型を実装して IDisposable を実装することがあります。Dispose() メソッドの目的は、すべてのイベントのサブスクライブを解除して、サブスクライブしているオブジェクトも収集できるようにすることです。技術的には、これは Dispose() パターンの目的ではありませんが、十分に機能するので、それについて議論するつもりはありません。このパターンをイベントで使用する場合、次の 2 つのことを知っておく必要があります。

  1. これが IDisposable を実装する唯一の理由である場合は、ファイナライザーは必要ありません
  2. あなたのタイプのインスタンスには、まだ using または try/finally ブロックが必要です。または、何も得ていません。そうしないと、Dispose() は呼び出されず、オブジェクトはまだ収集できません。

この場合、タイプ A はタイプ B に対してプライベートであるため、タイプ B のみが A のイベントをサブスクライブできます。'a' は型 B のメンバーであるため、B に到達できなくなるまで、どちらもガベージ コレクションの対象にはなりません。B に到達できなくなると、両方に到達できなくなり、イベント サブスクリプション参照はカウントされなくなります。つまり、A のイベントによって B に保持された参照は、B の収集を妨げません。ただし、他の場所で A 型を使用する場合でも、A に IDisposable を実装して、イベントのサブスクライブを確実に解除したい場合があります。その場合は、A のインスタンスが using または try/finally ブロックで囲まれるように、パターン全体に従ってください。

于 2012-07-16T16:34:38.313 に答える
5

サンプルコードに私のコメントを追加しました。

class A : IDisposable
{
   public event EventHandler EventHappened
   {
      add 
      {
         eventTable["EventHappened"] = (EventHandler)eventTable["EventHappened"] + value;
      }
      remove
      {
         eventTable["EventHappened"] = (EventHandler)eventTable["EventHappened"] - value; 
      }
   }

   public void Dispose()
   {
      //Amit: If you have only one event 'EventHappened', 
      //you can clear up the subscribers as follows

      eventTable["EventHappened"] = null;

      //Amit: EventHappened = null will not work here as it is 
      //just a syntactical sugar to clear the compiler generated backing delegate.
      //Since you have added 'add' and 'remove' there is no compiler generated 
      //delegate to clear
      //
      //Above was just to explain the concept.
      //If eventTable is a dictionary of EventHandlers
      //You can simply call 'clear' on it.
      //This will work even if there are more events like EventHappened          
   }
}

class B
{          
   public B(A a)
   {
      a.EventHappened += new EventHandler(this.HandleEventB);

      //You are absolutely right here.
      //class B does not store any reference to A
      //Subscribing an event does not add any reference to publisher
      //Here all you are doing is calling 'Add' method of 'EventHappened'
      //passing it a delegate which holds a reference to B.
      //Hence there is a path from A to B but not reverse.
   }

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

class C
{          
   public C(A a)
   {
      a.EventHappened += new EventHandler(this.HandleEventC);
   }

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

class Service
{       
    A _a;

    void Start()
    {
       CustomNotificationSystem.OnEventRaised += new EventHandler(CustomNotificationSystemHandler)

       _a = new A();

       //Amit:You are right all these do not store any reference to _a
       B b1 = new B(_a);
       B b2 = new B(_a);
       C c1 = new C(_a);
       C c2 = new C(_a);
    }

    void CustomNotificationSystemHandler(args)
    {

        //Amit: You decide that _a has lived its life and must be disposed.
        //Here I assume you want to dispose so that it stops firing its events
        //More on this later
        _a.Dispose();

        //Amit: Now _a points to a brand new A and hence previous instance 
        //is eligible for collection since there are no active references to 
        //previous _a now
        _a = new A();
    }    
}

b1、b2、c1、c2 はそのまま存在し続け、_a の以前のインスタンスをサブスクライブするようになることはわかっています。私はそれで問題ありませんが、その例では、今では誰も _a の以前のインスタンスを参照していません (b はありません)。 _a) への参照を保持し、私の理論によれば、_a の以前のインスタンスが収集される可能性があります...または何か不足していますか???

上記のコードのコメントで説明したように、ここに何も欠けていません:)

しかし...私が知っているように、GCがパブリッシャーをいつ収集するかは保証されていないため、理論的には、サブスクライバーの寿命がパブリッシャーよりも長い場合でも、サブスクライバーは収集が合法である可能性がありますが、パブリッシャーはまだ収集されていません(私は知りません.最も近い GC サイクル内で、GC がパブリッシャーを最初に収集し、次にサブスクライバーを収集するほどスマートになるかどうかはわかりません。

パブリッシャーはサブスクライバーを参照するため、サブスクライバーがパブリッシャーよりも先にコレクションの対象になることは決してありませんが、その逆の場合もあります。パブリッシャーがサブスクライバーより先に回収されれば、おっしゃる通り問題ありません。サブスクライバーがパブリッシャーよりも低い GC 世代に属している場合、パブリッシャーはサブスクライバーへの参照を保持しているため、GC はサブスクライバーを到達可能として扱い、収集しません。両者が同世代の場合はまとめて集計します。

サブスクライバーはパブリッシャーを直接参照しておらず、イベントのサブスクライブを解除できないため、パブリッシャーに IDisposable を実装させたいと考えています。

一部の提案に反して、オブジェクトが不要になったことを決定論的に確信している場合は、dispose を実装することをお勧めします。オブジェクト参照を単に更新するだけでは、オブジェクトの公開イベントが停止するとは限りません。

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

class MainClass
{
    public static Publisher Publisher;

    static void Main()
    {
        Publisher = new Publisher();

        Thread eventThread = new Thread(DoWork);
        eventThread.Start();

        Publisher.StartPublishing(); //Keep on firing events
    }

    static void DoWork()
    {
        var subscriber = new Subscriber();
        subscriber = null; 
        //Subscriber is referenced by publisher's SomeEvent only
        Thread.Sleep(200);
        //We have waited enough, we don't require the Publisher now
        Publisher = null;
        GC.Collect();
        //Even after GC.Collect, publisher is not collected even when we have set Publisher to null
        //This is because 'StartPublishing' method is under execution at this point of time
        //which means it is implicitly reachable from Main Thread's stack (through 'this' pointer)
        //This also means that subscriber remain alive
        //Even when we intended the Publisher to stop publishing, it will keep firing events due to somewhat 'hidden' reference to it from Main Thread!!!!
    }
}

internal class Publisher
{
    public void StartPublishing()
    {
        Thread.Sleep(100);
        InvokeSomeEvent(null);
        Thread.Sleep(100);
        InvokeSomeEvent(null);
        Thread.Sleep(100);
        InvokeSomeEvent(null);
        Thread.Sleep(100);
        InvokeSomeEvent(null);
    }

    public event EventHandler SomeEvent;

    public void InvokeSomeEvent(object e)
    {
        EventHandler handler = SomeEvent;
        if (handler != null)
        {
            handler(this, null);
        }
    }

    ~Publisher()
    {
        Console.WriteLine("I am never Printed");
    }
}

internal class Subscriber
{
    public Subscriber()
    {
        if(MainClass.Publisher != null)
        {
            MainClass.Publisher.SomeEvent += PublisherSomeEvent;
        }
    }

    void PublisherSomeEvent(object sender, EventArgs e)
    {
        if (MainClass.Publisher == null)
        {
            //How can null fire an event!!! Raise Exception
            throw new Exception("Booooooooommmm");
            //But notice 'sender' is not null
        }
    }
}

上記のコードを実行すると、多くの場合、'Booooooooommmm' が表示されます。したがって、イベント パブリッシャは、寿命が尽きたと確信したときにイベントの発生を停止する必要があるという考えです。

これは、Dispose メソッドを使用して実行できます。

これを実現するには、次の 2 つの方法があります。

  1. フラグ「IsDisposed」を設定し、イベントを発生させる前に確認してください。
  2. イベント サブスクライバー リストをクリアします (コード内の私のコメントで提案されているように)。

2 の利点は、サブスクライバーへの参照を解放することで、コレクションが可能になることです (前述のように、パブリッシャーがガベージであるが上位世代に属している場合でも、下位世代のサブスクライバーのコレクションが長引く可能性があります)。

確かに、パブリッシャーの「隠れた」到達可能性により、実証された動作を経験することは非常にまれですが、2 の利点は明らかであり、すべてのイベント パブリッシャー、特に長生きするイベント パブリッシャー (シングルトンの人!! !)。これ自体、Dispose を実装して 2 を使用する価値があります。

于 2012-07-20T11:58:39.540 に答える
4

他のいくつかの回答が主張していることとは反対に、パブリッシャーのGCライフタイムがサブスクライバーの有効ライフタイムを超える可能性があるイベントは、管理されていないリソースと見なす必要があります。「アンマネージリソース」というフレーズの「アンマネージ」という用語は、「マネージコードの世界の完全に外側」を意味するのではなく、オブジェクトがマネージガベージコレクターによって提供される以上のクリーンアップを必要とするかどうかに関係します。

たとえば、コレクションがCollectionChangedイベントを公開する場合があります。そのようなイベントをサブスクライブする他のタイプのオブジェクトが繰り返し作成されて放棄された場合、コレクションはそのようなすべてのオブジェクトへのデリゲート参照を保持することになります。そのような作成と放棄が1秒に1回発生する場合(問題のオブジェクトがUIウィンドウを更新するルーチンで作成された場合のように)、そのような参照の数は、プログラムが実行されている1日あたり86,000を超える可能性があります。数分以上実行されないプログラムにとっては大きな問題ではありませんが、一度に数週間実行される可能性のあるプログラムにとっては絶対的なキラーです。

Microsoftがvb.netまたはC#でより良いイベントクリーンアップパターンを思い付かなかったことは本当に残念です。イベントをサブスクライブするクラスインスタンスが、イベントが破棄される前にそれらをクリーンアップする必要がない理由はほとんどありませんが、Microsoftはそのようなクリーンアップを容易にするために何もしませんでした。実際には、イベントにサブスクライブされているオブジェクトを放棄することで、イベントを適切にクリーンアップするために必要な煩わしいレベルの作業が行われないように(イベント発行者はサブスクライバーとほぼ同時にスコープから外れるため)回避できます。価値がないようです。残念ながら、イベント発行者が予想よりも長生きする可能性があるすべてのケースを予測することは必ずしも容易ではありません。多くのクラスがイベントをぶら下げたままにしておくと、それは

編集に応じた補遺

Xからのイベントをサブスクライブしてから、へのYすべての参照を破棄し、収集の対象になった場合でも、収集がY妨げられることはありません。それは良いことです。それを処分できるようにする目的で強い参照を保持する場合、そのような参照は収集されないでしょう。それは間違いなくそれほど良いことではないかもしれません。状況によっては、直接参照するよりも、長い(2番目のパラメーターをに設定して構築されたもの)を維持する方がよい場合があります。がdのときにターゲットがnull以外の場合、からのサブスクライブを解除する必要がありますYXYXYYXWeakReferencetrueYWeakReferenceXDisposeYのイベント。ターゲットがnullの場合、サブスクライブを解除することはできませんが、それまでにY(およびその参照X)が完全に存在しなくなるため、問題にはなりません。万が一、Y死亡して復活した場合Xでも、そのイベントの購読を解除する必要があることに注意してください。longWeakReferenceを使用すると、それが引き続き発生する可能性があります。

Xを参照する必要はなくYYある種の弱いイベントディスパッチを使用するように作成する必要があると主張する人もいますが、一般的なケースでは、他のことを行うYかどうかを判断する方法がないため、このような動作は正しくありません。への唯一の参照を保持している場合でも、Xコードは気にする可能性があります。強力にルート化されたオブジェクトへの参照を保持し、そのイベントハンドラー内で他のオブジェクトに対して何かを行う可能性は完全にあります。への唯一の参照を保持しているという事実は、他のオブジェクトが「関心を持っていない」ことを意味するものではありません。YXXYXX。一般的に正しい唯一の解決策は、他のオブジェクトのイベントに関心がなくなったオブジェクトに、後者のオブジェクトにその事実を通知させることです。

于 2012-07-16T17:24:35.510 に答える
1

MSDN リファレンス

「イベントが発生したときにイベント ハンドラーが呼び出されないようにするには、イベントのサブスクライブを解除します。リソース リークを防ぐために、サブスクライバー オブジェクトを破棄する前にイベントのサブスクライブを解除する必要があります。イベントのサブスクライブを解除するまで、マルチキャスト デリゲートはパブリッシング オブジェクトのイベントの基礎となるオブジェクトには、サブスクライバーのイベント ハンドラーをカプセル化するデリゲートへの参照があります。パブリッシング オブジェクトがその参照を保持している限り、ガベージ コレクションはサブスクライバー オブジェクトを削除しません。

「すべてのサブスクライバーがイベントからサブスクライブを解除すると、パブリッシャー クラスのイベント インスタンスが null に設定されます。」

于 2012-07-18T22:22:07.670 に答える
1

クラス B にも IDisposable を実装させ、その破棄ルーチンで、最初に A が null でないかどうかを確認してから A を破棄します。他のすべての破棄を処理します。

于 2012-07-16T15:49:14.013 に答える
0

オブジェクトを破棄するときに、イベントハンドラーのフックを解除する必要はありませんが、必要になる場合があります。つまり、GCは、ユーザーの介入なしにイベントハンドラーを正常にクリーンアップしますが、シナリオによっては、GCが実行する前に、これらのイベントハンドラーを削除して、ハンドラーが呼び出されないようにすることができます。それを期待しています。

あなたの例では、あなたの役割が逆になっていると思います-クラスAは、他の人によって追加されたイベントハンドラーのサブスクライブを解除するべきではなく、イベントハンドラーを削除する必要はありません。代わりに、それらのイベントの発生を停止することができます。

ただし、状況が逆転したとします。

class A
{
   public EventHandler EventHappened;
}

class B : IDisposable
{
    A _a;
    private bool disposed;

    public B(A a)
    {
        _a = a;
        a.EventHappened += this.HandleEvent;
    }

    public void Dispose(bool disposing)
    {
        // As an aside - if disposing is false then we are being called during 
        // finalization and so cannot safely reference _a as it may have already 
        // been GCd
        // In this situation we dont to remove the handler anyway as its about
        // to be cleaned up by the GC anyway
        if (disposing)
        {
            // You may wish to unsubscribe from events here
            _a.EventHappened -= this.HandleEvent;
            disposed = true;
        }
    }

    public void HandleEvent(object sender, EventArgs args)
    {
        if (disposed)
        {
            throw new ObjectDisposedException();
        }
    }
 }

A破棄された後もイベントを発生させ続けることが可能でBあり、のイベントハンドラーが、破棄されたB場合に例外またはその他の予期しない動作を引き起こす可能性がある場合Bは、最初にこのイベントのサブスクライブを解除することをお勧めします。

于 2012-07-16T16:47:54.560 に答える
0

オブジェクトAは、EventHandlerデリゲートを介してBを参照します(Aには、Bを参照するEventHandlerのインスタンスがあります)。BにはAへの参照がありません。Aがnullに設定されている場合、Aは収集され、メモリが解放されます。したがって、この場合は何もクリアする必要はありません。

于 2012-07-19T13:23:07.400 に答える