web-dev-qa-db-ja.com

イベントはC#でどのようにメモリリークを引き起こし、弱参照はそれをどのように軽減するのに役立ちますか?

C#で意図しないメモリリークを引き起こすには、(私が知っている)2つの方法があります。

  1. IDisposableを実装するリソースを破棄しない
  2. イベントの参照と逆参照が正しくありません。

2点目はよくわかりません。ソースオブジェクトの存続期間がリスナーより長く、他に参照がないときにリスナーがイベントを必要としない場合、通常の.NETイベントを使用すると、メモリリークが発生します。ソースオブジェクトは、リスナーオブジェクトをメモリに保持します。ガベージコレクションする必要があります。

イベントがC#のコードでメモリリークを引き起こす可能性がある方法と、弱参照を使用して弱参照なしでそれを回避するためのコードを作成する方法を説明できますか?

27
jnvjgt

リスナーがイベントリスナーをイベントにアタッチすると、ソースオブジェクトはリスナーオブジェクトへの参照を取得します。これは、イベントハンドラーがデタッチされるか、ソースオブジェクトが収集されるまで、リスナーをガベージコレクターで収集できないことを意味します。

次のクラスを検討してください。

class Source
{
    public event EventHandler SomeEvent;
}

class Listener
{
    public Listener(Source source)
    {
        // attach an event listner; this adds a reference to the
        // source_SomeEvent method in this instance to the invocation list
        // of SomeEvent in source
        source.SomeEvent += new EventHandler(source_SomeEvent);
    }

    void source_SomeEvent(object sender, EventArgs e)
    {
        // whatever
    }
}

...そして次のコード:

Source newSource = new Source();
Listener listener = new Listener(newSource);
listener = null;

nulllistenerに割り当てても、newSourceはまだイベントハンドラーへの参照を保持しているため、ガベージコレクションの対象にはなりません(Listener.source_SomeEvent)。この種のリークを修正するには、イベントリスナーが不要になったときに常にデタッチすることが重要です。

上記のサンプルは、リークの問題に焦点を当てて作成されています。そのコードを修正するには、おそらくListenerSourceへの参照を保持させて、後でイベントリスナーを切り離すことができるようにするのが最も簡単です。

class Listener
{
    private Source _source;
    public Listener(Source source)
    {
        _source = source;
        // attach an event listner; this adds a reference to the
        // source_SomeEvent method in this instance to the invocation list
        // of SomeEvent in source
        _source.SomeEvent += source_SomeEvent;
    }

    void source_SomeEvent(object sender, EventArgs e)
    {
        // whatever
    }

    public void Close()
    {
        if (_source != null)
        {
            // detach event handler
            _source.SomeEvent -= source_SomeEvent;
            _source = null;
        }
    }
}

次に、呼び出し元のコードは、オブジェクトを使用して完了したことを通知できます。これにより、Sourceが「リスナー」に対して持つ必要のある参照が削除されます。

Source newSource = new Source();
Listener listener = new Listener(newSource);
// use listener
listener.Close();
listener = null;
35
Fredrik Mörk

イベントに関するJonSkeetの優れた 記事 を読んでください。これは、古典的な意味での真の「メモリリーク」ではありませんが、切断されていない保持された参照の多くです。したがって、前の時点で-=するイベントハンドラーを+=することを常に忘れないでください。そうすれば、ゴールデンになるはずです。

12
Jesse C. Slicer

厳密に言えば、マネージド.NETプロジェクトの「サンドボックス」内に「メモリリーク」はありません。開発者が必要と考えるよりも長く保持されている参照のみがあります。フレドリクにはその権利があります。ハンドラーをイベントにアタッチすると、ハンドラーは通常インスタンスメソッド(インスタンスを必要とする)であるため、リスナーを含むクラスのインスタンスは、この参照が維持されている限りメモリに残ります。リスナーインスタンスに他のクラスへの参照が順番に含まれている場合(たとえば、オブジェクトを含むことへの逆参照)、リスナーが他のすべてのスコープから外れた後も、ヒープは非常に大きくなる可能性があります。

DelegateとMulticastDelegateについてもう少し難解な知識を持っている人なら、これに光を当てることができるかもしれません。私の見方では、次のすべてが当てはまる場合、真のリークが発生する可能性があります。

  • イベントリスナーは、IDisposableを実装することにより、外部/管理されていないリソースを解放する必要がありますが、そうではないか、
  • イベントマルチキャストデリゲートは、オーバーライドされたFinalize()メソッドからDispose()メソッドを呼び出さず、
  • イベントを含むクラスは、独自のIDisposable実装を介して、またはFinalize()で、デリゲートの各ターゲットでDispose()を呼び出しません。

デリゲートターゲットでDispose()を呼び出すことを含むベストプラクティスは聞いたことがありません。イベントリスナーははるかに少ないので、.NET開発者はこの場合の動作を知っているとしか思えません。これが当てはまり、イベントの背後にあるMulticastDelegateがリスナーを適切に破棄しようとする場合、必要なのは、破棄が必要なリスニングクラスにIDisposableを適切に実装することだけです。

2
KeithS