C#の自動実装のイベントは解放されるのか?

C#では、プロパティのイベント版ともいえる「自動実装イベント」を以下のように宣言することができます。

public event Action EventAction;

上述のコードですが、イベントを登録した事によって親オブジェクトもしくは子オブジェクトが破棄されないのでは?と、考えたことありませんか?

説明のために以下のイベントを登録する先のObserverクラスを用意します。

// イベントを登録する親のクラス
public class Observer
{
    // 自動実装のイベント
    public event Action EventAction;
    
    // 登録されているイベントを全部呼び出す
    public void CallEvents() => this.EventAction?.Invoke();
}

更に、Observerに登録するクラスメソッドを持つクラスを以下のように定義します。

// イベントに登録するメソッドを持つクラス
public class Subject
{
    // イベントに登録するメソッド
    public void SubjectAction()
    {
        Console.WriteLine("Action!");
    }
}

そして、イベントを登録して呼び出す処理を記述します。

public static void Main(string[] args)
{
    var observer = new Observer();
    
    var sub1 = new Subject();
    var sub2 = new Subject();

    // イベントを登録する
    observer.EventAction += sub1.SubjectAction;
    observer.EventAction += sub2.SubjectAction;

    // 登録されたイベントを呼び出す
    observer.CallEvents();

    // 出力内容
    // > "Action!"
    // > "Action!"
}

この後、sub1, sub2 の変数を捨てて、プログラム上で参照がすべて失われたらどうなるでしょう?

答えは、「sub1, sub2 は解放されません」

これは、observer がイベントとしてsubへの参照を持っているため参照が残っているからです。変数から値が勝手に消える事は無いため CallEvents() を呼ぶごとにsub1 sub2の SubjectAction() の処理が実行されます。

また、sub1, 2 に加え observer の変数を捨てた場合、、、、observer は論理的には解放されます。(ただ、確かにGCに回収されるのですが、何故か捨ててからかなり長い時間解放されなかったり、GC.Collect() を自分で呼び出すまで解放されない事があったため、GCの対象として認識されていますが、次回以降のGC動作時に早めに開放という事にはならないようです。子への参照ってGCには無関係だと考えていますが理由は不明です…

ですから、もうイベントを発生させたくない場合やSubjectのオブジェクトを捨てたい時は以下コード例の通り、「-=」を使用してイベント登録を必ず解放する必要があります。

observer.EventAction -= sub1.SubjectAction;
observer.EventAction -= sub2.SubjectAction;

また Observer を捨てる際に保持しているイベントを解放したほうがいいと考えているので、IDisposableを継承し以下のように処理を追加します。

// イベントを登録する親のクラス
public class Observer : IDisposable // ← ★★★ 追加
{
    // 自動実装のイベント
    public event Action EventAction;
    
    // 登録されているイベントを全部呼び出す
    public void CallEvents() => this.EventAction?.Invoke();

    // ★★★ IDisposableの実装
    public void Dispose()
    {
        this._EventAction = null; // イベント登録をすべて解除(必要ないと思うけど…
    }
}

var observer = new Observer();
observer.EventAction += ...

// ..中略..

// 使い終わったらDispoaseする
using(observer) { }

【余談】デリゲートで登録すると解放できなくなる

イベント構文は、+= と -= のセットの記述でコードを記述するのは面倒です。

また、昨今ラムダ式の利用頻度が高まっていますが、イベントでは登録時と解放時で同じ参照必要なため、以下例のようなラムダ式の登録を行うとイベントが解除できなくなります。

同様に、インスタンスを登録直後に捨てしまうとイベントは解放できません。

public static void Main(string[] args)
{
    var observer = new Observer();
    
    // イベントを登録するするときに参照を覚えていないと解放できなくなる
    observer.EventAction += new Subject().SubjectAction;
    // デリゲートを単体で登録すると解放できなくなる
    observer.EventAction += ()=> Console.WriteLine("Action!");
    
    // ..中略..
    
    // 参照が異なるので解放できない(処理は同じだが参照が異なる
    observer.EventAction -= new Subject().SubjectAction;
    observer.EventAction -= Console.WriteLine("Action!");
}

こちらのサイトにRxライブラリを使った解決法があります。

上記ラムダ式をIDisposableのオブジェクトでくるんで使い終わったら -= じゃなくて、Disposeで開放しましょうという記事です。

最近は外部ライブラリもNuGetから簡単に導入できるので積極的うと幸せになれるかもしれません。