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

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

// event構文を使ったイベント登録処理の公開
public event Action EventAction;

イベント構文を使ってイベントを登録した場合のオブジェクトの破棄周りの事を少し紹介していきたいと思います。

まずプログラムですがイベント構文を使用してメソッドを登録する処理を以下の通り書きます。

まずイベントを登録する親クラスとして「Parentクラス」を用意します。

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

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

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

そして、以下のようにイベントを登録して呼び出す処理を記述します。

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

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

    // 登録されたイベントを呼び出す(テスト用の操作)
    parent.CallEvents();

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

この後、sub1, sub2 の変数を捨ててsubへの参照がすべて失われたら、sub1, sub2 外からは解放できなくなります。登録時と同じ参照を持つインスタンスで登録解除(-=)しないと開放できません。ただしこの時点ではまだsub1, sub2のインスタンスは生存しています。これはParentが持つイベント経由でsubのメソッドが参照されているためです。

この状態だとsubはParentのインスタンスを捨てるまでずっとメモリ上に残った状態になります。更にCallEventsメソッドを呼び出すと(自分は捨てたと思っている)subの処理が呼び出されて普通に実行されます。

唯一イベントの登録解除ができるコードは以下の通りです。

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

またはParentクラスに以下のようにクリア処理を追加してもよいかもしれません。

// Parent.cs

public event Action EventAction;

public void ClearEvents()
{
    this.EventAction = null; // これで全部消える
}

ただしこれだと全部開放する or 残るしか選べないので個別のインスタンスの識別はできないため個別に開放したい場合別途仕組みが必要です。

parent変数を捨てた場合、オブジェクトは解放されますが開放のタイミングはGC任せなので、タイマーを動かしっぱなしで捨てた場合、GCが回収してオブジェクトが完全に破棄されるまでの間に何度かsubのメソッド呼び出しが予期せず発生するため必要であればIDisposableを付与して開放を明示してあげる必要があります。

IDisposableの実装例は以下のようになります。

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

    // ★★★ IDisposableの実装
    public void Dispose()
    {
        this._EventAction = null; // イベント登録をすべて解除(必要ないと思うけど…
        
        // タイマー等を使用している場合最悪でもこのタイミングで止めておく
    }
}

var parent = new Parent();
parent.EventAction += ...

// ..中略..

// 使い終わったらDispoaseで開放する
using(parent) { }

【余談】デリゲートをイベント構文で登録すると解放できなくなる

イベント構文は、+= と -= のセットの記述でコードを記述するのはなためラムダ式を+=で登録することもできますがこれも注意が必要です。イベント構文ではイベントでは登録時と解放時で同じ参照必要なため、以下例のようなラムダ式を使い捨てで登録を行うとイベントが解除できなくなります(同様に、インスタンスがあるオブジェクトでもを登録直後に捨てしまうとイベントは解放できません)

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

従って最近ではイベント周りはReactive Extensionという外部ライブラリを利用する機会が多くなっています。

上記問題をある程度解決したうえで便利な機能拡張がされているのでこちらのサイトに説明があるので興味があれば使用してみるといいと思います。

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